diff --git a/src/Benchmarks/WriteBenchmark/data/binaries.xml b/src/Benchmarks/WriteBenchmark/data/binaries.xml new file mode 100644 index 0000000..a8793f5 --- /dev/null +++ b/src/Benchmarks/WriteBenchmark/data/binaries.xml @@ -0,0 +1,11 @@ + + f710b5f1c5c56a69f710b5f1b836de22 + f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22 + f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22f710b5f1c5c56a69f710b5f1b836de22 + + + F34255041131EDDFC769181B8F33892E + AA4A965AA8C2C169D145E75B5DA93879CD8AD1A3F32185662DC54341263DBB03 + 1CE9481CA73F4B0AD6867EB0D51A0E1672946BE5B6D1B109F327348C9B7CBB2C15781A0482C3953C + + \ No newline at end of file diff --git a/src/KbinXml.Net/KbinConverter.Writers.cs b/src/KbinXml.Net/KbinConverter.Writers.cs index 9cbc511..701cb05 100644 --- a/src/KbinXml.Net/KbinConverter.Writers.cs +++ b/src/KbinXml.Net/KbinConverter.Writers.cs @@ -89,7 +89,7 @@ private static byte[] WriterImpl(Encoding encoding, WriteContext context, XmlRea var holdingAttrs = new SortedDictionary(StringComparer.Ordinal); string holdingValue = ""; string? typeStr = null; - string? sizeStr = null; + string? arrayCountStr = null; byte typeid = 0; void EnsureHolding() @@ -104,23 +104,23 @@ void EnsureHolding() { var type = TypeDictionary.TypeMap[typeid]; var value = holdingValue.SpanSplit(' '); - var size = (uint)(type.Size * type.Count); - if (sizeStr != null) + var requiredBytes = (uint)(type.Size * type.Count); + if (arrayCountStr != null) { - size *= uint.Parse(sizeStr); - context.DataWriter.WriteU32(size); + requiredBytes *= uint.Parse(arrayCountStr); + context.DataWriter.WriteU32(requiredBytes); } - if (size > int.MaxValue) + if (requiredBytes > int.MaxValue) throw new KbinException("uint size is greater than int.MaxValue"); - var iSize = (int)size; + var iRequiredBytes = (int)requiredBytes; byte[]? arr = null; - var span = iSize <= Constants.MaxStackLength - ? stackalloc byte[iSize] - : arr = ArrayPool.Shared.Rent(iSize); + var span = iRequiredBytes <= Constants.MaxStackLength + ? stackalloc byte[iRequiredBytes] + : arr = ArrayPool.Shared.Rent(iRequiredBytes); - if (arr != null) span = span.Slice(0, iSize); + if (arr != null) span = span.Slice(0, iRequiredBytes); var builder = new ValueListBuilder(span); try @@ -130,7 +130,7 @@ void EnsureHolding() { try { - if (i == iSize) break; + if (i == iRequiredBytes) break; var add = type.WriteString(ref builder, s); if (add < type.Size) { @@ -156,7 +156,7 @@ void EnsureHolding() } typeStr = null; - sizeStr = null; + arrayCountStr = null; holdingValue = ""; typeid = 0; } @@ -165,8 +165,6 @@ void EnsureHolding() { foreach (var attribute in holdingAttrs) { - if (attribute.Key == "__size") continue; - context.NodeWriter.WriteU8(0x2E); context.NodeWriter.WriteString(attribute.Key); context.DataWriter.WriteString(attribute.Value); @@ -194,7 +192,11 @@ void EnsureHolding() } else if (reader.Name == "__count") { - sizeStr = reader.Value; + arrayCountStr = reader.Value; + } + else if (reader.Name == "__size") + { + // ignore } else { @@ -213,7 +215,7 @@ void EnsureHolding() else { typeid = TypeDictionary.ReverseTypeMap[typeStr]; - if (sizeStr != null) + if (arrayCountStr != null) context.NodeWriter.WriteU8((byte)(typeid | 0x40)); else context.NodeWriter.WriteU8(typeid); @@ -250,10 +252,11 @@ void EnsureHolding() //Write header data var output = new BeBinaryWriter(); - output.WriteU8(0xA0); //Magic - output.WriteU8(0x42); //Compression flag - output.WriteU8(EncodingDictionary.ReverseEncodingMap[encoding]); - output.WriteU8((byte)~EncodingDictionary.ReverseEncodingMap[encoding]); + output.WriteU8(0xA0); // Signature + output.WriteU8((byte)(context.NodeWriter.Compressed ? 0x42 : 0x45)); // Compression flag + var encodingBytes = EncodingDictionary.ReverseEncodingMap[encoding]; + output.WriteU8(encodingBytes); + output.WriteU8((byte)~encodingBytes); //Write node buffer length and contents. var nodeStream = context.NodeWriter.Stream; diff --git a/src/KbinXml.Net/Readers/DataReader.cs b/src/KbinXml.Net/Readers/DataReader.cs index cf326ea..25f545f 100644 --- a/src/KbinXml.Net/Readers/DataReader.cs +++ b/src/KbinXml.Net/Readers/DataReader.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Text; using KbinXml.Net.Internal; +using KbinXml.Net.Utils; namespace KbinXml.Net.Readers; @@ -88,54 +89,7 @@ public string ReadBinary(int count) var bin = Read32BitAligned(count); if (bin.Length == 0) return string.Empty; -#if NETCOREAPP3_1_OR_GREATER - var str = string.Create(bin.Length * 2, bin, static (dst, state) => - { - var src = state.Span; - - int i = 0; - int j = 0; - - while (i < src.Length) - { - var b = src[i++]; - dst[j++] = ToCharLower(b >> 4); - dst[j++] = ToCharLower(b); - } - }); - return str; -#else - var src = bin.Span; - var dstLen = bin.Length * 2; - - char[]? arr = null; - Span dst = dstLen <= Constants.MaxStackLength - ? stackalloc char[dstLen] - : arr = ArrayPool.Shared.Rent(dstLen); - if (arr != null) dst = dst.Slice(0, dstLen); - try - { - int i = 0; - int j = 0; - - while (i < bin.Length) - { - var b = src[i++]; - dst[j++] = ToCharLower(b >> 4); - dst[j++] = ToCharLower(b); - } - - unsafe - { - fixed (char* p = dst) - return new string(p, 0, dstLen); - } - } - finally - { - if (arr != null) ArrayPool.Shared.Return(arr); - } -#endif + return ConvertHelper.ToHexString(bin.Span); } private Memory ReadBytes(int offset, int count) @@ -152,13 +106,4 @@ private void Realign16_8() if (_pos16 % 4 == 0) _pos16 = _pos32; } - - private static char ToCharLower(int value) - { - value &= 0xF; - value += '0'; - - if (value > '9') value += ('a' - ('9' + 1)); - return (char)value; - } } \ No newline at end of file diff --git a/src/KbinXml.Net/Utils/ConvertHelper.cs b/src/KbinXml.Net/Utils/ConvertHelper.cs index 14f22b0..1a61d9d 100644 --- a/src/KbinXml.Net/Utils/ConvertHelper.cs +++ b/src/KbinXml.Net/Utils/ConvertHelper.cs @@ -145,6 +145,16 @@ public static string Ip4ToString(ReadOnlySpan bytes) } } + public static string ToHexString(ReadOnlySpan bytes) + { + if (bytes.Length == 0) + return string.Empty; + if (bytes.Length > int.MaxValue / 2) + throw new ArgumentOutOfRangeException(nameof(bytes)); + + return HexConverter.ToString(bytes, HexConverter.Casing.Lower); + } + private static int IPv4AddressToStringHelper(uint address, Span dst) { int offset = 0; diff --git a/src/KbinXml.Net/Utils/HexConverter.cs b/src/KbinXml.Net/Utils/HexConverter.cs new file mode 100644 index 0000000..0f8b63a --- /dev/null +++ b/src/KbinXml.Net/Utils/HexConverter.cs @@ -0,0 +1,352 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +#if NETCOREAPP3_1_OR_GREATER +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace System +{ + internal static class HexConverter + { + public enum Casing : uint + { + // Output [ '0' .. '9' ] and [ 'A' .. 'F' ]. + Upper = 0, + + // Output [ '0' .. '9' ] and [ 'a' .. 'f' ]. + // This works because values in the range [ 0x30 .. 0x39 ] ([ '0' .. '9' ]) + // already have the 0x20 bit set, so ORing them with 0x20 is a no-op, + // while outputs in the range [ 0x41 .. 0x46 ] ([ 'A' .. 'F' ]) + // don't have the 0x20 bit set, so ORing them maps to + // [ 0x61 .. 0x66 ] ([ 'a' .. 'f' ]), which is what we want. + Lower = 0x2020U, + } + + // We want to pack the incoming byte into a single integer [ 0000 HHHH 0000 LLLL ], + // where HHHH and LLLL are the high and low nibbles of the incoming byte. Then + // subtract this integer from a constant minuend as shown below. + // + // [ 1000 1001 1000 1001 ] + // - [ 0000 HHHH 0000 LLLL ] + // ========================= + // [ *YYY **** *ZZZ **** ] + // + // The end result of this is that YYY is 0b000 if HHHH <= 9, and YYY is 0b111 if HHHH >= 10. + // Similarly, ZZZ is 0b000 if LLLL <= 9, and ZZZ is 0b111 if LLLL >= 10. + // (We don't care about the value of asterisked bits.) + // + // To turn a nibble in the range [ 0 .. 9 ] into hex, we calculate hex := nibble + 48 (ascii '0'). + // To turn a nibble in the range [ 10 .. 15 ] into hex, we calculate hex := nibble - 10 + 65 (ascii 'A'). + // => hex := nibble + 55. + // The difference in the starting ASCII offset is (55 - 48) = 7, depending on whether the nibble is <= 9 or >= 10. + // Since 7 is 0b111, this conveniently matches the YYY or ZZZ value computed during the earlier subtraction. + + // The commented out code below is code that directly implements the logic described above. + + // uint packedOriginalValues = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU); + // uint difference = 0x8989U - packedOriginalValues; + // uint add7Mask = (difference & 0x7070U) >> 4; // line YYY and ZZZ back up with the packed values + // uint packedResult = packedOriginalValues + add7Mask + 0x3030U /* ascii '0' */; + + // The code below is equivalent to the commented out code above but has been tweaked + // to allow codegen to make some extra optimizations. + + // The low byte of the packed result contains the hex representation of the incoming byte's low nibble. + // The adjacent byte of the packed result contains the hex representation of the incoming byte's high nibble. + + // Finally, write to the output buffer starting with the *highest* index so that codegen can + // elide all but the first bounds check. (This only works if 'startingIndex' is a compile-time constant.) + + // The JIT can elide bounds checks if 'startingIndex' is constant and if the caller is + // writing to a span of known length (or the caller has already checked the bounds of the + // furthest access). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToBytesBuffer(byte value, Span buffer, int startingIndex = 0, Casing casing = Casing.Upper) + { + uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U; + uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing; + + buffer[startingIndex + 1] = (byte)packedResult; + buffer[startingIndex] = (byte)(packedResult >> 8); + } + +#if ALLOW_PARTIALLY_TRUSTED_CALLERS + [System.Security.SecuritySafeCriticalAttribute] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToCharsBuffer(byte value, Span buffer, int startingIndex = 0, Casing casing = Casing.Upper) + { + uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U; + uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing; + + buffer[startingIndex + 1] = (char)(packedResult & 0xFF); + buffer[startingIndex] = (char)(packedResult >> 8); + } + +#if NETCOREAPP3_1_OR_GREATER + private static void EncodeToUtf16_Ssse3(ReadOnlySpan bytes, Span chars, Casing casing) + { + Debug.Assert(bytes.Length >= 4); + nint pos = 0; + + Vector128 shuffleMask = Vector128.Create( + 0xFF, 0xFF, 0, 0xFF, 0xFF, 0xFF, 1, 0xFF, + 0xFF, 0xFF, 2, 0xFF, 0xFF, 0xFF, 3, 0xFF); + + Vector128 asciiTable = (casing == Casing.Upper) + ? Vector128.Create((byte)'0', (byte)'1', (byte)'2', (byte)'3', + (byte)'4', (byte)'5', (byte)'6', (byte)'7', + (byte)'8', (byte)'9', (byte)'A', (byte)'B', + (byte)'C', (byte)'D', (byte)'E', (byte)'F') + : Vector128.Create((byte)'0', (byte)'1', (byte)'2', (byte)'3', + (byte)'4', (byte)'5', (byte)'6', (byte)'7', + (byte)'8', (byte)'9', (byte)'a', (byte)'b', + (byte)'c', (byte)'d', (byte)'e', (byte)'f'); + + do + { + // Read 32bits from "bytes" span at "pos" offset + uint block = Unsafe.ReadUnaligned( + ref Unsafe.Add(ref MemoryMarshal.GetReference(bytes), pos)); + + // Calculate nibbles + Vector128 lowNibbles = Ssse3.Shuffle( + Vector128.CreateScalarUnsafe(block).AsByte(), shuffleMask); + Vector128 highNibbles = Sse2.ShiftRightLogical( + Sse2.ShiftRightLogical128BitLane(lowNibbles, 2).AsInt32(), 4).AsByte(); + + // Lookup the hex values at the positions of the indices + Vector128 indices = Sse2.And( + Sse2.Or(lowNibbles, highNibbles), Vector128.Create((byte)0xF)); + Vector128 hex = Ssse3.Shuffle(asciiTable, indices); + + // The high bytes (0x00) of the chars have also been converted + // to ascii hex '0', so clear them out. + hex = Sse2.And(hex, Vector128.Create((ushort)0xFF).AsByte()); + + // Save to "chars" at pos*2 offset + Unsafe.WriteUnaligned( + ref Unsafe.As( + ref Unsafe.Add(ref MemoryMarshal.GetReference(chars), pos * 2)), hex); + + pos += 4; + } while (pos < bytes.Length - 3); + + // Process trailing elements (bytes.Length % 4) + for (; pos < bytes.Length; pos++) + { + ToCharsBuffer(Unsafe.Add(ref MemoryMarshal.GetReference(bytes), pos), chars, (int)pos * 2, casing); + } + } +#endif + + public static void EncodeToUtf16(ReadOnlySpan bytes, Span chars, Casing casing = Casing.Upper) + { + Debug.Assert(chars.Length >= bytes.Length * 2); + +#if NETCOREAPP3_1_OR_GREATER + if (Ssse3.IsSupported && bytes.Length >= 4) + { + EncodeToUtf16_Ssse3(bytes, chars, casing); + return; + } +#endif + for (int pos = 0; pos < bytes.Length; pos++) + { + ToCharsBuffer(bytes[pos], chars, pos * 2, casing); + } + } + +#if ALLOW_PARTIALLY_TRUSTED_CALLERS + [System.Security.SecuritySafeCriticalAttribute] +#endif + public static unsafe string ToString(ReadOnlySpan bytes, Casing casing = Casing.Upper) + { +#if NETFRAMEWORK || NETSTANDARD2_0 + Span result = stackalloc char[0]; + if (bytes.Length > 16) + { + var array = new char[bytes.Length * 2]; + result = array.AsSpan(); + } + else + { + result = stackalloc char[bytes.Length * 2]; + } + + int pos = 0; + foreach (byte b in bytes) + { + ToCharsBuffer(b, result, pos, casing); + pos += 2; + } + return result.ToString(); +#else + fixed (byte* bytesPtr = bytes) + { + return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length, casing), static (chars, args) => + { + var ros = new ReadOnlySpan((byte*)args.Ptr, args.Length); + EncodeToUtf16(ros, chars, args.casing); + }); + } +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char ToCharUpper(int value) + { + value &= 0xF; + value += '0'; + + if (value > '9') + { + value += ('A' - ('9' + 1)); + } + + return (char)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char ToCharLower(int value) + { + value &= 0xF; + value += '0'; + + if (value > '9') + { + value += ('a' - ('9' + 1)); + } + + return (char)value; + } + + public static bool TryDecodeFromUtf16(ReadOnlySpan chars, Span bytes) + { + return TryDecodeFromUtf16(chars, bytes, out _); + } + + public static bool TryDecodeFromUtf16(ReadOnlySpan chars, Span bytes, out int charsProcessed) + { + Debug.Assert(chars.Length % 2 == 0, "Un-even number of characters provided"); + Debug.Assert(chars.Length / 2 == bytes.Length, "Target buffer not right-sized for provided characters"); + + int i = 0; + int j = 0; + int byteLo = 0; + int byteHi = 0; + while (j < bytes.Length) + { + byteLo = FromChar(chars[i + 1]); + byteHi = FromChar(chars[i]); + + // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern + // is if either byteHi or byteLo was not a hex character. + if ((byteLo | byteHi) == 0xFF) + break; + + bytes[j++] = (byte)((byteHi << 4) | byteLo); + i += 2; + } + + if (byteLo == 0xFF) + i++; + + charsProcessed = i; + return (byteLo | byteHi) != 0xFF; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int FromChar(int c) + { + return c >= CharToHexLookup.Length ? 0xFF : CharToHexLookup[c]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int FromUpperChar(int c) + { + return c > 71 ? 0xFF : CharToHexLookup[c]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int FromLowerChar(int c) + { + if ((uint)(c - '0') <= '9' - '0') + return c - '0'; + + if ((uint)(c - 'a') <= 'f' - 'a') + return c - 'a' + 10; + + return 0xFF; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexChar(int c) + { + if (IntPtr.Size == 8) + { + // This code path, when used, has no branches and doesn't depend on cache hits, + // so it's faster and does not vary in speed depending on input data distribution. + // We only use this logic on 64-bit systems, as using 64 bit values would otherwise + // be much slower than just using the lookup table anyway (no hardware support). + // The magic constant 18428868213665201664 is a 64 bit value containing 1s at the + // indices corresponding to all the valid hex characters (ie. "0123456789ABCDEFabcdef") + // minus 48 (ie. '0'), and backwards (so from the most significant bit and downwards). + // The offset of 48 for each bit is necessary so that the entire range fits in 64 bits. + // First, we subtract '0' to the input digit (after casting to uint to account for any + // negative inputs). Note that even if this subtraction underflows, this happens before + // the result is zero-extended to ulong, meaning that `i` will always have upper 32 bits + // equal to 0. We then left shift the constant with this offset, and apply a bitmask that + // has the highest bit set (the sign bit) if and only if `c` is in the ['0', '0' + 64) range. + // Then we only need to check whether this final result is less than 0: this will only be + // the case if both `i` was in fact the index of a set bit in the magic constant, and also + // `c` was in the allowed range (this ensures that false positive bit shifts are ignored). + ulong i = (uint)c - '0'; + ulong shift = 18428868213665201664UL << (int)i; + ulong mask = i - 64; + + return (long)(shift & mask) < 0 ? true : false; + } + + return FromChar(c) != 0xFF; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexUpperChar(int c) + { + return (uint)(c - '0') <= 9 || (uint)(c - 'A') <= ('F' - 'A'); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexLowerChar(int c) + { + return (uint)(c - '0') <= 9 || (uint)(c - 'a') <= ('f' - 'a'); + } + + /// Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit. + public static ReadOnlySpan CharToHexLookup => new byte[] + { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 + 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 + 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 + }; + } +} \ No newline at end of file diff --git a/src/KbinXml.Net/Writers/DataWriter.cs b/src/KbinXml.Net/Writers/DataWriter.cs index 60cdfea..b905f69 100644 --- a/src/KbinXml.Net/Writers/DataWriter.cs +++ b/src/KbinXml.Net/Writers/DataWriter.cs @@ -70,7 +70,7 @@ public void WriteBinary(string value) if (arr != null) span = span.Slice(0, length); try { - FillHexBuilder(ref span, value); + HexConverter.TryDecodeFromUtf16(value.AsSpan(), span); Write32BitAligned(span); } finally @@ -152,27 +152,5 @@ private void Pad(int target) Stream.WriteByte(0); } } - - private static void FillHexBuilder(ref Span builder, string hex) - { - if (hex.Length % 2 == 1) - throw new Exception("The binary key cannot have an odd number of digits"); - - for (int i = 0; i < builder.Length; ++i) - { - builder[i] = ((byte)((GetHexVal(hex[i << 1]) << 4) + (GetHexVal(hex[(i << 1) + 1])))); - } - } - - private static int GetHexVal(char hex) - { - int val = (int)hex; - //For uppercase A-F letters: - //return val - (val < 58 ? 48 : 55); - //For lowercase a-f letters: - //return val - (val < 58 ? 48 : 87); - //Or the two combined, but a bit slower: - return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); - } } } \ No newline at end of file diff --git a/src/KbinXml.Net/Writers/NodeWriter.cs b/src/KbinXml.Net/Writers/NodeWriter.cs index 543a95d..ad3de1f 100644 --- a/src/KbinXml.Net/Writers/NodeWriter.cs +++ b/src/KbinXml.Net/Writers/NodeWriter.cs @@ -5,18 +5,18 @@ namespace KbinXml.Net.Writers { public class NodeWriter : BeBinaryWriter { - private readonly bool _compressed; + public bool Compressed { get; } private readonly Encoding _encoding; public NodeWriter(bool compressed, Encoding encoding) { - _compressed = compressed; + Compressed = compressed; _encoding = encoding; } public void WriteString(string value) { - if (_compressed) + if (Compressed) { WriteU8((byte)value.Length); SixbitHelper.EncodeAndWrite(Stream, value); diff --git a/src/Tests/GeneralUnitTests/WritingTests.cs b/src/Tests/GeneralUnitTests/WritingAndReadingTests.cs similarity index 74% rename from src/Tests/GeneralUnitTests/WritingTests.cs rename to src/Tests/GeneralUnitTests/WritingAndReadingTests.cs index 1d16889..48f6b4b 100644 --- a/src/Tests/GeneralUnitTests/WritingTests.cs +++ b/src/Tests/GeneralUnitTests/WritingAndReadingTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -7,13 +8,17 @@ using KbinXml.Net; using KbinXml.Net.Utils; using Xunit; +using Xunit.Abstractions; namespace GeneralUnitTests { - public class WritingTests + public class WritingAndReadingTests { - public WritingTests() + private readonly ITestOutputHelper _outputHelper; + + public WritingAndReadingTests(ITestOutputHelper outputHelper) { + _outputHelper = outputHelper; Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } @@ -28,13 +33,9 @@ public WritingTests() -35721234 -253178167252134 ")] - public void WriteNumbers(string value) + public void TestNumbers(string value) { - var xml = XElement.Parse(value); - var cvt = new kbinxmlcs.KbinWriter(xml, Encoding.UTF8); - var bytes = cvt.Write(); - var bytes2 = KbinConverter.Write(xml, KnownEncodings.UTF8); - Assert.Equal(bytes, bytes2); + DoWorks(value); } [Theory] @@ -54,11 +55,17 @@ public void WriteNumbers(string value) ")] public void WriteString(string value) { - var xml = XElement.Parse(value); - var cvt = new kbinxmlcs.KbinWriter(xml, Encoding.UTF8); - var bytes = cvt.Write(); - var bytes2 = KbinConverter.Write(xml, KnownEncodings.UTF8); - Assert.Equal(bytes, bytes2); + DoWorks(value); + } + + [Theory] + [InlineData(@" + 21 52 11 53 43 134 21 -43 -12 -61 -13 -52 -47 -114 21 52 11 53 43 134 21 -43 -12 -61 -13 -52 -47 -114 134 21 -43 -12 + +")] + public void TestArrayNotValid(string value) + { + DoWorks(value); } [Theory] @@ -68,13 +75,9 @@ public void WriteString(string value) -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 ")] - public void WriteArray(string value) + public void TestArray(string value) { - var xml = XElement.Parse(value); - var cvt = new kbinxmlcs.KbinWriter(xml, Encoding.UTF8); - var bytes = cvt.Write(); - var bytes2 = KbinConverter.Write(xml, KnownEncodings.UTF8); - Assert.Equal(bytes, bytes2); + DoWorks(value); } [Theory] @@ -83,13 +86,25 @@ public void WriteArray(string value) AA4A965AA8C2C169D145E75B5DA93879CD8AD1A3F32185662DC54341263DBB03 1CE9481CA73F4B0AD6867EB0D51A0E1672946BE5B6D1B109F327348C9B7CBB2C15781A0482C3953C ")] - public void WriteBinary(string value) + public void TestBinary(string value) + { + DoWorks(value); + } + + private void DoWorks(string value) { var xml = XElement.Parse(value); var cvt = new kbinxmlcs.KbinWriter(xml, Encoding.UTF8); var bytes = cvt.Write(); var bytes2 = KbinConverter.Write(xml, KnownEncodings.UTF8); + + var cvt2 = new kbinxmlcs.KbinReader(bytes2); + var result = cvt2.ReadLinq().ToString(); + var result2 = KbinConverter.ReadXmlLinq(bytes2).ToString(); + + _outputHelper.WriteLine(result2); Assert.Equal(bytes, bytes2); + Assert.Equal(result, result2); } } }