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);
}
}
}