From 09aea56a301df74c23229a5f56126628965109f3 Mon Sep 17 00:00:00 2001 From: Davipb Date: Fri, 24 Feb 2017 22:47:29 -0300 Subject: [PATCH 1/4] Add packed boolean array nodes --- .../Graph/TypeGraph/ContainerTypeNode.cs | 4 +- .../TypeGraph/PackedBooleanArrayTypeNode.cs | 17 +++ .../ValueGraph/PackedBooleanArrayValueNode.cs | 105 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 BinarySerializer/Graph/TypeGraph/PackedBooleanArrayTypeNode.cs create mode 100644 BinarySerializer/Graph/ValueGraph/PackedBooleanArrayValueNode.cs diff --git a/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs b/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs index 61b49a5c..fc78e0bd 100644 --- a/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs +++ b/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.IO; using System.Reflection; @@ -100,6 +100,8 @@ private static Type GetNodeType(Type type) return typeof(ValueTypeNode); } + if (type == typeof(bool[])) + return typeof(PackedBooleanArrayTypeNode); 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); + } +} From 7c3ab752f99afa7cf07d59eafce601a76e7c264e Mon Sep 17 00:00:00 2001 From: Davipb Date: Sat, 25 Feb 2017 00:29:01 -0300 Subject: [PATCH 2/4] Add packed boolean array tests --- .../ConstantSizePackedBooleanClass.cs | 20 +++ .../EndianAwarePackedBooleanClass.cs | 15 ++ .../PackedBoolean/PackedBooleanTests.cs | 136 ++++++++++++++++++ .../PackedBoolean/ValidPackedBooleanClass.cs | 16 +++ 4 files changed, 187 insertions(+) create mode 100644 BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs create mode 100644 BinarySerializer.Test/PackedBoolean/EndianAwarePackedBooleanClass.cs create mode 100644 BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs create mode 100644 BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs diff --git a/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs new file mode 100644 index 00000000..cc053cc9 --- /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)] + public bool[] ConstantCountArray { get; set; } + + [FieldLength(2)] + [FieldOrder(1)] + 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..4dc08e68 --- /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)] + public bool[] LittleEndianArray { get; set; } + + [FieldEndianness(BinarySerialization.Endianness.Big)] + [FieldOrder(1)] + 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..fead08ef --- /dev/null +++ b/BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs @@ -0,0 +1,136 @@ +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); + } + + 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/ValidPackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs new file mode 100644 index 00000000..ca6b1715 --- /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)] + public bool[] BooleanArray { get; set; } + } +} From 5420caef7e6b060ff9c25d91f5a080852b12ca85 Mon Sep 17 00:00:00 2001 From: Davipb Date: Sat, 25 Feb 2017 13:06:49 -0300 Subject: [PATCH 3/4] Make boolean packing optional through PackAttribute --- .../ConstantSizePackedBooleanClass.cs | 6 +++--- .../EndianAwarePackedBooleanClass.cs | 4 ++-- .../PackedBoolean/ValidPackedBooleanClass.cs | 2 +- .../Graph/TypeGraph/ContainerTypeNode.cs | 17 +++++++++++++---- BinarySerializer/PackAttribute.cs | 14 ++++++++++++++ 5 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 BinarySerializer/PackAttribute.cs diff --git a/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs index cc053cc9..adef2af8 100644 --- a/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs +++ b/BinarySerializer.Test/PackedBoolean/ConstantSizePackedBooleanClass.cs @@ -10,11 +10,11 @@ public class ConstantSizePackedBooleanClass [Ignore] public const int LengthConstraint = 2; [FieldCount(CountConstraint)] - [FieldOrder(0)] + [FieldOrder(0), Pack] public bool[] ConstantCountArray { get; set; } - [FieldLength(2)] - [FieldOrder(1)] + [FieldLength(LengthConstraint)] + [FieldOrder(1), Pack] public bool[] ConstantLengthArray { get; set; } } } diff --git a/BinarySerializer.Test/PackedBoolean/EndianAwarePackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/EndianAwarePackedBooleanClass.cs index 4dc08e68..33f310c5 100644 --- a/BinarySerializer.Test/PackedBoolean/EndianAwarePackedBooleanClass.cs +++ b/BinarySerializer.Test/PackedBoolean/EndianAwarePackedBooleanClass.cs @@ -5,11 +5,11 @@ namespace BinarySerialization.Test.PackedBoolean public class EndianAwarePackedBooleanClass { [FieldEndianness(BinarySerialization.Endianness.Little)] - [FieldOrder(0)] + [FieldOrder(0), Pack] public bool[] LittleEndianArray { get; set; } [FieldEndianness(BinarySerialization.Endianness.Big)] - [FieldOrder(1)] + [FieldOrder(1), Pack] public bool[] BigEndianArray { get; set; } } } diff --git a/BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs b/BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs index ca6b1715..db35fb52 100644 --- a/BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs +++ b/BinarySerializer.Test/PackedBoolean/ValidPackedBooleanClass.cs @@ -10,7 +10,7 @@ public class ValidPackedBooleanClass [FieldOrder(1)] public long BooleanArrayLength { get; set; } [FieldCount(nameof(BooleanArrayCount)), FieldLength(nameof(BooleanArrayLength))] - [FieldOrder(2)] + [FieldOrder(2), Pack] public bool[] BooleanArray { get; set; } } } diff --git a/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs b/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs index fc78e0bd..d39e8146 100644 --- a/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs +++ b/BinarySerializer/Graph/TypeGraph/ContainerTypeNode.cs @@ -1,6 +1,8 @@ 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,8 +103,14 @@ private static Type GetNodeType(Type type) return typeof(ValueTypeNode); } - if (type == typeof(bool[])) - return typeof(PackedBooleanArrayTypeNode); + 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/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() { } + } +} From b6ab38476245a5e829607cd29de3db30d52b6f5d Mon Sep 17 00:00:00 2001 From: Davipb Date: Sat, 25 Feb 2017 13:32:10 -0300 Subject: [PATCH 4/4] Add test for unpacked boolean arrays --- .../PackedBoolean/PackedBooleanTests.cs | 21 +++++++++++++++++++ .../PackedBoolean/UnpackedBooleanClass.cs | 16 ++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 BinarySerializer.Test/PackedBoolean/UnpackedBooleanClass.cs diff --git a/BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs b/BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs index fead08ef..a1d3733d 100644 --- a/BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs +++ b/BinarySerializer.Test/PackedBoolean/PackedBooleanTests.cs @@ -108,6 +108,27 @@ public void AddsNewItemsOnFixedSize() 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"); 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; } + } +}