diff --git a/eAmuseCore/KBinXML/EnumHelpers.cs b/eAmuseCore/KBinXML/EnumHelpers.cs index 239e417..3d9e0fd 100644 --- a/eAmuseCore/KBinXML/EnumHelpers.cs +++ b/eAmuseCore/KBinXML/EnumHelpers.cs @@ -189,27 +189,38 @@ namespace eAmuseCore.KBinXML.Helpers list.AddRange(bytes); } - public static IEnumerable TakeBytesAligned(ref IEnumerable input, int size = -1) + public static void Realign(this List list, int alignment = 4) { - if (size < 0) - { - size = input.FirstS32(); - input = input.Skip(4); - } + int align = alignment - (list.Count % alignment); + if (align == alignment) + return; + while (align-- > 0) + list.Add(0); + } + public static void AddRangeAligned(this List list, IEnumerable data, int alignment = 4) + { + list.AddRange(data); + list.Realign(alignment); + } + + public static IEnumerable TakeBytesAligned(ref IEnumerable input, int size, int alignment = 4) + { var res = input.Take(size); input = input.Skip(size); - int align = 4 - (size % 4); - if (align != 4) + int align = alignment - (size % alignment); + if (align != alignment) input = input.Skip(align); return res; } - public static string TakeStringAligned(ref IEnumerable input, Encoding encoding, int size = -1) + public static string TakeStringAligned(ref IEnumerable input, Encoding encoding, int alignment = 4) { - byte[] data = TakeBytesAligned(ref input, size).ToArray(); + int size = input.FirstS32(); + input = input.Skip(4); + byte[] data = TakeBytesAligned(ref input, size, alignment).ToArray(); return encoding.GetString(data, 0, data.Length - 1); // drop final null byte } } diff --git a/eAmuseCore/KBinXML/KBinXML.cs b/eAmuseCore/KBinXML/KBinXML.cs index 7569cf0..97fa9e8 100644 --- a/eAmuseCore/KBinXML/KBinXML.cs +++ b/eAmuseCore/KBinXML/KBinXML.cs @@ -88,11 +88,187 @@ namespace eAmuseCore.KBinXML private void Generate() { List header = new List(8); + header.AddU8(SIGNATURE); + + if (compressed) + header.AddU8(SIG_COMPRESSED); + else + header.AddU8(SIG_UNCOMPRESSED); + + byte encodingSig = GetEncodingSig(BinEncoding); + header.AddU8(encodingSig); + header.AddU8((byte)(0xFF ^ encodingSig)); + + nodeList = new List(); + dataList = new List(); + dataByteOffset = dataWordOffset = 0; + + GenerateNode(Document.Root); + + nodeList.AddU8(XmlTypes.XmlTypes.SectionEndType | 64); + nodeList.Realign(); + + header.AddU32((uint)nodeList.Count); + nodeList.AddU32((uint)dataList.Count); + + Bytes = header.Concat(nodeList).Concat(dataList).ToArray(); + + nodeList = dataList = null; } private List nodeList = null, dataList = null; private int dataByteOffset = 0, dataWordOffset = 0; + private bool NodeIsMixed(XElement element) + { + bool text = false; + bool nontext = false; + + foreach (XNode node in element.Nodes()) + { + if (node.NodeType == System.Xml.XmlNodeType.Text) + text = true; + else + nontext = true; + + if (text && nontext) + return true; + } + + return false; + } + + private void AddNodeName(string name) + { + if (compressed) + { + nodeList.AddRange(SixBit.Pack(name)); + } + else + { + byte[] bytes = BinEncoding.GetBytes(name); + nodeList.AddU8((byte)((bytes.Length - 1) | 64)); + nodeList.AddRange(bytes); + } + } + + private void AddDataAligned(byte[] data) + { + if (data.Length == 1) + { + if (dataByteOffset % 4 == 0) + { + dataByteOffset = dataList.Count; + dataList.AddU32(0); + } + dataList[dataByteOffset++] = data[0]; + } + else if (data.Length == 2) + { + if(dataWordOffset % 4 == 0) + { + dataWordOffset = dataList.Count; + dataList.AddU32(0); + } + dataList[dataWordOffset++] = data[0]; + dataList[dataWordOffset++] = data[1]; + } + else + { + dataList.AddRangeAligned(data); + } + } + + private void AddStringAligned(string str) + { + byte[] bytes = BinEncoding.GetBytes(str); + dataList.AddS32(bytes.Length + 1); + dataList.AddRange(bytes); + dataList.AddU8(0); + dataList.Realign(); + } + + private void GenerateNode(XElement node) + { + if (NodeIsMixed(node)) + throw new ArgumentException("Nodes with mixed elements/text are not supported.", "node"); + + XAttribute nodeTypeXAttr = node.Attribute("__type"); + KValueAttribute nodeTypeAttrs; + if (nodeTypeXAttr != null) + { + nodeTypeAttrs = KValueAttribute.GetAttrByName(nodeTypeXAttr.Value.ToLower()); + } + else + { + if (node.IsEmpty || node.HasElements) + nodeTypeAttrs = KValueAttribute.GetAttrByType(XmlTypes.XmlTypes.VoidType); + else + nodeTypeAttrs = KValueAttribute.GetAttrByType(XmlTypes.XmlTypes.StrType); + } + + bool isArray = false; + int count = 1; + XAttribute countXAttr = node.Attribute("__count"); + if (countXAttr != null) + { + count = Convert.ToInt32(countXAttr.Value); + isArray = true; + } + + nodeList.AddU8((byte)(nodeTypeAttrs.NodeType | (isArray ? 64 : 0))); + AddNodeName(node.Name.LocalName); + + if (nodeTypeAttrs.NodeType != XmlTypes.XmlTypes.VoidType) + { + Type valueType = XmlTypes.XmlTypes.GetByType(nodeTypeAttrs.NodeType); + + IEnumerable data; + if (nodeTypeAttrs.NodeType == XmlTypes.XmlTypes.StrType) + { + if (count != 1) + throw new FormatException("String value cannot have a count != 1."); + + data = BinEncoding.GetBytes(node.Value).Concat(new byte[] { 0 }); + } + else if (nodeTypeAttrs.NodeType == XmlTypes.XmlTypes.BinType) + { + data = XmlTypes.Bin.FromString(node.Value).ToBytes(); + } + else + { + IKValue kValue = XmlTypes.XmlTypes.KValueFromString(valueType, node.Value, count); + data = kValue.ToBytes(); + } + + if (isArray || nodeTypeAttrs.Count < 0) + { + dataList.AddU32((uint)data.Count()); + dataList.AddRangeAligned(data); + } + else + { + AddDataAligned(data.ToArray()); + } + } + + foreach (XAttribute attr in node.Attributes()) + { + if (new[] { "__type", "__size", "__count" }.Contains(attr.Name.LocalName)) + continue; + + nodeList.AddU8(XmlTypes.XmlTypes.AttrType); + AddNodeName(attr.Name.LocalName); + + AddStringAligned(attr.Value); + } + + foreach (XElement child in node.Elements()) + GenerateNode(child); + + nodeList.AddU8(XmlTypes.XmlTypes.NodeEndType | 64); + } + private void Parse() { IEnumerable input = Bytes; @@ -254,7 +430,7 @@ namespace eAmuseCore.KBinXML case XmlTypes.XmlTypes.SectionEndType: nodesLeft = false; break; - case XmlTypes.XmlTypes.NodeStartType: + case XmlTypes.XmlTypes.VoidType: startNode = true; break; default: diff --git a/eAmuseCore/KBinXML/SixBit.cs b/eAmuseCore/KBinXML/SixBit.cs index ad3a89c..338b0ae 100644 --- a/eAmuseCore/KBinXML/SixBit.cs +++ b/eAmuseCore/KBinXML/SixBit.cs @@ -4,10 +4,11 @@ using System.Linq; using System; using eAmuseCore.KBinXML.Helpers; +using System.Text; namespace eAmuseCore.KBinXML { - static class SixBit + public static class SixBit { static readonly string charmap = "0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; static readonly Dictionary bytemap = new Dictionary(); @@ -23,7 +24,10 @@ namespace eAmuseCore.KBinXML if (input.Length > byte.MaxValue) throw new ArgumentException("input string is too long", "input"); - int padding = (8 - (input.Length * 6)) % 8; + int length_bits = input.Length * 6; + int length_bytes = (length_bits + 7) / 8; + int padding = (8 - (length_bits % 8)) % 8; + BigInteger bits = new BigInteger(0); try @@ -41,7 +45,7 @@ namespace eAmuseCore.KBinXML bits <<= padding; - return bits.ToByteArray().Append((byte)input.Length).Reverse().ToArray(); + return bits.ToByteArray().Take(length_bytes).Append((byte)input.Length).Reverse().ToArray(); } public static string Unpack(byte[] data) diff --git a/eAmuseCore/KBinXML/TypeHelpers.cs b/eAmuseCore/KBinXML/TypeHelpers.cs index 50a7c1f..62c59ed 100644 --- a/eAmuseCore/KBinXML/TypeHelpers.cs +++ b/eAmuseCore/KBinXML/TypeHelpers.cs @@ -113,12 +113,18 @@ namespace eAmuseCore.KBinXML.Helpers if (Value == null) return Enumerable.Empty(); - IEnumerable res = BitConverter.GetBytes(Convert.ToUInt64(Value)); + dynamic v = Value; + Type t = v.GetType(); + + if (t == typeof(byte) || t == typeof(sbyte)) + return new byte[] { unchecked((byte)v) }; + + IEnumerable res = BitConverter.GetBytes(v); if (BitConverter.IsLittleEndian) res = res.Reverse(); - return res.Skip(8 - Size); + return res; } protected KValueAttribute KValAttr diff --git a/eAmuseCore/KBinXML/XmlTypes.cs b/eAmuseCore/KBinXML/XmlTypes.cs index ee0c538..77ab203 100644 --- a/eAmuseCore/KBinXML/XmlTypes.cs +++ b/eAmuseCore/KBinXML/XmlTypes.cs @@ -99,7 +99,7 @@ namespace eAmuseCore.KBinXML.XmlTypes static public Bin FromString(string input) { - if ((input.Length % 2) != 2) + if ((input.Length % 2) != 0) throw new ArgumentException("Hex string needs to consist of pairs of two chars.", "input"); byte[] res = new byte[input.Length / 2]; for (int i = 0, j = 0; i < input.Length; i += 2, ++j) @@ -119,6 +119,8 @@ namespace eAmuseCore.KBinXML.XmlTypes public override IEnumerable ToBytes() => Encoding.UTF8.GetBytes(Value); + public IEnumerable ToBytes(Encoding encoding) => encoding.GetBytes(Value); + static public Str FromString(string input) => new Str(input); static public Str FromBytes(IEnumerable input, Encoding encoding) @@ -527,7 +529,7 @@ namespace eAmuseCore.KBinXML.XmlTypes public static class XmlTypes { - public const int NodeStartType = 1; + public const int VoidType = 1; public const int BinType = 10; public const int StrType = 11; public const int AttrType = 46; @@ -583,7 +585,7 @@ namespace eAmuseCore.KBinXML.XmlTypes } } - public static IKValue ValueListFromString(Type type, string input, int count) + public static IKValue KValueFromString(Type type, string input, int count) { Type listType = typeof(KValueArray<>).MakeGenericType(type); KValueAttribute attr = type.GetCustomAttribute(false); @@ -595,14 +597,17 @@ namespace eAmuseCore.KBinXML.XmlTypes if (vals.Length != attr.Count * count) throw new ArgumentException("input string had invalid field count", "input"); - object[] mainParams = new object[count]; + IKValue[] mainParams = new IKValue[count]; for (int i = 0; i < count; ++i) { - mainParams[i] = fromString.Invoke(null, new object[] { string.Join(" ", strings.Take(attr.Count)) }); + mainParams[i] = (IKValue)fromString.Invoke(null, new object[] { string.Join(" ", strings.Take(attr.Count)) }); strings = strings.Skip(attr.Count); } + if (count == 1) + return mainParams[0]; + return (IKValue)Activator.CreateInstance(listType, mainParams); } diff --git a/eAmuseCore/eAmuseCore.csproj b/eAmuseCore/eAmuseCore.csproj index 1099f21..0ba7584 100644 --- a/eAmuseCore/eAmuseCore.csproj +++ b/eAmuseCore/eAmuseCore.csproj @@ -5,6 +5,7 @@ + diff --git a/eAmuseTest/Program.cs b/eAmuseTest/Program.cs index 545ab83..0689446 100644 --- a/eAmuseTest/Program.cs +++ b/eAmuseTest/Program.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using eAmuseCore.Crypto; using eAmuseCore.Compression; using eAmuseCore.KBinXML; +using eAmuseCore.KBinXML; +using System.Xml.Linq; namespace eAmuseTest { @@ -47,9 +49,20 @@ namespace eAmuseTest throw new ArgumentException("Unsupported compression algorithm"); KBinXML kbinxml = new KBinXML(rawData); - KBinXML testDoc = new KBinXML(ExtractResource("eAmuseTest.testcases_out.kbin")); + //KBinXML testDoc = new KBinXML(ExtractResource("eAmuseTest.testcases_out.kbin")); - Console.WriteLine(testDoc.ToString()); + KBinXML reverseTest = new KBinXML(kbinxml.Document); + + Console.WriteLine(kbinxml); + + Console.WriteLine("Orig: " + BytesToString(rawData)); + Console.WriteLine("Len: " + rawData.Count()); + Console.WriteLine("Mine: " + BytesToString(reverseTest.Bytes)); + Console.WriteLine("Len: " + reverseTest.Bytes.Count()); + + KBinXML reReverseText = new KBinXML(reverseTest.Bytes); + + Console.WriteLine(reReverseText); } private static string BytesToString(IEnumerable bytes)