Skip to content

Commit

Permalink
Add bit array converter use unsafe accessor
Browse files Browse the repository at this point in the history
  • Loading branch information
afxres committed Jan 5, 2024
1 parent 074b9c3 commit 1744e2f
Show file tree
Hide file tree
Showing 5 changed files with 384 additions and 1 deletion.
116 changes: 116 additions & 0 deletions code/Binary.Tests/Converters/BitArrayConverterInternalTests.cs
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);
}
}
157 changes: 157 additions & 0 deletions code/Binary.Tests/Converters/BitArrayConverterTests.cs
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);
}
}
2 changes: 1 addition & 1 deletion code/Binary.Tests/Implementations/CodeContractsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ static bool Equals(string name, params string[] patterns)
var converterParameters = parameters.Where(x => x.Member is MethodInfo && typeof(IConverter).IsAssignableFrom(x.Member.DeclaringType)).ToList();
var converterExpectedParameters = converterParameters.Where(x => !x.Member.Name.StartsWith("Throw") && !x.Member.Name.StartsWith("Except") && Equals(x.ParameterType.Name, names)).ToList();
var ignoredParameters = converterExpectedParameters.Where(x => !x.ParameterType.IsByRef).ToList();
var knownIssues = new[] { "IPAddressConverter", "IPEndPointConverter", "VersionConverter", "BigIntegerConverter", "VariableBoundArrayConverter`2" };
var knownIssues = new[] { "IPAddressConverter", "IPEndPointConverter", "VersionConverter", "BigIntegerConverter", "BitArrayConverter", "VariableBoundArrayConverter`2" };
var parametersWithIssue = ignoredParameters.Where(x => knownIssues.Contains(x.Member.ReflectedType?.Name)).ToList();
var parametersWithoutIssue = ignoredParameters.Except(parametersWithIssue).ToList();
Assert.Empty(parametersWithoutIssue);
Expand Down
109 changes: 109 additions & 0 deletions code/Binary/Creators.Isolated.Variables/BitArrayConverter.cs
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);
}
}
}
Loading

0 comments on commit 1744e2f

Please sign in to comment.