diff --git a/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs new file mode 100644 index 00000000..adef2af8 --- /dev/null +++ b/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs @@ -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; } + } +} diff --git a/BinarySerializer.Test/PackedBoolean/EndianAwarePackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/EndianAwarePackedBooleanClass.cs new file mode 100644 index 00000000..33f310c5 --- /dev/null +++ b/BinarySerializer.Test/PackedBoolean/EndianAwarePackedBooleanClass.cs @@ -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; } + } +} diff --git a/BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs b/BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs new file mode 100644 index 00000000..a1d3733d --- /dev/null +++ b/BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs @@ -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(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(IEnumerable expected, IEnumerable 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 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; + } + } + + } +} diff --git a/BinarySerializer.Test/PackedBoolean/UnpackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/UnpackedBooleanClass.cs new file mode 100644 index 00000000..22564543 --- /dev/null +++ b/BinarySerializer.Test/PackedBoolean/UnpackedBooleanClass.cs @@ -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; } + } +} diff --git a/BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs new file mode 100644 index 00000000..db35fb52 --- /dev/null +++ b/BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs @@ -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; } + } +} diff --git a/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs b/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs index 61b49a5c..d39e8146 100644 --- a/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs +++ b/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs @@ -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 @@ -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); } @@ -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()); + private static Type GetNodeType(Type type, IEnumerable attributes) { var nullableType = Nullable.GetUnderlyingType(type); @@ -100,6 +103,14 @@ private static Type GetNodeType(Type type) return typeof(ValueTypeNode); } + if (attributes.OfType().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); diff --git a/BinarySerializer/Graph/TypeGraph/PackedBooleanArrayTypeNode.cs b/BinarySerializer/Graph/TypeGraph/PackedBooleanArrayTypeNode.cs new file mode 100644 index 00000000..56c1ef03 --- /dev/null +++ b/BinarySerializer/Graph/TypeGraph/PackedBooleanArrayTypeNode.cs @@ -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); + + } +} diff --git a/BinarySerializer/Graph/ValueGraph/PackedBooleanArrayValueNode.cs b/BinarySerializer/Graph/ValueGraph/PackedBooleanArrayValueNode.cs new file mode 100644 index 00000000..d2d2febe --- /dev/null +++ b/BinarySerializer/Graph/ValueGraph/PackedBooleanArrayValueNode.cs @@ -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); + } +} diff --git a/BinarySerializer/PackAttribute.cs b/BinarySerializer/PackAttribute.cs new file mode 100644 index 00000000..918a971b --- /dev/null +++ b/BinarySerializer/PackAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BinarySerialization +{ + /// Tells the to pack the decorated member. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public sealed class PackAttribute : Attribute + { + /// Initializes a new instance of the class. + public PackAttribute() { } + } +}