-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add bit array converter use unsafe accessor
- Loading branch information
Showing
5 changed files
with
384 additions
and
1 deletion.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
code/Binary.Tests/Converters/BitArrayConverterInternalTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
namespace Mikodev.Binary.Tests.Converters; | ||
|
||
using System; | ||
using System.Linq; | ||
using System.Reflection; | ||
using Xunit; | ||
|
||
public class BitArrayConverterInternalTests | ||
{ | ||
private delegate void EncodeFunction(Span<byte> target, ReadOnlySpan<int> source, int length); | ||
|
||
private delegate void DecodeFunction(Span<int> target, ReadOnlySpan<byte> source, int length); | ||
|
||
private readonly EncodeFunction encode; | ||
|
||
private readonly DecodeFunction decode; | ||
|
||
public BitArrayConverterInternalTests() | ||
{ | ||
var type = typeof(IConverter).Assembly.GetTypes().Single(x => x.Name is "BitArrayConverter"); | ||
var encode = type.GetMethod("EncodeContents", BindingFlags.Static | BindingFlags.NonPublic); | ||
var decode = type.GetMethod("DecodeContents", BindingFlags.Static | BindingFlags.NonPublic); | ||
this.encode = (EncodeFunction)Delegate.CreateDelegate(typeof(EncodeFunction), encode ?? throw new Exception()); | ||
this.decode = (DecodeFunction)Delegate.CreateDelegate(typeof(DecodeFunction), decode ?? throw new Exception()); | ||
} | ||
|
||
[Theory(DisplayName = "Encode Trim Tail Bits")] | ||
[InlineData(new byte[] { 1 }, new int[] { unchecked((int)0xFFFF_FFFF) }, 1)] | ||
[InlineData(new byte[] { 127 }, new int[] { unchecked((int)0xFFFF_FFFF) }, 7)] | ||
[InlineData(new byte[] { 0x78, 0x56, 0x34, 0x12, 0xFF, 1 }, new int[] { 0x12345678, unchecked((int)0xFFFF_FFFF) }, 41)] | ||
[InlineData(new byte[] { 0 }, new int[] { unchecked((int)0xAAAA_AAAAU) }, 1)] | ||
[InlineData(new byte[] { 1 }, new int[] { unchecked((int)0x5555_5555U) }, 1)] | ||
[InlineData(new byte[] { 0b010 }, new int[] { unchecked((int)0xAAAA_AAAAU) }, 3)] | ||
[InlineData(new byte[] { 0b10101 }, new int[] { unchecked((int)0x5555_5555U) }, 5)] | ||
public void EncodeTrimTailBits(byte[] expected, int[] source, int length) | ||
{ | ||
var actual = new byte[expected.Length]; | ||
this.encode.Invoke(actual, source, length); | ||
Assert.Equal(expected, actual); | ||
} | ||
|
||
[Theory(DisplayName = "Decode Trim Tail Bits")] | ||
[InlineData(new int[] { 1 }, new byte[] { 0xFF }, 1)] | ||
[InlineData(new int[] { 0x001F_5566 }, new byte[] { 0x66, 0x55, 0xFF }, 21)] | ||
[InlineData(new int[] { 0x3344_5566, 0x0000_3FCC }, new byte[] { 0x66, 0x55, 0x44, 0x33, 0xCC, 0xFF }, 46)] | ||
[InlineData(new int[] { 0 }, new byte[] { 0xAA }, 1)] | ||
[InlineData(new int[] { 1 }, new byte[] { 0x55 }, 1)] | ||
[InlineData(new int[] { 0b01010 }, new byte[] { 0xAA }, 5)] | ||
[InlineData(new int[] { 0b1010101 }, new byte[] { 0x55 }, 7)] | ||
public void DecodeTrimTailBits(int[] expected, byte[] source, int length) | ||
{ | ||
var actual = new int[expected.Length]; | ||
this.decode.Invoke(actual, source, length); | ||
Assert.Equal(expected, actual); | ||
} | ||
|
||
[Theory(DisplayName = "Encode Bounds Checking")] | ||
[InlineData(0, 1, 1)] | ||
[InlineData(1, 0, 1)] | ||
[InlineData(7, 2, 63)] | ||
[InlineData(17, 4, 129)] | ||
public void EncodeBoundsChecking(int target, int source, int length) | ||
{ | ||
var error = Assert.Throws<IndexOutOfRangeException>(() => | ||
{ | ||
var a = new byte[target]; | ||
var b = new int[source]; | ||
this.encode.Invoke(a, b, length); | ||
}); | ||
Assert.Equal(new IndexOutOfRangeException().Message, error.Message); | ||
} | ||
|
||
[Theory(DisplayName = "Decode Bounds Checking")] | ||
[InlineData(0, 1, 1)] | ||
[InlineData(1, 0, 1)] | ||
[InlineData(2, 7, 63)] | ||
[InlineData(4, 17, 129)] | ||
public void DecodeBoundsChecking(int target, int source, int length) | ||
{ | ||
var error = Assert.Throws<IndexOutOfRangeException>(() => | ||
{ | ||
var a = new int[target]; | ||
var b = new byte[source]; | ||
this.decode.Invoke(a, b, length); | ||
}); | ||
Assert.Equal(new IndexOutOfRangeException().Message, error.Message); | ||
} | ||
|
||
[Theory(DisplayName = "Encode Slices Error")] | ||
[InlineData(0, 1, 33)] | ||
[InlineData(4, 2, 65)] | ||
public void EncodeSlicesError(int target, int source, int length) | ||
{ | ||
var error = Assert.Throws<ArgumentOutOfRangeException>(() => | ||
{ | ||
var a = new byte[target]; | ||
var b = new int[source]; | ||
this.encode.Invoke(a, b, length); | ||
}); | ||
Assert.Equal("length", error.ParamName); | ||
} | ||
|
||
[Theory(DisplayName = "Decode Slices Error")] | ||
[InlineData(1, 0, 33)] | ||
[InlineData(2, 4, 65)] | ||
public void DecodeSlicesError(int target, int source, int length) | ||
{ | ||
var error = Assert.Throws<ArgumentOutOfRangeException>(() => | ||
{ | ||
var a = new int[target]; | ||
var b = new byte[source]; | ||
this.decode.Invoke(a, b, length); | ||
}); | ||
Assert.Equal("length", error.ParamName); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
namespace Mikodev.Binary.Tests.Converters; | ||
|
||
using Mikodev.Binary.Tests.Contexts; | ||
using System; | ||
using System.Collections; | ||
using System.Linq; | ||
using Xunit; | ||
|
||
public class BitArrayConverterTests | ||
{ | ||
[Fact(DisplayName = "Converter Type Name And Length")] | ||
public void GetConverter() | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
Assert.Equal("BitArrayConverter", converter.GetType().Name); | ||
Assert.Equal(0, converter.Length); | ||
} | ||
|
||
[Fact(DisplayName = "Encode Decode Random Data")] | ||
public void BasicTest() | ||
{ | ||
var random = new Random(); | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
|
||
for (var ignore = 0; ignore < 4; ignore++) | ||
{ | ||
var buffer = new byte[32]; | ||
for (var k = 0; k < buffer.Length * 8; k++) | ||
{ | ||
random.NextBytes(buffer); | ||
var source = new BitArray(buffer) { Length = k }; | ||
var encode = converter.Encode(source); | ||
var target = new ReadOnlySpan<byte>(encode); | ||
var padding = Converter.Decode(ref target); | ||
Assert.True(padding is >= 0 and <= 7); | ||
Assert.Equal((-k) & 7, padding); | ||
var actual = new byte[(k + 7) >> 3]; | ||
source.CopyTo(actual, 0); | ||
Assert.Equal(actual, target); | ||
|
||
var result = converter.Decode(encode); | ||
Assert.Equal(k, result.Count); | ||
Assert.Equal(source.Cast<bool>(), result.Cast<bool>()); | ||
|
||
ConverterTests.TestVariableEncodeDecodeMethods(converter, source); | ||
} | ||
} | ||
} | ||
|
||
[Fact(DisplayName = "Null Instance")] | ||
public void NullInstance() | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray?>(); | ||
var encode = converter.Encode(null); | ||
Assert.True(ReferenceEquals(Array.Empty<byte>(), encode)); | ||
var result = converter.Decode(Array.Empty<byte>()); | ||
Assert.Null(result); | ||
} | ||
|
||
[Fact(DisplayName = "Empty Collection")] | ||
public void Empty() | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
var source = new BitArray(0); | ||
var encode = converter.Encode(source); | ||
Assert.Equal([(byte)0], encode.AsSpan()); | ||
var result = converter.Decode(encode); | ||
Assert.Empty(result.Cast<bool>()); | ||
} | ||
|
||
[Fact(DisplayName = "Empty Collection Compatible Byte Sequence")] | ||
public void EmptyCompatibles() | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
var a = new byte[] { 0 }; | ||
var b = new byte[] { 0x80, 0, 0, 0 }; | ||
var x = converter.Decode(a); | ||
var y = converter.Decode(b); | ||
Assert.Empty(x.Cast<bool>()); | ||
Assert.Empty(y.Cast<bool>()); | ||
} | ||
|
||
[Fact(DisplayName = "Large Array Encode")] | ||
public void LargeArrayEncode() | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
var source = new BitArray(int.MaxValue); | ||
var encode = converter.Encode(source); | ||
var target = new ReadOnlySpan<byte>(encode); | ||
var margin = Converter.Decode(ref target); | ||
Assert.Equal(1, margin); | ||
Assert.Equal(0x1000_0000, target.Length); | ||
} | ||
|
||
[Fact(DisplayName = "Large Array Decode")] | ||
public void LargeArrayDecode() | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
var buffer = new byte[0x1000_0001]; | ||
buffer[0] = 1; | ||
var result = converter.Decode(buffer); | ||
Assert.Equal(int.MaxValue, result.Length); | ||
} | ||
|
||
[Fact(DisplayName = "Large Array Overflow")] | ||
public void LargeArrayOverflow() | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
var buffer = new byte[0x1000_0001]; | ||
var error = Assert.Throws<OverflowException>(() => converter.Decode(buffer)); | ||
Assert.Equal(new OverflowException().Message, error.Message); | ||
} | ||
|
||
[Theory(DisplayName = "Invalid Margin Info")] | ||
[InlineData(new byte[] { 8, 0 })] | ||
[InlineData(new byte[] { 127, 1, 2 })] | ||
[InlineData(new byte[] { 0x80, 0, 0, 8, 4 })] | ||
[InlineData(new byte[] { 0x80, 2, 0, 0, 2, 8 })] | ||
public void InvalidMarginInfo(byte[] buffer) | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
var error = Assert.Throws<ArgumentException>(() => converter.Decode(buffer)); | ||
var target = new ReadOnlySpan<byte>(buffer); | ||
var margin = Converter.Decode(ref target); | ||
Assert.True((uint)margin >= 8U); | ||
Assert.True(target.Length is not 0); | ||
Assert.Null(error.ParamName); | ||
Assert.Equal("Not enough bytes or byte sequence invalid.", error.Message); | ||
} | ||
|
||
[Theory(DisplayName = "Not Enough Bytes")] | ||
[InlineData(new byte[] { 1 })] | ||
[InlineData(new byte[] { 7 })] | ||
[InlineData(new byte[] { 0x80, 0, 0, 2 })] | ||
[InlineData(new byte[] { 0x80, 0, 0, 5 })] | ||
public void NotEnoughBytes(byte[] buffer) | ||
{ | ||
var generator = Generator.CreateDefault(); | ||
var converter = generator.GetConverter<BitArray>(); | ||
var error = Assert.Throws<ArgumentException>(() => converter.Decode(buffer)); | ||
var target = new ReadOnlySpan<byte>(buffer); | ||
var margin = Converter.Decode(ref target); | ||
Assert.True((uint)margin <= 7U); | ||
Assert.True(target.Length is 0); | ||
Assert.Null(error.ParamName); | ||
Assert.Equal("Not enough bytes or byte sequence invalid.", error.Message); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
code/Binary/Creators.Isolated.Variables/BitArrayConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
namespace Mikodev.Binary.Creators.Isolated.Variables; | ||
|
||
using Mikodev.Binary; | ||
using Mikodev.Binary.Features.Contexts; | ||
using Mikodev.Binary.Internal; | ||
using System; | ||
using System.Buffers.Binary; | ||
using System.Collections; | ||
using System.Diagnostics; | ||
using System.Runtime.CompilerServices; | ||
using System.Runtime.InteropServices; | ||
|
||
internal sealed class BitArrayConverter : VariableConverter<BitArray?, BitArrayConverter.Functions> | ||
{ | ||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_array")] | ||
private static extern ref int[]? AccessFunction(BitArray array); | ||
|
||
private static uint FilterFunction(uint buffer, int remain) | ||
{ | ||
Debug.Assert((uint)remain < 32); | ||
var offset = 32 - remain; | ||
buffer <<= offset; | ||
buffer >>= offset; | ||
return buffer; | ||
} | ||
|
||
private static void EncodeContents(Span<byte> target, ReadOnlySpan<int> source, int length) | ||
{ | ||
var bounds = length >> 5; | ||
for (var i = 0; i < bounds; i++, target = target.Slice(4)) | ||
BinaryPrimitives.WriteInt32LittleEndian(target, source[i]); | ||
var remain = length & 31; | ||
if (remain is 0) | ||
return; | ||
var buffer = FilterFunction((uint)source[bounds], remain); | ||
var limits = (remain + 7) >> 3; | ||
for (var i = 0; i < limits; i++) | ||
target[i] = (byte)(buffer >> (i * 8)); | ||
} | ||
|
||
private static void DecodeContents(Span<int> target, ReadOnlySpan<byte> source, int length) | ||
{ | ||
var bounds = length >> 5; | ||
for (var i = 0; i < bounds; i++, source = source.Slice(4)) | ||
target[i] = BinaryPrimitives.ReadInt32LittleEndian(source); | ||
var remain = length & 31; | ||
if (remain is 0) | ||
return; | ||
var buffer = (uint)0; | ||
var limits = (remain + 7) >> 3; | ||
for (var i = 0; i < limits; i++) | ||
buffer |= (uint)source[i] << (i * 8); | ||
target[bounds] = (int)FilterFunction(buffer, remain); | ||
} | ||
|
||
private static void EncodeInternal(ref Allocator allocator, BitArray? item) | ||
{ | ||
if (item is null) | ||
return; | ||
var length = item.Count; | ||
var margin = (-length) & 7; | ||
Debug.Assert((uint)margin < 8); | ||
Converter.Encode(ref allocator, margin); | ||
if (length is 0) | ||
return; | ||
var required = (int)(((uint)length + 7U) >> 3); | ||
var buffer = MemoryMarshal.CreateSpan(ref Allocator.Assign(ref allocator, required), required); | ||
var source = AccessFunction(item); | ||
EncodeContents(buffer, source, length); | ||
} | ||
|
||
private static BitArray? DecodeInternal(in ReadOnlySpan<byte> span) | ||
{ | ||
if (span.Length is 0) | ||
return null; | ||
var cursor = span; | ||
var margin = Converter.Decode(ref cursor); | ||
Debug.Assert(margin >= 0); | ||
if (margin is 0 && cursor.Length is 0) | ||
return new BitArray(0); | ||
if (margin >= 8 || cursor.Length is 0) | ||
ThrowHelper.ThrowNotEnoughBytes(); | ||
var length = checked((int)(((ulong)cursor.Length << 3) - (uint)margin)); | ||
var result = new BitArray(length); | ||
var target = AccessFunction(result); | ||
DecodeContents(target, cursor, length); | ||
return result; | ||
} | ||
|
||
internal readonly struct Functions : IVariableConverterFunctions<BitArray?> | ||
{ | ||
public static BitArray? Decode(in ReadOnlySpan<byte> span) | ||
{ | ||
return DecodeInternal(in span); | ||
} | ||
|
||
public static void Encode(ref Allocator allocator, BitArray? item) | ||
{ | ||
EncodeInternal(ref allocator, item); | ||
} | ||
|
||
public static void EncodeWithLengthPrefix(ref Allocator allocator, BitArray? item) | ||
{ | ||
var anchor = Allocator.Anchor(ref allocator, sizeof(int)); | ||
EncodeInternal(ref allocator, item); | ||
Allocator.FinishAnchor(ref allocator, anchor); | ||
} | ||
} | ||
} |
Oops, something went wrong.