Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Packed Boolean Arrays #56

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace BinarySerialization.Test.PackedBoolean
{
public class ConstantSizePackedBooleanClass
{
[Ignore] public const int CountConstraint = 20;
[Ignore] public const int LengthConstraint = 2;

[FieldCount(CountConstraint)]
[FieldOrder(0), Pack]
public bool[] ConstantCountArray { get; set; }

[FieldLength(LengthConstraint)]
[FieldOrder(1), Pack]
public bool[] ConstantLengthArray { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using BinarySerialization;

namespace BinarySerialization.Test.PackedBoolean
{
public class EndianAwarePackedBooleanClass
{
[FieldEndianness(BinarySerialization.Endianness.Little)]
[FieldOrder(0), Pack]
public bool[] LittleEndianArray { get; set; }

[FieldEndianness(BinarySerialization.Endianness.Big)]
[FieldOrder(1), Pack]
public bool[] BigEndianArray { get; set; }
}
}
157 changes: 157 additions & 0 deletions BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BinarySerialization.Test.PackedBoolean
{
[TestClass, TestCategory("Packed Booleans")]
public class PackedBooleanTests : TestBase
{
[TestMethod]
public void PreservesData()
{
var original = new ValidPackedBooleanClass
{
BooleanArray = GenerateBools().Take(50).ToArray()
};

var deserialized = Roundtrip(original);

CheckSequence(original.BooleanArray, deserialized.BooleanArray);
}

[TestMethod]
public void BindsCorrectData()
{
var test = new ValidPackedBooleanClass
{
BooleanArray = GenerateBools().Take(50).ToArray()
};

test = Roundtrip(test);

Assert.AreEqual(50, test.BooleanArrayCount, "Incorrect count binding.");
Assert.AreEqual(7, test.BooleanArrayLength, "Incorrect length binding.");
}

[TestMethod]
public void ProperlyPacksBooleans()
{
var original = new ValidPackedBooleanClass
{
BooleanArray = Enumerable.Repeat(true, 10).ToArray()
};

// Count = 10L
// Length = 2L
// Packed Booleans = 1111 1111 0000 0011 (Little Endian)
byte[] expected = new byte[] { 10, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0x03 };
byte[] result = Serialize(original);

CheckSequence(expected, result);
}

[TestMethod]
public void RespectsEndianness()
{
var original = new EndianAwarePackedBooleanClass
{
LittleEndianArray = new[] { true, true, true, true },
BigEndianArray = new[] { true, true, true, true }
};

// Little: 00001111
// Big: 11110000

byte[] result = Serialize(original);

Assert.AreEqual(0x0F, result[0], "Incorrect Little-Endian boolean packing");
Assert.AreEqual(0xF0, result[1], "Incorrect Big-Endian boolean packing");

}

[TestMethod]
public void DiscardsExtraItemsOnFixedSize()
{
var original = new ConstantSizePackedBooleanClass
{
ConstantCountArray = GenerateBools().Take(40).ToArray(),
ConstantLengthArray = GenerateBools().Take(30).ToArray()
};

var result = Roundtrip(original);

CheckSequence(GenerateBools().Take(ConstantSizePackedBooleanClass.CountConstraint), result.ConstantCountArray);
CheckSequence(GenerateBools().Take(ConstantSizePackedBooleanClass.LengthConstraint * 8), result.ConstantLengthArray);
}

[TestMethod]
public void AddsNewItemsOnFixedSize()
{
var original = new ConstantSizePackedBooleanClass
{
ConstantCountArray = new[] { true },
ConstantLengthArray = new[] { true },
};

var result = Roundtrip(original);

bool[] expectedCount = new bool[ConstantSizePackedBooleanClass.CountConstraint];
expectedCount[0] = true;

bool[] expectedLength = new bool[ConstantSizePackedBooleanClass.LengthConstraint * 8];
expectedLength[0] = true;

CheckSequence(expectedCount, result.ConstantCountArray);
CheckSequence(expectedLength, result.ConstantLengthArray);
}

[TestMethod]
public void DoesntAffectUnpackedBooleanArrays()
{
var original = new UnpackedBooleanClass
{
UnpackedArray = new[] { true, true, false, false, true, true }
};

var expected = new byte[] { 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1 };
var actual = Serialize(original);

CheckSequence(expected, actual);

var deserialized = Deserialize<UnpackedBooleanClass>(actual);

Assert.AreEqual(original.UnpackedArray.Length, deserialized.UnpackedArrayLength, "Invalid length binding on unpacked boolean array.");
Assert.AreEqual(original.UnpackedArray.Length, deserialized.UnpackedArrayCount, "Invalid count binding on unpacked boolean array.");

CheckSequence(original.UnpackedArray, deserialized.UnpackedArray);
}

private void CheckSequence<T>(IEnumerable<T> expected, IEnumerable<T> actual)
{
Assert.AreEqual(expected.Count(), actual.Count(), "Incorrect length");

var zipped = expected.Zip(actual, Tuple.Create);
int i = 0;
foreach (var item in zipped)
{
Assert.AreEqual(item.Item1, item.Item2, "Mismatch at value {0}", i);
i++;
}
}

private IEnumerable<bool> GenerateBools()
{
bool[] toRepeat = new bool[] { true, false, false, true, true, false, true, false, true, true, false, true };

while (true)
{
foreach (var b in toRepeat)
yield return b;
}
}

}
}
16 changes: 16 additions & 0 deletions BinarySerializer.Test/PackedBoolean/UnpackedBooleanClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace BinarySerialization.Test.PackedBoolean
{
public class UnpackedBooleanClass
{
[FieldOrder(0)] public long UnpackedArrayCount { get; set; }
[FieldOrder(1)] public long UnpackedArrayLength { get; set; }

[FieldCount(nameof(UnpackedArrayCount)), FieldLength(nameof(UnpackedArrayLength))]
[FieldOrder(2)]
public bool[] UnpackedArray { get; set; }
}
}
16 changes: 16 additions & 0 deletions BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace BinarySerialization.Test.PackedBoolean
{
public class ValidPackedBooleanClass
{
[FieldOrder(0)] public long BooleanArrayCount { get; set; }
[FieldOrder(1)] public long BooleanArrayLength { get; set; }

[FieldCount(nameof(BooleanArrayCount)), FieldLength(nameof(BooleanArrayLength))]
[FieldOrder(2), Pack]
public bool[] BooleanArray { get; set; }
}
}
17 changes: 14 additions & 3 deletions BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace BinarySerialization.Graph.TypeGraph
Expand Down Expand Up @@ -45,7 +47,7 @@ protected TypeNode GenerateChild(Type parentType, MemberInfo memberInfo)
{
ThrowOnBadType(memberType);

var nodeType = GetNodeType(memberType);
var nodeType = GetNodeType(memberType, memberInfo.GetCustomAttributes());

return (TypeNode) Activator.CreateInstance(nodeType, this, parentType, memberInfo);
}
Expand Down Expand Up @@ -84,7 +86,8 @@ private static void ThrowOnBadType(Type type)
}
// ReSharper restore UnusedParameter.Local

private static Type GetNodeType(Type type)
private static Type GetNodeType(Type type) => GetNodeType(type, Enumerable.Empty<Attribute>());
private static Type GetNodeType(Type type, IEnumerable<Attribute> attributes)
{
var nullableType = Nullable.GetUnderlyingType(type);

Expand All @@ -100,6 +103,14 @@ private static Type GetNodeType(Type type)
return typeof(ValueTypeNode);
}

if (attributes.OfType<PackAttribute>().Any())
{
if (type == typeof(bool[]))
return typeof(PackedBooleanArrayTypeNode);

throw new InvalidOperationException($"Cannot use the Pack attribute on member of type {type.Name}.");
}

if (type.IsArray)
{
return typeof(ArrayTypeNode);
Expand Down
17 changes: 17 additions & 0 deletions BinarySerializer/Graph/TypeGraph/PackedBooleanArrayTypeNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using BinarySerialization.Graph.ValueGraph;
using System;
using System.Reflection;

namespace BinarySerialization.Graph.TypeGraph
{
internal class PackedBooleanArrayTypeNode : TypeNode
{
public PackedBooleanArrayTypeNode(TypeNode parent) : base(parent) { }
public PackedBooleanArrayTypeNode(TypeNode parent, Type type) : base(parent, type) { }
public PackedBooleanArrayTypeNode(TypeNode parent, Type type, MemberInfo memberInfo) : base(parent, type, memberInfo) { }

public override ValueNode CreateSerializerOverride(ValueNode parent)
=> new PackedBooleanArrayValueNode(parent, Name, this);

}
}
105 changes: 105 additions & 0 deletions BinarySerializer/Graph/ValueGraph/PackedBooleanArrayValueNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using BinarySerialization.Graph.TypeGraph;
using System;
using System.Collections.Generic;

namespace BinarySerialization.Graph.ValueGraph
{
internal class PackedBooleanArrayValueNode : ValueNode
{
public override object Value
{
get { return Values; }
set
{
if (!(value is bool[]))
throw new InvalidOperationException("Only Boolean Arrays are valid as values for a Packed Boolean Array node.");

Values = (bool[])value;
}
}

private bool[] Values;

public PackedBooleanArrayValueNode(Node parent, string name, TypeNode typeNode) : base(parent, name, typeNode) { }

internal override void DeserializeOverride(BoundedStream stream, EventShuttle eventShuttle)
{
var size = GetFieldCount() ?? (GetFieldLength() * 8) ?? (stream.AvailableForReading * 8);

Values = new bool[size];

if (size == 0)
return;

int index = -1;
int bit = -1;
byte currentByte = 0;

while (++index < Values.Length)
{
if (bit < 0 || bit > 7)
{
int read;
if (stream.IsAtLimit || (read = stream.ReadByte()) < 0)
throw new InvalidOperationException("Stream ended before all booleans were deserialized.");

currentByte = (byte)read;
// Big Endian = Read from MSB to LSB (X000 0000, 0X00 0000, ..., 0000 00X0, 0000 000X)
// Little Endian = Read from LSB to MSB (0000 000X, 0000 00X0, ..., 0X00 000, X000 0000)
bit = GetFieldEndianness() == Endianness.Big ? 7 : 0;
}

int mask = (byte)(1 << bit);
Values[index] = (currentByte & mask) == mask;

if (GetFieldEndianness() == Endianness.Big)
bit--;
else
bit++;
}

}

internal override void SerializeOverride(BoundedStream stream, EventShuttle eventShuttle)
{
if (Values == null || Values.Length == 0)
return;

if (GetConstFieldCount() != null)
Array.Resize(ref Values, (int)GetConstFieldCount().Value);
if (GetConstFieldLength() != null)
Array.Resize(ref Values, (int)GetConstFieldLength().Value * 8);

int index = -1;

// Big Endian = Write from MSB to LSB (X000 0000, 0X00 0000, ..., 0000 00X0, 0000 000X)
// Little Endian = Write from LSB to MSB (0000 000X, 0000 00X0, ..., 0X00 000, X000 0000)
int bit = GetFieldEndianness() == Endianness.Big ? 7 : 0;
byte currentByte = 0;

while (++index < Values.Length)
{
if (Values[index])
currentByte |= (byte)(1 << bit);

if (GetFieldEndianness() == Endianness.Big)
bit--;
else
bit++;

if (bit < 0 || bit > 7 || index == Values.Length - 1)
{
if (stream.IsAtLimit)
break;

stream.WriteByte(currentByte);
bit = GetFieldEndianness() == Endianness.Big ? 7 : 0;
currentByte = 0;
}
}
}

protected override long CountOverride() => Values?.Length ?? 0;
protected override long MeasureOverride() => (long)Math.Ceiling((Values?.Length ?? 0) / 8.0);
}
}
Loading