From 2df6167b2d1f84e4324e5f291778fe8f0b735848 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Fri, 26 Jan 2024 10:28:37 +0100 Subject: [PATCH 01/10] :shirt: Extract the binary serialization into class Throw exceptions when the attributes are missing. Add public API to write 24-bits integers. --- src/Yarhl.UnitTests/IO/DataWriterTests.cs | 596 ++---------------- .../IO/Serialization/BinarySerializerTests.cs | 455 +++++++++++++ src/Yarhl/IO/DataWriter.cs | 180 ++---- .../IO/Serialization/BinarySerializer.cs | 161 +++++ 4 files changed, 729 insertions(+), 663 deletions(-) create mode 100644 src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs create mode 100644 src/Yarhl/IO/Serialization/BinarySerializer.cs diff --git a/src/Yarhl.UnitTests/IO/DataWriterTests.cs b/src/Yarhl.UnitTests/IO/DataWriterTests.cs index 55e5f2b0..6f25c0b5 100644 --- a/src/Yarhl.UnitTests/IO/DataWriterTests.cs +++ b/src/Yarhl.UnitTests/IO/DataWriterTests.cs @@ -25,18 +25,10 @@ namespace Yarhl.UnitTests.IO using System.Text; using NUnit.Framework; using Yarhl.IO; - using Yarhl.IO.Serialization.Attributes; [TestFixture] public class DataWriterTests { - enum Enum1 - { - Value1, - Value2, - Value3, - } - [Test] public void ConstructorSetProperties() { @@ -225,6 +217,56 @@ public void WriteUIntBig() Assert.AreEqual(0xBE, stream.ReadByte()); } + [Test] + public void WriteInt24Big() + { + int value = 0x7F_FC0FFE; + byte[] expected = { + 0xFC, 0x0F, 0xFE, + }; + + using var stream = new DataStream(); + var writer = new DataWriter(stream); + writer.Endianness = EndiannessMode.BigEndian; + + writer.WriteInt24(value); + + byte[] actual = new byte[expected.Length]; + stream.Position = 0; + int read = stream.Read(actual); + + Assert.Multiple(() => { + Assert.AreEqual(expected.Length, stream.Length); + Assert.That(read, Is.EqualTo(expected.Length)); + Assert.That(expected, Is.EquivalentTo(actual)); + }); + } + + [Test] + public void WriteInt24Little() + { + int value = 0x7F_FC0FFE; + byte[] expected = { + 0xFE, 0x0F, 0xFC, + }; + + using var stream = new DataStream(); + var writer = new DataWriter(stream); + writer.Endianness = EndiannessMode.LittleEndian; + + writer.WriteInt24(value); + + byte[] actual = new byte[expected.Length]; + stream.Position = 0; + int read = stream.Read(actual); + + Assert.Multiple(() => { + Assert.AreEqual(expected.Length, stream.Length); + Assert.That(read, Is.EqualTo(expected.Length)); + Assert.That(expected, Is.EquivalentTo(actual)); + }); + } + [Test] public void WriteIntLittle() { @@ -1270,543 +1312,5 @@ public void WritePaddingLessEqualOneDoesNothing() stream.Read(actual, 0, expected.Length); Assert.IsTrue(expected.SequenceEqual(actual)); } - - [Test] - public void WriteUsingReflection() - { - var obj = new ComplexObject { - IntegerValue = 1, - LongValue = 2, - IgnoredIntegerValue = 3, - AnotherIntegerValue = 4, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteNestedObjectUsingReflection() - { - var obj = new NestedObject() { - IntegerValue = 10, - ComplexValue = new ComplexObject { - IntegerValue = 1, - LongValue = 2, - IgnoredIntegerValue = 3, - AnotherIntegerValue = 4, - }, - AnotherIntegerValue = 20, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x0A, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, - 0x14, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteBooleanUsingReflection() - { - var obj = new ObjectWithDefaultBooleanAttribute() { - IntegerValue = 1, - BooleanValue = false, - IgnoredIntegerValue = 3, - AnotherIntegerValue = 4, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteCustomBooleanUsingReflection() - { - var obj = new ObjectWithCustomBooleanAttribute() { - IntegerValue = 1, - BooleanValue = false, - IgnoredIntegerValue = 5, - AnotherIntegerValue = 4, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x66, 0x61, 0x6C, 0x73, 0x65, 0x00, // "false" - 0x04, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteBooleanWithoutAttributeThrowsException() - { - var obj = new ObjectWithoutBooleanAttribute() { - IntegerValue = 1, - BooleanValue = true, - IgnoredIntegerValue = 3, - AnotherIntegerValue = 4, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - Assert.Throws( - () => writer.WriteOfType(obj)); - } - - [Test] - public void WriteStringWithoutAttributeUsesDefaultWriterSettings() - { - var obj = new ObjectWithoutStringAttribute { - IntegerValue = 1, - StringValue = "あア", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 3, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0xE3, 0x81, 0x82, 0xE3, 0x82, 0xA2, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteStringWithDefaultAttributeUsesDefaultWriterSettings() - { - var obj = new ObjectWithDefaultStringAttribute() { - IntegerValue = 1, - StringValue = "あア", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 3, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0xE3, 0x81, 0x82, 0xE3, 0x82, 0xA2, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteCustomStringWithSizeTypeUsingReflection() - { - var obj = new ObjectWithCustomStringAttributeSizeUshort() { - IntegerValue = 1, - StringValue = "あ", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 4, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x03, 0x00, 0xE3, 0x81, 0x82, - 0x04, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteCustomFixedStringUsingReflection() - { - var obj = new ObjectWithCustomStringAttributeFixedSize() { - IntegerValue = 1, - StringValue = "あ", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 4, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0xE3, 0x81, 0x82, - 0x04, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteCustomStringUsingReflectionWithDifferentEncoding() - { - var obj = new ObjectWithCustomStringAttributeCustomEncoding() { - IntegerValue = 1, - StringValue = "あア", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 4, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x82, 0xA0, 0x83, 0x41, 0x00, - 0x04, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteCustomStringUsingReflectionWithUnknownEncodingThrowsException() - { - var obj = new ObjectWithCustomStringAttributeUnknownEncoding() { - IntegerValue = 1, - StringValue = "あア", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 4, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - Assert.Throws( - () => writer.WriteOfType(obj)); - } - - [Test] - public void WriteObjectWithForcedEndianness() - { - ObjectWithForcedEndianness obj = new ObjectWithForcedEndianness() { - LittleEndianInteger = 1, - BigEndianInteger = 2, - DefaultEndianInteger = 3, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x02, - 0x03, 0x00, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteObjectWithEnum() - { - ObjectWithEnum obj = new ObjectWithEnum() { - EnumValue = Enum1.Value2, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Test] - public void WriteObjectWithInt24() - { - ObjectWithInt24 obj = new ObjectWithInt24() { - Int24Value = 1, - }; - - using DataStream stream = new DataStream(); - DataWriter writer = new DataWriter(stream); - - writer.WriteOfType(obj); - - byte[] expected = { - 0x01, 0x00, 0x00, - }; - Assert.AreEqual(expected.Length, stream.Length); - - stream.Position = 0; - byte[] actual = new byte[expected.Length]; - stream.Read(actual, 0, expected.Length); - Assert.IsTrue(expected.SequenceEqual(actual)); - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ComplexObject - { - public int IntegerValue { get; set; } - - public long LongValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class NestedObject - { - public int IntegerValue { get; set; } - - public ComplexObject ComplexValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithDefaultBooleanAttribute - { - public int IntegerValue { get; set; } - - [BinaryBoolean] - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithoutBooleanAttribute - { - public int IntegerValue { get; set; } - - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomBooleanAttribute - { - public int IntegerValue { get; set; } - - [BinaryBoolean(WriteAs = typeof(string), TrueValue = "true", FalseValue = "false")] - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithDefaultStringAttribute - { - public int IntegerValue { get; set; } - - [BinaryString] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithoutStringAttribute - { - public int IntegerValue { get; set; } - - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeSizeUshort - { - public int IntegerValue { get; set; } - - [BinaryString(SizeType = typeof(ushort), Terminator = "")] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeFixedSize - { - public int IntegerValue { get; set; } - - [BinaryString(FixedSize = 3, Terminator = "")] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeCustomEncoding - { - public int IntegerValue { get; set; } - - [BinaryString(CodePage = 932)] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeUnknownEncoding - { - public int IntegerValue { get; set; } - - [BinaryString(CodePage = 666)] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithForcedEndianness - { - [BinaryForceEndianness(EndiannessMode.LittleEndian)] - public int LittleEndianInteger { get; set; } - - [BinaryForceEndianness(EndiannessMode.BigEndian)] - public int BigEndianInteger { get; set; } - - public int DefaultEndianInteger { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithEnum - { - [BinaryEnum(WriteAs = typeof(byte))] - public Enum1 EnumValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithInt24 - { - [BinaryInt24] - public int Int24Value { get; set; } - } } } diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs new file mode 100644 index 00000000..e6df74d0 --- /dev/null +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -0,0 +1,455 @@ +namespace Yarhl.UnitTests.IO.Serialization; + +using System; +using System.Linq; +using NUnit.Framework; +using Yarhl.IO; +using Yarhl.IO.Serialization; +using Yarhl.IO.Serialization.Attributes; + +[TestFixture] +public class BinarySerializerTests +{ + [Test] + public void SerializeMultipleProperties() + { + var obj = new ComplexObject { + IntegerValue = 1, + LongValue = 2, + IgnoredIntegerValue = 3, + AnotherIntegerValue = 4, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeNestedObject() + { + var obj = new NestedObject() { + IntegerValue = 10, + ComplexValue = new ComplexObject { + IntegerValue = 1, + LongValue = 2, + IgnoredIntegerValue = 3, + AnotherIntegerValue = 4, + }, + AnotherIntegerValue = 20, + }; + + byte[] expected = { + 0x0A, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeBooleanType() + { + var obj = new ObjectWithDefaultBooleanAttribute() { + IntegerValue = 1, + BooleanValue = false, + IgnoredIntegerValue = 3, + AnotherIntegerValue = 4, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeBooleanWithCustomFalseValue() + { + var obj = new ObjectWithCustomBooleanAttribute() { + IntegerValue = 1, + BooleanValue = false, + IgnoredIntegerValue = 5, + AnotherIntegerValue = 4, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0x66, 0x61, 0x6C, 0x73, 0x65, 0x00, // "false" + 0x04, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void TrySerializeBooleanWithoutAttributeThrowsException() + { + var obj = new ObjectWithoutBooleanAttribute() { + IntegerValue = 1, + BooleanValue = true, + IgnoredIntegerValue = 3, + AnotherIntegerValue = 4, + }; + + using var stream = new DataStream(); + var serializer = new BinarySerializer(stream); + + _ = Assert.Throws(() => serializer.Serialize(obj)); + } + + [Test] + public void SerializeStringWithoutAttributeUsesDefaultWriterSettings() + { + var obj = new ObjectWithoutStringAttribute { + IntegerValue = 1, + StringValue = "あア", + IgnoredIntegerValue = 2, + AnotherIntegerValue = 3, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0xE3, 0x81, 0x82, 0xE3, 0x82, 0xA2, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeStringWithDefaultAttributeUsesDefaultWriterSettings() + { + var obj = new ObjectWithDefaultStringAttribute() { + IntegerValue = 1, + StringValue = "あア", + IgnoredIntegerValue = 2, + AnotherIntegerValue = 3, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0xE3, 0x81, 0x82, 0xE3, 0x82, 0xA2, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeStringWithSizeType() + { + var obj = new ObjectWithCustomStringAttributeSizeUshort() { + IntegerValue = 1, + StringValue = "あ", + IgnoredIntegerValue = 2, + AnotherIntegerValue = 4, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0x03, 0x00, 0xE3, 0x81, 0x82, + 0x04, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeStringWithFixedSize() + { + var obj = new ObjectWithCustomStringAttributeFixedSize() { + IntegerValue = 1, + StringValue = "あ", + IgnoredIntegerValue = 2, + AnotherIntegerValue = 4, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0xE3, 0x81, 0x82, + 0x04, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeStringWithDifferentEncoding() + { + var obj = new ObjectWithCustomStringAttributeCustomEncoding() { + IntegerValue = 1, + StringValue = "あア", + IgnoredIntegerValue = 2, + AnotherIntegerValue = 4, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0x82, 0xA0, 0x83, 0x41, 0x00, + 0x04, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void TrySerializeStringWithUnknownEncodingThrowsException() + { + var obj = new ObjectWithCustomStringAttributeUnknownEncoding() { + IntegerValue = 1, + StringValue = "あア", + IgnoredIntegerValue = 2, + AnotherIntegerValue = 4, + }; + + using var stream = new DataStream(); + var serializer = new BinarySerializer(stream); + + _ = Assert.Throws(() => serializer.Serialize(obj)); + } + + [Test] + public void SerializeObjectWithSpecificEndianness() + { + var obj = new ObjectWithForcedEndianness() { + LittleEndianInteger = 1, + BigEndianInteger = 2, + DefaultEndianInteger = 3, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, + 0x03, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeEnum() + { + var obj = new ObjectWithEnum() { + EnumValue = Enum1.Value2, + }; + + byte[] expected = { 0x01 }; + + AssertSerialization(obj, expected); + } + + private static void AssertSerialization(T obj, byte[] expected) + { + using var stream = new DataStream(); + var serializer = new BinarySerializer(stream); + + serializer.Serialize(obj); + + AssertBinary(stream, expected); + } + + private static void AssertBinary(DataStream actual, byte[] expected) + { + Assert.That(expected.Length, Is.EqualTo(actual.Length)); + + byte[] actualData = new byte[expected.Length]; + Assert.Multiple(() => { + actual.Position = 0; + int read = actual.Read(actualData); + + Assert.That(read, Is.EqualTo(expected.Length)); + Assert.That(actualData, Is.EquivalentTo(actualData)); + }); + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ComplexObject + { + public int IntegerValue { get; set; } + + public long LongValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class NestedObject + { + public int IntegerValue { get; set; } + + public ComplexObject ComplexValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithDefaultBooleanAttribute + { + public int IntegerValue { get; set; } + + [BinaryBoolean] + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithoutBooleanAttribute + { + public int IntegerValue { get; set; } + + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomBooleanAttribute + { + public int IntegerValue { get; set; } + + [BinaryBoolean(WriteAs = typeof(string), TrueValue = "true", FalseValue = "false")] + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithDefaultStringAttribute + { + public int IntegerValue { get; set; } + + [BinaryString] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithoutStringAttribute + { + public int IntegerValue { get; set; } + + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomStringAttributeSizeUshort + { + public int IntegerValue { get; set; } + + [BinaryString(SizeType = typeof(ushort), Terminator = "")] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomStringAttributeFixedSize + { + public int IntegerValue { get; set; } + + [BinaryString(FixedSize = 3, Terminator = "")] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomStringAttributeCustomEncoding + { + public int IntegerValue { get; set; } + + [BinaryString(CodePage = 932)] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomStringAttributeUnknownEncoding + { + public int IntegerValue { get; set; } + + [BinaryString(CodePage = 666)] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithForcedEndianness + { + [BinaryForceEndianness(EndiannessMode.LittleEndian)] + public int LittleEndianInteger { get; set; } + + [BinaryForceEndianness(EndiannessMode.BigEndian)] + public int BigEndianInteger { get; set; } + + public int DefaultEndianInteger { get; set; } + } + + private enum Enum1 + { + Value1, + Value2, + Value3, + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithEnum + { + [BinaryEnum(WriteAs = typeof(byte))] + public Enum1 EnumValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithInt24 + { + [BinaryInt24] + public int Int24Value { get; set; } + } +} diff --git a/src/Yarhl/IO/DataWriter.cs b/src/Yarhl/IO/DataWriter.cs index c5ea2b8d..e8e4d89e 100644 --- a/src/Yarhl/IO/DataWriter.cs +++ b/src/Yarhl/IO/DataWriter.cs @@ -419,6 +419,15 @@ public void Write(char[] chars, Encoding? encoding = null) Write(text, textSize, terminator, encoding); } + /// + /// Write the specified 24-bits value. + /// + /// 24-bits value. + public void WriteInt24(int val) + { + WriteNumber((uint)val, 24); + } + /// /// Write the specified value converting to any supported type. /// @@ -428,66 +437,59 @@ public void Write(char[] chars, Encoding? encoding = null) /// The supported types are: long, ulong, int, uint, short, /// ushort, byte, sbyte, char and string. /// - public void WriteOfType(Type type, dynamic val) + public void WriteOfType(Type type, object val) { - if (val == null) - throw new ArgumentNullException(nameof(val)); - if (type == null) - throw new ArgumentNullException(nameof(type)); - - val = Convert.ChangeType(val, type, CultureInfo.InvariantCulture); - - bool serializable = Attribute.IsDefined(type, typeof(Serialization.Attributes.SerializableAttribute)); - if (serializable) { - WriteUsingReflection(type, val); - } else { - switch (val) { - case long l: - Write(l); - break; - case ulong ul: - Write(ul); - break; - - case int i: - Write(i); - break; - case uint ui: - Write(ui); - break; - - case short s: - Write(s); - break; - case ushort us: - Write(us); - break; - - case byte b: - Write(b); - break; - case sbyte sb: - Write(sb); - break; - - case char ch: - Write(ch); - break; - case string str: - Write(str); - break; - - case float f: - Write(f); - break; - - case double d: - Write(d); - break; - - default: - throw new FormatException("Unsupported type"); - } + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(val); + + object converted = Convert.ChangeType(val, type, CultureInfo.InvariantCulture)!; + + switch (converted) { + case long l: + Write(l); + break; + case ulong ul: + Write(ul); + break; + + case int i: + Write(i); + break; + case uint ui: + Write(ui); + break; + + case short s: + Write(s); + break; + case ushort us: + Write(us); + break; + + case byte b: + Write(b); + break; + case sbyte sb: + Write(sb); + break; + + case char ch: + Write(ch); + break; + case string str: + Write(str); + break; + + case float f: + Write(f); + break; + + case double d: + Write(d); + break; + + default: + throw new FormatException("Unsupported type"); } } @@ -498,8 +500,7 @@ public void WriteOfType(Type type, dynamic val) /// The type of the value. public void WriteOfType(T val) { - if (val == null) - throw new ArgumentNullException(nameof(val)); + ArgumentNullException.ThrowIfNull(val); WriteOfType(typeof(T), val); } @@ -570,7 +571,7 @@ public void WritePadding(byte val, int padding) WriteTimes(val, Stream.Position.Pad(padding) - Stream.Position); } - void WriteNumber(ulong number, byte numBits) + private void WriteNumber(ulong number, byte numBits) { byte start; byte end; @@ -593,60 +594,5 @@ void WriteNumber(ulong number, byte numBits) Stream.WriteByte(val); } } - - void WriteUsingReflection(Type type, dynamic obj) - { - PropertyInfo[] properties = type.GetProperties( - BindingFlags.DeclaredOnly | - BindingFlags.Public | - BindingFlags.Instance); - - foreach (PropertyInfo property in properties) { - bool ignore = Attribute.IsDefined(property, typeof(BinaryIgnoreAttribute)); - if (ignore) { - continue; - } - - EndiannessMode currentEndianness = Endianness; - var endiannessAttr = property.GetCustomAttribute(); - if (endiannessAttr is not null) { - Endianness = endiannessAttr.Mode; - } - - dynamic value = property.GetValue(obj); - - if (property.PropertyType == typeof(bool) && property.GetCustomAttribute() is { } boolAttr) { - // booleans can only be written if they have the attribute. - dynamic typeValue = value ? boolAttr.TrueValue : boolAttr.FalseValue; - WriteOfType(boolAttr.WriteAs, typeValue); - } else if (property.PropertyType == typeof(int) && Attribute.IsDefined(property, typeof(BinaryInt24Attribute))) { - // write the number as int24 - WriteNumber((uint)value, 24); - } else if (property.PropertyType.IsEnum && property.GetCustomAttribute() is { } enumAttr) { - // enums can only be written if they have the attribute. - WriteOfType(enumAttr.WriteAs, value); - } else if (property.PropertyType == typeof(string) && property.GetCustomAttribute() is { } stringAttr) { - Encoding? encoding = null; - if (stringAttr.CodePage != -1) { - encoding = Encoding.GetEncoding(stringAttr.CodePage); - } - - if (stringAttr.SizeType is null) { - if (stringAttr.FixedSize == -1) { - Write((string)value, stringAttr.Terminator, encoding, stringAttr.MaxSize); - } else { - Write((string)value, stringAttr.FixedSize, stringAttr.Terminator, encoding); - } - } else { - Write((string)value, stringAttr.SizeType, stringAttr.Terminator, encoding, stringAttr.MaxSize); - } - } else { - WriteOfType(property.PropertyType, value); - } - - // Restore previous endianness - Endianness = currentEndianness; - } - } } } diff --git a/src/Yarhl/IO/Serialization/BinarySerializer.cs b/src/Yarhl/IO/Serialization/BinarySerializer.cs new file mode 100644 index 00000000..ae9f46c0 --- /dev/null +++ b/src/Yarhl/IO/Serialization/BinarySerializer.cs @@ -0,0 +1,161 @@ +namespace Yarhl.IO.Serialization; + +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Yarhl.IO.Serialization.Attributes; + +/// +/// Binary serialization of objects based on attributes. Equivalent to convert +/// an object into binary. +/// +public class BinarySerializer +{ + private readonly DataWriter writer; + + /// + /// Initializes a new instance of the class. + /// + /// The stream to write the binary data. + public BinarySerializer(Stream stream) + { + writer = new DataWriter(stream); + } + + /// + /// Gets or sets the default endianness for the serialization. + /// + public EndiannessMode DefaultEndianness { get; set; } + + /// + /// Serialize the public properties of the object in binary data in the stream. + /// + /// The object to serialize into the stream. + /// The stream to write the binary data. + /// The type of the object. + public static void Serialize(T obj, Stream stream) + { + new BinarySerializer(stream).Serialize(obj); + } + + /// + /// Serialize the public properties of the object in binary data in the stream. + /// + /// The type of object to serialize. + /// The object to serialize into the stream. + /// The stream to write the binary data. + public static void Serialize(Type objType, object obj, Stream stream) + { + new BinarySerializer(stream).Serialize(objType, obj); + } + + /// + /// Serialize the public properties of the object in binary data in the stream. + /// + /// The object to serialize into the stream. + /// The type of the object. + public void Serialize(T obj) + { + ArgumentNullException.ThrowIfNull(obj); + + Serialize(typeof(T), obj); + } + + /// + /// Serialize the public properties of the object in binary data in the stream. + /// + /// The type of object to serialize. + /// The object to serialize into the stream. + public void Serialize(Type type, object obj) + { + PropertyInfo[] properties = type.GetProperties( + BindingFlags.DeclaredOnly | + BindingFlags.Public | + BindingFlags.Instance); + + // TODO: Introduce property to sort + foreach (PropertyInfo property in properties) { + bool ignore = Attribute.IsDefined(property, typeof(BinaryIgnoreAttribute)); + if (ignore) { + continue; + } + + SerializeProperty(property, obj); + } + } + + private void SerializeProperty(PropertyInfo property, object obj) + { + writer.Endianness = DefaultEndianness; + var endiannessAttr = property.GetCustomAttribute(); + if (endiannessAttr is not null) { + writer.Endianness = endiannessAttr.Mode; + } + + object value = property.GetValue(obj) + ?? throw new FormatException("Cannot serialize nullable values"); + + if (property.PropertyType.IsPrimitive) { + SerializePrimitiveField(property, value); + } else if (property.PropertyType.IsEnum) { + if (property.GetCustomAttribute() is not { } enumAttr) { + throw new FormatException("Missing BinaryEnum attribute in property"); + } + + writer.WriteOfType(enumAttr.WriteAs, value); + } else if (property.PropertyType == typeof(string)) { + SerializeString(property, value); + } else { + Serialize(property.PropertyType, value); + } + } + + private void SerializePrimitiveField(PropertyInfo property, object value) + { + // Handle first the special cases + if (property.PropertyType == typeof(bool)) { + if (property.GetCustomAttribute() is not { } boolAttr) { + throw new FormatException("Properties of type 'bool' must have the attribute BinaryBoolean"); + } + + object typeValue = (bool)value ? boolAttr.TrueValue : boolAttr.FalseValue; + writer.WriteOfType(boolAttr.WriteAs, typeValue); + return; + } + + if (property.PropertyType == typeof(int) && Attribute.IsDefined(property, typeof(BinaryInt24Attribute))) { + writer.WriteInt24((int)value); + return; + } + + // Fallback to DataWriter primitive write + writer.WriteOfType(property.PropertyType, value); + } + + private void SerializeString(PropertyInfo property, object value) + { + if (property.GetCustomAttribute() is not { } stringAttr) { + // Use default settings if not specified. + writer.Write((string)value); + return; + } + + Encoding? encoding = null; + if (stringAttr.CodePage != -1) { + encoding = Encoding.GetEncoding(stringAttr.CodePage); + } + + string strValue = (string)value; + + if (stringAttr.SizeType is null) { + if (stringAttr.FixedSize == -1) { + writer.Write(strValue, stringAttr.Terminator, encoding, stringAttr.MaxSize); + } else { + writer.Write(strValue, stringAttr.FixedSize, stringAttr.Terminator, encoding); + } + } else { + writer.Write(strValue, stringAttr.SizeType, stringAttr.Terminator, encoding, stringAttr.MaxSize); + } + } +} From bdc37a366f4e7f81e19efcabae7cf386320c34eb Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Fri, 26 Jan 2024 10:45:49 +0100 Subject: [PATCH 02/10] :arrow_up: Bump dependencies --- build/orchestrator/BuildSystem.csproj | 2 +- src/Directory.Packages.props | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/build/orchestrator/BuildSystem.csproj b/build/orchestrator/BuildSystem.csproj index ee8b40fb..4d63cfa9 100644 --- a/build/orchestrator/BuildSystem.csproj +++ b/build/orchestrator/BuildSystem.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 1d55a5b5..7616e76a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,20 +1,17 @@ - - - + + - - - + + - - + \ No newline at end of file From e8265b5382f60cee4f633e51cf3fff34c4a4d09d Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Fri, 26 Jan 2024 13:06:49 +0100 Subject: [PATCH 03/10] :shirt: Extract binary deserializer into class --- src/Yarhl.UnitTests/IO/DataReaderTests.cs | 572 ------------------ .../Serialization/BinaryDeserializerTests.cs | 480 +++++++++++++++ .../IO/Serialization/BinarySerializerTests.cs | 14 + src/Yarhl/IO/DataReader.cs | 114 +--- .../IO/Serialization/BinaryDeserializer.cs | 160 +++++ 5 files changed, 676 insertions(+), 664 deletions(-) create mode 100644 src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs create mode 100644 src/Yarhl/IO/Serialization/BinaryDeserializer.cs diff --git a/src/Yarhl.UnitTests/IO/DataReaderTests.cs b/src/Yarhl.UnitTests/IO/DataReaderTests.cs index b1c2a5fc..cfacf4a6 100644 --- a/src/Yarhl.UnitTests/IO/DataReaderTests.cs +++ b/src/Yarhl.UnitTests/IO/DataReaderTests.cs @@ -33,13 +33,6 @@ public class DataReaderTests DataStream stream; DataReader reader; - enum Enum1 - { - Value1, - Value2, - Value3, - } - [OneTimeSetUp] public void FixtureSetUp() { @@ -911,570 +904,5 @@ public void ReadByTypeThrowExceptionForNullType() stream.Position = 0; Assert.Throws(() => reader.ReadByType((Type)null)); } - - [Test] - public void ReadUsingReflection() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ComplexObject obj = reader.Read(); - - Assert.AreEqual(1, obj.IntegerValue); - Assert.AreEqual(2L, obj.LongValue); - Assert.AreEqual(0, obj.IgnoredIntegerValue); - Assert.AreEqual(3, obj.AnotherIntegerValue); - } - - [Test] - public void ReadNestedObjectUsingReflection() - { - byte[] expected = { - 0x0A, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, - 0x14, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - NestedObject obj = reader.Read(); - - Assert.AreEqual(10, obj.IntegerValue); - Assert.AreEqual(1, obj.ComplexValue.IntegerValue); - Assert.AreEqual(2L, obj.ComplexValue.LongValue); - Assert.AreEqual(0, obj.ComplexValue.IgnoredIntegerValue); - Assert.AreEqual(3, obj.ComplexValue.AnotherIntegerValue); - Assert.AreEqual(20, obj.AnotherIntegerValue); - } - - [Test] - public void ReadBooleanUsingReflection() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithDefaultBooleanAttribute obj = reader.Read(); - - Assert.AreEqual(1, obj.IntegerValue); - Assert.AreEqual(false, obj.BooleanValue); - Assert.AreEqual(0, obj.IgnoredIntegerValue); - Assert.AreEqual(3, obj.AnotherIntegerValue); - } - - [Test] - public void ReadCustomBooleanUsingReflection() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x74, 0x72, 0x75, 0x65, 0x00, // "true" - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithCustomBooleanAttribute obj = reader.Read(); - - Assert.AreEqual(1, obj.IntegerValue); - Assert.AreEqual(true, obj.BooleanValue); - Assert.AreEqual(0, obj.IgnoredIntegerValue); - Assert.AreEqual(3, obj.AnotherIntegerValue); - } - - [Test] - public void ReadBooleanWithoutAttributeThrowsException() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - Assert.Throws(() => reader.Read()); - } - - [Test] - public void ReadStringWithoutAttributeUsesDefaultReaderSettings() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0xE3, 0x81, 0x82, 0xE3, 0x82, 0xA2, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithoutStringAttribute obj = reader.Read(); - - Assert.AreEqual(1, obj.IntegerValue); - Assert.AreEqual("あア", obj.StringValue); - Assert.AreEqual(0, obj.IgnoredIntegerValue); - Assert.AreEqual(3, obj.AnotherIntegerValue); - } - - [Test] - public void ReadStringWithDefaultAttributeUsesDefaultReaderSettings() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0xE3, 0x81, 0x82, 0xE3, 0x82, 0xA2, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithDefaultStringAttribute obj = reader.Read(); - - Assert.AreEqual(1, obj.IntegerValue); - Assert.AreEqual("あア", obj.StringValue); - Assert.AreEqual(0, obj.IgnoredIntegerValue); - Assert.AreEqual(3, obj.AnotherIntegerValue); - } - - [Test] - public void ReadCustomStringWithSizeTypeUsingReflection() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x03, 0x00, 0xE3, 0x81, 0x82, - 0x04, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithCustomStringAttributeSizeUshort obj = reader.Read(); - - Assert.AreEqual(1, obj.IntegerValue); - Assert.AreEqual("あ", obj.StringValue); - Assert.AreEqual(0, obj.IgnoredIntegerValue); - Assert.AreEqual(4, obj.AnotherIntegerValue); - } - - [Test] - public void ReadCustomFixedStringUsingReflection() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0xE3, 0x81, 0x82, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithCustomStringAttributeFixedSize obj = reader.Read(); - - Assert.AreEqual(1, obj.IntegerValue); - Assert.AreEqual("あ", obj.StringValue); - Assert.AreEqual(0, obj.IgnoredIntegerValue); - Assert.AreEqual(3, obj.AnotherIntegerValue); - } - - [Test] - public void ReadCustomStringUsingReflectionWithDifferentEncoding() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x82, 0xA0, 0x83, 0x41, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithCustomStringAttributeCustomEncoding obj = reader.Read(); - - Assert.AreEqual(1, obj.IntegerValue); - Assert.AreEqual("あア", obj.StringValue); - Assert.AreEqual(0, obj.IgnoredIntegerValue); - Assert.AreEqual(3, obj.AnotherIntegerValue); - } - - [Test] - public void ReadCustomStringUsingReflectionWithUnknownEncodingThrowsException() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x82, 0xA0, 0x83, 0x41, 0x00, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - Assert.Throws(() => reader.Read()); - } - - [Test] - public void ReadObjectWithForcedEndianness() - { - byte[] expected = { - 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x02, - 0x03, 0x00, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithForcedEndianness obj = reader.Read(); - - Assert.AreEqual(1, obj.LittleEndianInteger); - Assert.AreEqual(2, obj.BigEndianInteger); - Assert.AreEqual(3, obj.DefaultEndianInteger); - } - - [Test] - public void ReadObjectWithEnumValue() - { - byte[] expected = { - 0x01, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithEnum obj = reader.Read(); - - Assert.AreEqual(Enum1.Value2, obj.EnumValue); - } - - [Test] - public void ReadObjectWithInt24() - { - byte[] expected = { - 0x01, 0x00, 0x00, - }; - stream.Write(expected, 0, expected.Length); - - stream.Position = 0; - - ObjectWithInt24 obj = reader.Read(); - - Assert.AreEqual(1, obj.Int24Value); - } - - [Test] - public void ReflectionReadingDoesNotSupportNullable() - { - stream.Write(new byte[4], 0, 4); - stream.Position = 0; - - Assert.That( - () => reader.Read(), - Throws.InstanceOf()); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ComplexObject - { - public int IntegerValue { get; set; } - - public long LongValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class NestedObject - { - public int IntegerValue { get; set; } - - public ComplexObject ComplexValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithDefaultBooleanAttribute - { - public int IntegerValue { get; set; } - - [BinaryBoolean] - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithoutBooleanAttribute - { - public int IntegerValue { get; set; } - - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomBooleanAttribute - { - public int IntegerValue { get; set; } - - [BinaryBoolean(ReadAs = typeof(string), TrueValue = "true")] - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithDefaultStringAttribute - { - public int IntegerValue { get; set; } - - [BinaryString] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithoutStringAttribute - { - public int IntegerValue { get; set; } - - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeSizeUshort - { - public int IntegerValue { get; set; } - - [BinaryString(SizeType = typeof(ushort), Terminator = "")] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeFixedSize - { - public int IntegerValue { get; set; } - - [BinaryString(FixedSize = 3, Terminator = "")] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeCustomEncoding - { - public int IntegerValue { get; set; } - - [BinaryString(CodePage = 932)] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeUnknownEncoding - { - public int IntegerValue { get; set; } - - [BinaryString(CodePage = 666)] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithForcedEndianness - { - [BinaryForceEndianness(EndiannessMode.LittleEndian)] - public int LittleEndianInteger { get; set; } - - [BinaryForceEndianness(EndiannessMode.BigEndian)] - public int BigEndianInteger { get; set; } - - public int DefaultEndianInteger { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithEnum - { - [BinaryEnum(ReadAs = typeof(byte))] - public Enum1 EnumValue { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Microsoft.Performance", - "CA1812:Class never instantiated", - Justification = "The class is instantiated by reflection")] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Sonar.CodeSmell", - "S3459:Unassigned auto-property", - Justification = "The properties are assigned by reflection")] - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithInt24 - { - [BinaryInt24] - public int Int24Value { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithNullable - { - public int? NullValue { get; set; } - } } } diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs new file mode 100644 index 00000000..a8f8a9fd --- /dev/null +++ b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs @@ -0,0 +1,480 @@ +namespace Yarhl.UnitTests.IO.Serialization; + +using System; +using FluentAssertions; +using NUnit.Framework; +using Yarhl.IO; +using Yarhl.IO.Serialization; +using Yarhl.IO.Serialization.Attributes; + +[TestFixture] +public class BinaryDeserializerTests +{ + [Test] + public void DeserializeFullObject() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + var expected = new ComplexObject { + IntegerValue = 1, + LongValue = 2L, + IgnoredIntegerValue = 0, + AnotherIntegerValue = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeNestedObject() + { + byte[] data = { + 0x0A, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, + }; + var expected = new NestedObject { + IntegerValue = 10, + ComplexValue = new ComplexObject { + IntegerValue = 1, + LongValue = 2L, + IgnoredIntegerValue = 0, + AnotherIntegerValue = 3, + }, + AnotherIntegerValue = 20, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeBoolean() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new ObjectWithDefaultBooleanAttribute { + IntegerValue = 1, + BooleanValue = false, + IgnoredIntegerValue = 0, + AnotherIntegerValue = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeBooleanWithCustomTrueValue() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0x74, 0x72, 0x75, 0x65, 0x00, // "true" + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new ObjectWithCustomBooleanAttribute { + IntegerValue = 1, + BooleanValue = true, + IgnoredIntegerValue = 0, + AnotherIntegerValue = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void TryDeserializeBooleanWithoutAttributeThrowsException() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + var stream = new DataStream(); + stream.Write(data); + + stream.Position = 0; + var deserializer = new BinaryDeserializer(stream); + Assert.That( + () => deserializer.Deserialize(), + Throws.InstanceOf()); + } + + [Test] + public void DeserializeInt24() + { + byte[] data = { + 0x01, 0x00, 0x00, + }; + + var expected = new ObjectWithInt24 { + Int24Value = 1, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeStringWithoutAttributeUsesDefaultReaderSettings() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0xE3, 0x81, 0x82, 0xE3, 0x82, 0xA2, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new ObjectWithoutStringAttribute { + IntegerValue = 1, + StringValue = "あア", + IgnoredIntegerValue = 0, + AnotherIntegerValue = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeStringWithDefaultAttributeUsesDefaultReaderSettings() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0xE3, 0x81, 0x82, 0xE3, 0x82, 0xA2, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new ObjectWithDefaultStringAttribute { + IntegerValue = 1, + StringValue = "あア", + IgnoredIntegerValue = 0, + AnotherIntegerValue = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeStringWithSizeType() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0x03, 0x00, 0xE3, 0x81, 0x82, + 0x04, 0x00, 0x00, 0x00, + }; + + var expected = new ObjectWithCustomStringAttributeSizeUshort { + IntegerValue = 1, + StringValue = "あ", + IgnoredIntegerValue = 0, + AnotherIntegerValue = 4, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeStringWithFixedSize() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0xE3, 0x81, 0x82, + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new ObjectWithCustomStringAttributeFixedSize { + IntegerValue = 1, + StringValue = "あ", + IgnoredIntegerValue = 0, + AnotherIntegerValue = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeStringWithDifferentEncoding() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0x82, 0xA0, 0x83, 0x41, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new ObjectWithCustomStringAttributeCustomEncoding { + IntegerValue = 1, + StringValue = "あア", + IgnoredIntegerValue = 0, + AnotherIntegerValue = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void TryDeserializeStringWithUnknownEncodingThrowsException() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0x82, 0xA0, 0x83, 0x41, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + var stream = new DataStream(); + stream.Write(data); + + stream.Position = 0; + var deserializer = new BinaryDeserializer(stream); + + Assert.That( + () => deserializer.Deserialize(), + Throws.InstanceOf()); + } + + [Test] + public void DeserializeObjectWithSpecificEndianness() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new ObjectWithForcedEndianness { + LittleEndianInteger = 1, + BigEndianInteger = 2, + DefaultEndianInteger = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeEnum() + { + byte[] data = { 0x01 }; + + var expected = new ObjectWithEnum { EnumValue = Enum1.Value2 }; + + AssertDeserialization(data, expected); + } + + [Test] + public void TryDeserializeNullableThrowsException() + { + var stream = new DataStream(); + stream.Write(new byte[4]); + + stream.Position = 0; + var deserializer = new BinaryDeserializer(stream); + + Assert.That( + () => deserializer.Deserialize(), + Throws.InstanceOf()); + } + + private static void AssertDeserialization(byte[] data, T expected) + { + var stream = new DataStream(); + stream.Write(data); + + stream.Position = 0; + var deserializer = new BinaryDeserializer(stream); + T obj = deserializer.Deserialize(); + + _ = obj.Should().BeEquivalentTo(expected); + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ComplexObject + { + public int IntegerValue { get; set; } + + public long LongValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class NestedObject + { + public int IntegerValue { get; set; } + + public ComplexObject ComplexValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithDefaultBooleanAttribute + { + public int IntegerValue { get; set; } + + [BinaryBoolean] + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithoutBooleanAttribute + { + public int IntegerValue { get; set; } + + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomBooleanAttribute + { + public int IntegerValue { get; set; } + + [BinaryBoolean(ReadAs = typeof(string), TrueValue = "true")] + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithDefaultStringAttribute + { + public int IntegerValue { get; set; } + + [BinaryString] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithoutStringAttribute + { + public int IntegerValue { get; set; } + + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomStringAttributeSizeUshort + { + public int IntegerValue { get; set; } + + [BinaryString(SizeType = typeof(ushort), Terminator = "")] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomStringAttributeFixedSize + { + public int IntegerValue { get; set; } + + [BinaryString(FixedSize = 3, Terminator = "")] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomStringAttributeCustomEncoding + { + public int IntegerValue { get; set; } + + [BinaryString(CodePage = 932)] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithCustomStringAttributeUnknownEncoding + { + public int IntegerValue { get; set; } + + [BinaryString(CodePage = 666)] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithForcedEndianness + { + [BinaryForceEndianness(EndiannessMode.LittleEndian)] + public int LittleEndianInteger { get; set; } + + [BinaryForceEndianness(EndiannessMode.BigEndian)] + public int BigEndianInteger { get; set; } + + public int DefaultEndianInteger { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithEnum + { + [BinaryEnum(ReadAs = typeof(byte))] + public Enum1 EnumValue { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithInt24 + { + [BinaryInt24] + public int Int24Value { get; set; } + } + + [Yarhl.IO.Serialization.Attributes.Serializable] + private class ObjectWithNullable + { + public int? NullValue { get; set; } + } + + private enum Enum1 + { + Value1, + Value2, + Value3, + } +} diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs index e6df74d0..3a51c87d 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -108,6 +108,20 @@ public void TrySerializeBooleanWithoutAttributeThrowsException() _ = Assert.Throws(() => serializer.Serialize(obj)); } + [Test] + public void SerializeInt24() + { + var obj = new ObjectWithInt24 { + Int24Value = 0x7F_FC0FFE, + }; + + byte[] expected = { + 0xFE, 0x0F, 0xFC, + }; + + AssertSerialization(obj, expected); + } + [Test] public void SerializeStringWithoutAttributeUsesDefaultWriterSettings() { diff --git a/src/Yarhl/IO/DataReader.cs b/src/Yarhl/IO/DataReader.cs index db6e6927..c2152b4b 100644 --- a/src/Yarhl/IO/DataReader.cs +++ b/src/Yarhl/IO/DataReader.cs @@ -403,12 +403,13 @@ public string ReadString(int bytesCount, Encoding? encoding = null) /// Optional encoding to use. public string ReadString(Type sizeType, Encoding? encoding = null) { - if (encoding == null) + if (encoding is null) { encoding = DefaultEncoding; + } - dynamic size = ReadByType(sizeType); + object size = ReadByType(sizeType); size = Convert.ChangeType(size, typeof(int), CultureInfo.InvariantCulture); - return ReadString(size, encoding); + return ReadString((int)size, encoding); } /// @@ -417,39 +418,35 @@ public string ReadString(Type sizeType, Encoding? encoding = null) /// The field. /// Nullable types are not supported. /// Type of the field. - public dynamic ReadByType(Type type) + public object ReadByType(Type type) { - if (type == null) - throw new ArgumentNullException(nameof(type)); + ArgumentNullException.ThrowIfNull(type); - bool serializable = Attribute.IsDefined(type, typeof(Serialization.Attributes.SerializableAttribute)); - if (serializable) - return ReadUsingReflection(type); - - if (type == typeof(long)) + if (type == typeof(long)) { return ReadInt64(); - if (type == typeof(ulong)) + } else if (type == typeof(ulong)) { return ReadUInt64(); - if (type == typeof(int)) + } else if (type == typeof(int)) { return ReadInt32(); - if (type == typeof(uint)) + } else if (type == typeof(uint)) { return ReadUInt32(); - if (type == typeof(short)) + } else if (type == typeof(short)) { return ReadInt16(); - if (type == typeof(ushort)) + } else if (type == typeof(ushort)) { return ReadUInt16(); - if (type == typeof(byte)) + } else if (type == typeof(byte)) { return ReadByte(); - if (type == typeof(sbyte)) + } else if (type == typeof(sbyte)) { return ReadSByte(); - if (type == typeof(char)) + } else if (type == typeof(char)) { return ReadChar(); - if (type == typeof(string)) + } else if (type == typeof(string)) { return ReadString(); - if (type == typeof(float)) + } else if (type == typeof(float)) { return ReadSingle(); - if (type == typeof(double)) + } else if (type == typeof(double)) { return ReadDouble(); + } throw new FormatException("Unsupported type"); } @@ -459,7 +456,7 @@ public dynamic ReadByType(Type type) /// /// The field. /// The type of the field. - public dynamic Read() + public object Read() { return ReadByType(typeof(T)); } @@ -470,8 +467,9 @@ public dynamic Read() /// Padding value. public void SkipPadding(int padding) { - if (padding < 0) + if (padding < 0) { throw new ArgumentOutOfRangeException(nameof(padding)); + } if (padding <= 1) { return; @@ -482,73 +480,5 @@ public void SkipPadding(int padding) _ = Stream.Seek(remainingBytes, SeekOrigin.Current); } } - - dynamic ReadUsingReflection(Type type) - { - // It returns null for Nullable, but as that is a class and - // it won't have the serializable attribute, it will throw an - // unsupported exception before. So this can't be null at this point. - #pragma warning disable SA1009 // False positive - object obj = Activator.CreateInstance(type)!; - #pragma warning restore SA1009 - - PropertyInfo[] properties = type.GetProperties( - BindingFlags.DeclaredOnly | - BindingFlags.Public | - BindingFlags.Instance); - - foreach (PropertyInfo property in properties) { - bool ignore = Attribute.IsDefined(property, typeof(BinaryIgnoreAttribute)); - if (ignore) { - continue; - } - - EndiannessMode currentEndianness = Endianness; - bool forceEndianness = Attribute.IsDefined(property, typeof(BinaryForceEndiannessAttribute)); - if (forceEndianness) { - var attr = Attribute.GetCustomAttribute(property, typeof(BinaryForceEndiannessAttribute)) as BinaryForceEndiannessAttribute; - Endianness = attr!.Mode; - } - - if (property.PropertyType == typeof(bool) && Attribute.IsDefined(property, typeof(BinaryBooleanAttribute))) { - // booleans can only be read if they have the attribute. - var attr = Attribute.GetCustomAttribute(property, typeof(BinaryBooleanAttribute)) as BinaryBooleanAttribute; - dynamic value = ReadByType(attr!.ReadAs); - property.SetValue(obj, value == (dynamic)attr.TrueValue); - } else if (property.PropertyType == typeof(int) && Attribute.IsDefined(property, typeof(BinaryInt24Attribute))) { - // read the number as int24. - int value = ReadInt24(); - property.SetValue(obj, value); - } else if (property.PropertyType.IsEnum && Attribute.IsDefined(property, typeof(BinaryEnumAttribute))) { - // enums can only be read if they have the attribute. - var attr = Attribute.GetCustomAttribute(property, typeof(BinaryEnumAttribute)) as BinaryEnumAttribute; - dynamic value = ReadByType(attr!.ReadAs); - property.SetValue(obj, Enum.ToObject(property.PropertyType, value)); - } else if (property.PropertyType == typeof(string) && Attribute.IsDefined(property, typeof(BinaryStringAttribute))) { - var attr = Attribute.GetCustomAttribute(property, typeof(BinaryStringAttribute)) as BinaryStringAttribute; - Encoding? encoding = null; - if (attr!.CodePage != -1) { - encoding = Encoding.GetEncoding(attr.CodePage); - } - - dynamic value; - if (attr.SizeType == null) { - value = attr.FixedSize == -1 ? this.ReadString(encoding) : this.ReadString(attr.FixedSize, encoding); - } else { - value = ReadString(attr.SizeType, encoding); - } - - property.SetValue(obj, value); - } else { - dynamic value = ReadByType(property.PropertyType); - property.SetValue(obj, value); - } - - // Restore previous endianness - Endianness = currentEndianness; - } - - return obj; - } } } diff --git a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs new file mode 100644 index 00000000..f8e368fc --- /dev/null +++ b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs @@ -0,0 +1,160 @@ +namespace Yarhl.IO.Serialization; + +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Yarhl.IO.Serialization.Attributes; + +/// +/// Binary deserialization of objects based on their attributes. Equivalent of +/// converting a binary format into an object. +/// +public class BinaryDeserializer +{ + private readonly DataReader reader; + + /// + /// Initializes a new instance of the class. + /// + /// The stream to read from. + public BinaryDeserializer(Stream stream) + { + reader = new DataReader(stream); + } + + /// + /// Gets or sets the default endianness for the deserialization. + /// + public EndiannessMode DefaultEndianness { get; set; } + + /// + /// Deserialize an object from the binary data of the stream. + /// + /// The type of the object to deserialize. + /// The stream to read from. + /// A new object deserialized. + public static T Deserialize(Stream stream) + { + return new BinaryDeserializer(stream).Deserialize(); + } + + /// + /// Deserialize an object from the binary data of the stream. + /// + /// The stream to read from. + /// The type of the object to deserialize. + /// A new object deserialized. + public static object Deserialize(Stream stream, Type objType) + { + return new BinaryDeserializer(stream).Deserialize(objType); + } + + /// + /// Deserialize an object from the binary data of the stream. + /// + /// The type of the object to deserialize. + /// A new object deserialized. + public T Deserialize() + { + return (T)Deserialize(typeof(T)); + } + + /// + /// Deserialize an object from the binary data of the stream. + /// + /// The type of the object to deserialize. + /// A new object deserialized. + public object Deserialize(Type objType) + { + // It returns null for Nullable, but as that is a class and + // it won't have the serializable attribute, it will throw an + // unsupported exception before. So this can't be null at this point. + object obj = Activator.CreateInstance(objType)!; + + PropertyInfo[] properties = objType.GetProperties( + BindingFlags.DeclaredOnly | + BindingFlags.Public | + BindingFlags.Instance); + + foreach (PropertyInfo property in properties) { + bool ignore = Attribute.IsDefined(property, typeof(BinaryIgnoreAttribute)); + if (ignore) { + continue; + } + + object propertyValue = DeserializePropertyValue(property); + property.SetValue(obj, propertyValue); + } + + return obj; + } + + private object DeserializePropertyValue(PropertyInfo property) + { + reader.Endianness = DefaultEndianness; + var endiannessAttr = property.GetCustomAttribute(); + if (endiannessAttr is not null) { + reader.Endianness = endiannessAttr.Mode; + } + + if (property.PropertyType.IsPrimitive) { + return DeserializePrimitiveField(property); + } else if (property.PropertyType.IsEnum) { + return DeserializeEnumField(property); + } else if (property.PropertyType == typeof(string)) { + return DeserializeStringField(property); + } else { + return Deserialize(property.PropertyType); + } + } + + private object DeserializePrimitiveField(PropertyInfo property) + { + if (property.PropertyType == typeof(bool)) { + if (property.GetCustomAttribute() is not { } boolAttr) { + throw new FormatException("Properties of type 'bool' must have the attribute BinaryBoolean"); + } + + object value = reader.ReadByType(boolAttr!.ReadAs); + return value.Equals(boolAttr.TrueValue); + } + + if (property.PropertyType == typeof(int) && Attribute.IsDefined(property, typeof(BinaryInt24Attribute))) { + return reader.ReadInt24(); + } + + return reader.ReadByType(property.PropertyType); + } + + private object DeserializeEnumField(PropertyInfo property) + { + if (property.GetCustomAttribute() is not { } enumAttr) { + throw new FormatException("Missing BinaryEnum attribute in property"); + } + + object value = reader.ReadByType(enumAttr!.ReadAs); + return Enum.ToObject(property.PropertyType, value); + } + + private string DeserializeStringField(PropertyInfo property) + { + if (property.GetCustomAttribute() is not { } stringAttr) { + // Use default settings if not specified. + return reader.ReadString(); + } + + Encoding? encoding = null; + if (stringAttr!.CodePage != -1) { + encoding = Encoding.GetEncoding(stringAttr.CodePage); + } + + if (stringAttr.SizeType is null) { + return (stringAttr.FixedSize == -1) + ? reader.ReadString(encoding) + : reader.ReadString(stringAttr.FixedSize, encoding); + } + + return reader.ReadString(stringAttr.SizeType, encoding); + } +} From c46377e0ca7971956eb911712b071058d06e1849 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Sun, 28 Jan 2024 22:18:00 +0100 Subject: [PATCH 04/10] :fire: Remove unnecessary serializable attribute Refactor type test classes --- .../Serialization/BinaryDeserializerTests.cs | 186 ----------------- .../Serialization/BinarySerializableTypes.cs | 179 +++++++++++++++++ .../IO/Serialization/BinarySerializerTests.cs | 188 +----------------- .../Attributes/SerializableAttribute.cs | 31 --- 4 files changed, 183 insertions(+), 401 deletions(-) create mode 100644 src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs delete mode 100644 src/Yarhl/IO/Serialization/Attributes/SerializableAttribute.cs diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs index a8f8a9fd..69461834 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs @@ -291,190 +291,4 @@ private static void AssertDeserialization(byte[] data, T expected) _ = obj.Should().BeEquivalentTo(expected); } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ComplexObject - { - public int IntegerValue { get; set; } - - public long LongValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class NestedObject - { - public int IntegerValue { get; set; } - - public ComplexObject ComplexValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithDefaultBooleanAttribute - { - public int IntegerValue { get; set; } - - [BinaryBoolean] - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithoutBooleanAttribute - { - public int IntegerValue { get; set; } - - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomBooleanAttribute - { - public int IntegerValue { get; set; } - - [BinaryBoolean(ReadAs = typeof(string), TrueValue = "true")] - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithDefaultStringAttribute - { - public int IntegerValue { get; set; } - - [BinaryString] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithoutStringAttribute - { - public int IntegerValue { get; set; } - - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeSizeUshort - { - public int IntegerValue { get; set; } - - [BinaryString(SizeType = typeof(ushort), Terminator = "")] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeFixedSize - { - public int IntegerValue { get; set; } - - [BinaryString(FixedSize = 3, Terminator = "")] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeCustomEncoding - { - public int IntegerValue { get; set; } - - [BinaryString(CodePage = 932)] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeUnknownEncoding - { - public int IntegerValue { get; set; } - - [BinaryString(CodePage = 666)] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithForcedEndianness - { - [BinaryForceEndianness(EndiannessMode.LittleEndian)] - public int LittleEndianInteger { get; set; } - - [BinaryForceEndianness(EndiannessMode.BigEndian)] - public int BigEndianInteger { get; set; } - - public int DefaultEndianInteger { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithEnum - { - [BinaryEnum(ReadAs = typeof(byte))] - public Enum1 EnumValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithInt24 - { - [BinaryInt24] - public int Int24Value { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithNullable - { - public int? NullValue { get; set; } - } - - private enum Enum1 - { - Value1, - Value2, - Value3, - } } diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs new file mode 100644 index 00000000..a9001756 --- /dev/null +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs @@ -0,0 +1,179 @@ +namespace Yarhl.UnitTests.IO.Serialization; + +using Yarhl.IO; +using Yarhl.IO.Serialization.Attributes; + +// Disable file may only contain a single class since we aren't going +// to create a file per test converter. +#pragma warning disable SA1649 // File name match type name + +public class ComplexObject +{ + public int IntegerValue { get; set; } + + public long LongValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class NestedObject +{ + public int IntegerValue { get; set; } + + public ComplexObject ComplexValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithDefaultBooleanAttribute +{ + public int IntegerValue { get; set; } + + [BinaryBoolean] + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithoutBooleanAttribute +{ + public int IntegerValue { get; set; } + + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithCustomBooleanAttribute +{ + public int IntegerValue { get; set; } + + [BinaryBoolean(ReadAs = typeof(string), WriteAs = typeof(string), TrueValue = "true", FalseValue = "false")] + public bool BooleanValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithDefaultStringAttribute +{ + public int IntegerValue { get; set; } + + [BinaryString] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithoutStringAttribute +{ + public int IntegerValue { get; set; } + + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithCustomStringAttributeSizeUshort +{ + public int IntegerValue { get; set; } + + [BinaryString(SizeType = typeof(ushort), Terminator = "")] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithCustomStringAttributeFixedSize +{ + public int IntegerValue { get; set; } + + [BinaryString(FixedSize = 3, Terminator = "")] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithCustomStringAttributeCustomEncoding +{ + public int IntegerValue { get; set; } + + [BinaryString(CodePage = 932)] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithCustomStringAttributeUnknownEncoding +{ + public int IntegerValue { get; set; } + + [BinaryString(CodePage = 666)] + public string StringValue { get; set; } + + [BinaryIgnore] + public int IgnoredIntegerValue { get; set; } + + public int AnotherIntegerValue { get; set; } +} + +public class ObjectWithForcedEndianness +{ + [BinaryForceEndianness(EndiannessMode.LittleEndian)] + public int LittleEndianInteger { get; set; } + + [BinaryForceEndianness(EndiannessMode.BigEndian)] + public int BigEndianInteger { get; set; } + + public int DefaultEndianInteger { get; set; } +} + +public class ObjectWithEnum +{ + [BinaryEnum(ReadAs = typeof(byte), WriteAs = typeof(short))] + public Enum1 EnumValue { get; set; } +} + +public class ObjectWithInt24 +{ + [BinaryInt24] + public int Int24Value { get; set; } +} + +public class ObjectWithNullable +{ + public int? NullValue { get; set; } +} + +public enum Enum1 +{ + Value1, + Value2, + Value3, +} diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs index 3a51c87d..4569b783 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -258,7 +258,7 @@ public void SerializeEnum() EnumValue = Enum1.Value2, }; - byte[] expected = { 0x01 }; + byte[] expected = { 0x01, 0x00 }; AssertSerialization(obj, expected); } @@ -275,195 +275,15 @@ private static void AssertSerialization(T obj, byte[] expected) private static void AssertBinary(DataStream actual, byte[] expected) { - Assert.That(expected.Length, Is.EqualTo(actual.Length)); + Assert.That(actual.Length, Is.EqualTo(expected.Length), "Stream size mismatch"); byte[] actualData = new byte[expected.Length]; Assert.Multiple(() => { actual.Position = 0; int read = actual.Read(actualData); - Assert.That(read, Is.EqualTo(expected.Length)); - Assert.That(actualData, Is.EquivalentTo(actualData)); + Assert.That(read, Is.EqualTo(expected.Length), "Read mismatch"); + Assert.That(actualData, Is.EquivalentTo(expected)); }); } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ComplexObject - { - public int IntegerValue { get; set; } - - public long LongValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class NestedObject - { - public int IntegerValue { get; set; } - - public ComplexObject ComplexValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithDefaultBooleanAttribute - { - public int IntegerValue { get; set; } - - [BinaryBoolean] - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithoutBooleanAttribute - { - public int IntegerValue { get; set; } - - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomBooleanAttribute - { - public int IntegerValue { get; set; } - - [BinaryBoolean(WriteAs = typeof(string), TrueValue = "true", FalseValue = "false")] - public bool BooleanValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithDefaultStringAttribute - { - public int IntegerValue { get; set; } - - [BinaryString] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithoutStringAttribute - { - public int IntegerValue { get; set; } - - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeSizeUshort - { - public int IntegerValue { get; set; } - - [BinaryString(SizeType = typeof(ushort), Terminator = "")] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeFixedSize - { - public int IntegerValue { get; set; } - - [BinaryString(FixedSize = 3, Terminator = "")] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeCustomEncoding - { - public int IntegerValue { get; set; } - - [BinaryString(CodePage = 932)] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithCustomStringAttributeUnknownEncoding - { - public int IntegerValue { get; set; } - - [BinaryString(CodePage = 666)] - public string StringValue { get; set; } - - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithForcedEndianness - { - [BinaryForceEndianness(EndiannessMode.LittleEndian)] - public int LittleEndianInteger { get; set; } - - [BinaryForceEndianness(EndiannessMode.BigEndian)] - public int BigEndianInteger { get; set; } - - public int DefaultEndianInteger { get; set; } - } - - private enum Enum1 - { - Value1, - Value2, - Value3, - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithEnum - { - [BinaryEnum(WriteAs = typeof(byte))] - public Enum1 EnumValue { get; set; } - } - - [Yarhl.IO.Serialization.Attributes.Serializable] - private class ObjectWithInt24 - { - [BinaryInt24] - public int Int24Value { get; set; } - } } diff --git a/src/Yarhl/IO/Serialization/Attributes/SerializableAttribute.cs b/src/Yarhl/IO/Serialization/Attributes/SerializableAttribute.cs deleted file mode 100644 index f019ad91..00000000 --- a/src/Yarhl/IO/Serialization/Attributes/SerializableAttribute.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2020 SceneGate - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -namespace Yarhl.IO.Serialization.Attributes -{ - using System; - - /// - /// Set to enable automatic serialization. - /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class SerializableAttribute : Attribute - { - } -} From 8c1afd295c1a2697e9fd0f0816b36f10a80b29d5 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Mon, 29 Jan 2024 16:57:37 +0100 Subject: [PATCH 05/10] :shirt: Refactor serializable boolean attribute --- .../Serialization/BinaryDeserializerTests.cs | 149 +++++++++++++--- .../Serialization/BinarySerializableTypes.cs | 85 +++++++--- .../IO/Serialization/BinarySerializerTests.cs | 159 ++++++++++++++---- .../Attributes/BinaryBooleanAttribute.cs | 17 +- .../IO/Serialization/BinaryDeserializer.cs | 2 +- .../IO/Serialization/BinarySerializer.cs | 2 +- 6 files changed, 315 insertions(+), 99 deletions(-) diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs index 69461834..24ece501 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs @@ -5,24 +5,66 @@ using NUnit.Framework; using Yarhl.IO; using Yarhl.IO.Serialization; -using Yarhl.IO.Serialization.Attributes; [TestFixture] public class BinaryDeserializerTests { [Test] - public void DeserializeFullObject() + public void DeserializeIntegerTypes() + { + byte[] data = { + 0xCE, 0xA9, // char un UTF-8 (default encoding) + 0x84, + 0xF4, + 0x7F, 0x80, + 0xF0, 0xFF, + 0x78, 0x56, 0x34, 0x12, + 0xD6, 0xFF, 0xFF, 0xFF, + 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + }; + var expected = new ClassTypeWithIntegerProperties { + CharValue = 'Ω', + ByteValue = 0x84, + SByteValue = -12, + UShortValue = 0x807F, + ShortValue = -16, + UIntValue = 0x12345678, + IntegerValue = -42, + ULongValue = 0x800000000000002A, + LongValue = -2L, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeDecimalTypes() + { + byte[] data = { + 0xC3, 0xF5, 0x48, 0x40, + 0x1F, 0x85, 0xEB, 0x51, 0xB8, 0x1E, 0x09, 0xC0, + }; + var obj = new ClassTypeWithDecimalProperties { + SingleValue = 3.14f, + DoubleValue = -3.14d, + }; + + AssertDeserialization(data, obj); + } + + [Test] + public void DeserializeMultiPropertyStruct() { byte[] data = { 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, + (byte)'y', (byte)'a', (byte)'r', (byte)'h', (byte)'l', (byte)'\0', }; - var expected = new ComplexObject { + var expected = new MultiPropertyStruct { IntegerValue = 1, LongValue = 2L, - IgnoredIntegerValue = 0, - AnotherIntegerValue = 3, + TextValue = "yarhl", }; AssertDeserialization(data, expected); @@ -34,17 +76,12 @@ public void DeserializeNestedObject() byte[] data = { 0x0A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, }; - var expected = new NestedObject { + var expected = new TypeWithNestedObject { IntegerValue = 10, - ComplexValue = new ComplexObject { - IntegerValue = 1, - LongValue = 2L, - IgnoredIntegerValue = 0, - AnotherIntegerValue = 3, + ComplexValue = new TypeWithNestedObject.NestedType { + NestedValue = 1, }, AnotherIntegerValue = 20, }; @@ -53,7 +90,21 @@ public void DeserializeNestedObject() } [Test] - public void DeserializeBoolean() + public void DeserializeIgnorePropertiesViaAttribute() + { + byte[] data = { + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + var expected = new TypeWithIgnoredProperties { + LongValue = 2L, + IgnoredIntegerValue = 0, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeBooleanType() { byte[] data = { 0x01, 0x00, 0x00, 0x00, @@ -61,33 +112,75 @@ public void DeserializeBoolean() 0x03, 0x00, 0x00, 0x00, }; - var expected = new ObjectWithDefaultBooleanAttribute { - IntegerValue = 1, + var expected = new TypeWithBooleanDefaultAttribute { + BeforeValue = 1, BooleanValue = false, - IgnoredIntegerValue = 0, - AnotherIntegerValue = 3, + AfterValue = 3, }; AssertDeserialization(data, expected); + + data[4] = 0x01; + expected.BooleanValue = true; + AssertDeserialization(data, expected); } [Test] - public void DeserializeBooleanWithCustomTrueValue() + public void DeserializeBooleanWithDefinedValues() { - byte[] data = { + var falseObj = new TypeWithBooleanDefinedValue() { + BeforeValue = 1, + BooleanValue = false, + AfterValue = 3, + }; + byte[] serializedFalse = { 0x01, 0x00, 0x00, 0x00, - 0x74, 0x72, 0x75, 0x65, 0x00, // "true" + 0xD6, 0xFF, 0x03, 0x00, 0x00, 0x00, }; - var expected = new ObjectWithCustomBooleanAttribute { - IntegerValue = 1, + var trueObj = new TypeWithBooleanDefinedValue() { + BeforeValue = 1, BooleanValue = true, - IgnoredIntegerValue = 0, - AnotherIntegerValue = 3, + AfterValue = 3, + }; + byte[] serializedTrue = { + 0x01, 0x00, 0x00, 0x00, + 0x2A, 0x00, + 0x03, 0x00, 0x00, 0x00, }; - AssertDeserialization(data, expected); + AssertDeserialization(serializedFalse, falseObj); + AssertDeserialization(serializedTrue, trueObj); + } + + [Test] + public void DeserializeBooleanWithTextValues() + { + var falseObj = new TypeWithBooleanTextValue() { + BeforeValue = 1, + BooleanValue = false, + AfterValue = 3, + }; + byte[] serializedFalse = { + 0x01, 0x00, 0x00, 0x00, + (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e', (byte)'\0', + 0x03, 0x00, 0x00, 0x00, + }; + + var trueObj = new TypeWithBooleanTextValue() { + BeforeValue = 1, + BooleanValue = true, + AfterValue = 3, + }; + byte[] serializedTrue = { + 0x01, 0x00, 0x00, 0x00, + (byte)'t', (byte)'r', (byte)'u', (byte)'e', (byte)'\0', + 0x03, 0x00, 0x00, 0x00, + }; + + AssertDeserialization(serializedFalse, falseObj); + AssertDeserialization(serializedTrue, trueObj); } [Test] @@ -105,7 +198,7 @@ public void TryDeserializeBooleanWithoutAttributeThrowsException() stream.Position = 0; var deserializer = new BinaryDeserializer(stream); Assert.That( - () => deserializer.Deserialize(), + () => deserializer.Deserialize(), Throws.InstanceOf()); } diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs index a9001756..3bcb9719 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs @@ -7,63 +7,102 @@ // to create a file per test converter. #pragma warning disable SA1649 // File name match type name -public class ComplexObject +public class ClassTypeWithIntegerProperties { + public char CharValue { get; set; } + + public byte ByteValue { get; set; } + + public sbyte SByteValue { get; set; } + + public ushort UShortValue { get; set; } + + public short ShortValue { get; set; } + + public uint UIntValue { get; set; } + public int IntegerValue { get; set; } + public ulong ULongValue { get; set; } + public long LongValue { get; set; } +} - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } +public class ClassTypeWithDecimalProperties +{ + public float SingleValue { get; set; } - public int AnotherIntegerValue { get; set; } + public double DoubleValue { get; set; } } -public class NestedObject +public struct MultiPropertyStruct { public int IntegerValue { get; set; } - public ComplexObject ComplexValue { get; set; } + public long LongValue { get; set; } - public int AnotherIntegerValue { get; set; } + public string TextValue { get; set; } } -public class ObjectWithDefaultBooleanAttribute +public class TypeWithIgnoredProperties { - public int IntegerValue { get; set; } - - [BinaryBoolean] - public bool BooleanValue { get; set; } + public long LongValue { get; set; } [BinaryIgnore] public int IgnoredIntegerValue { get; set; } +} + +public class TypeWithNestedObject +{ + public int IntegerValue { get; set; } + + public NestedType ComplexValue { get; set; } public int AnotherIntegerValue { get; set; } + + public sealed class NestedType + { + public int NestedValue { get; set; } + } } -public class ObjectWithoutBooleanAttribute +public class TypeWithBooleanDefaultAttribute { - public int IntegerValue { get; set; } + public int BeforeValue { get; set; } + [BinaryBoolean] public bool BooleanValue { get; set; } - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } + public int AfterValue { get; set; } +} - public int AnotherIntegerValue { get; set; } +public class TypeWithBooleanWithoutAttribute +{ + public int BeforeValue { get; set; } + + public bool BooleanValue { get; set; } + + public int AfterValue { get; set; } } -public class ObjectWithCustomBooleanAttribute +public class TypeWithBooleanDefinedValue { - public int IntegerValue { get; set; } + public int BeforeValue { get; set; } - [BinaryBoolean(ReadAs = typeof(string), WriteAs = typeof(string), TrueValue = "true", FalseValue = "false")] + [BinaryBoolean(UnderlyingType = typeof(short), TrueValue = (short)42, FalseValue = (short)-42)] public bool BooleanValue { get; set; } - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } + public int AfterValue { get; set; } +} - public int AnotherIntegerValue { get; set; } +public class TypeWithBooleanTextValue +{ + public int BeforeValue { get; set; } + + [BinaryBoolean(UnderlyingType = typeof(string), TrueValue = "true", FalseValue = "false")] + public bool BooleanValue { get; set; } + + public int AfterValue { get; set; } } public class ObjectWithDefaultStringAttribute diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs index 4569b783..f21a1cd9 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -1,29 +1,71 @@ namespace Yarhl.UnitTests.IO.Serialization; using System; -using System.Linq; using NUnit.Framework; using Yarhl.IO; using Yarhl.IO.Serialization; -using Yarhl.IO.Serialization.Attributes; [TestFixture] public class BinarySerializerTests { [Test] - public void SerializeMultipleProperties() + public void SerializeIntegerTypes() { - var obj = new ComplexObject { + var obj = new ClassTypeWithIntegerProperties { + CharValue = 'Ω', + ByteValue = 0x84, + SByteValue = -12, + UShortValue = 0x807F, + ShortValue = -16, + UIntValue = 0x12345678, + IntegerValue = -42, + ULongValue = 0x8000000000002A, + LongValue = -2L, + }; + + byte[] data = { + 0xCE, 0xA9, // char un UTF-8 (default encoding) + 0x84, + 0xF4, + 0x7F, 0x80, + 0xF0, 0xFF, + 0x78, 0x56, 0x34, 0x12, + 0xD6, 0xFF, 0xFF, 0xFF, + 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + }; + + AssertSerialization(obj, data); + } + + [Test] + public void SerializeDecimalTypes() + { + byte[] data = { + 0xC3, 0xF5, 0x48, 0x40, + 0x1F, 0x85, 0xEB, 0x51, 0xB8, 0x1E, 0x09, 0xC0, + }; + var obj = new ClassTypeWithDecimalProperties { + SingleValue = 3.14f, + DoubleValue = -3.14d, + }; + + AssertSerialization(obj, data); + } + + [Test] + public void SerializeStruct() + { + var obj = new MultiPropertyStruct { IntegerValue = 1, LongValue = 2, - IgnoredIntegerValue = 3, - AnotherIntegerValue = 4, + TextValue = "yarhl", }; byte[] expected = { 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, + (byte)'y', (byte)'a', (byte)'r', (byte)'h', (byte)'l', (byte)'\0', }; AssertSerialization(obj, expected); @@ -32,13 +74,10 @@ public void SerializeMultipleProperties() [Test] public void SerializeNestedObject() { - var obj = new NestedObject() { + var obj = new TypeWithNestedObject() { IntegerValue = 10, - ComplexValue = new ComplexObject { - IntegerValue = 1, - LongValue = 2, - IgnoredIntegerValue = 3, - AnotherIntegerValue = 4, + ComplexValue = new TypeWithNestedObject.NestedType { + NestedValue = 1, }, AnotherIntegerValue = 20, }; @@ -46,60 +85,114 @@ public void SerializeNestedObject() byte[] expected = { 0x0A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, }; AssertSerialization(obj, expected); } + [Test] + public void SerializeIgnorePropertiesViaAttribute() + { + var obj = new TypeWithIgnoredProperties { + LongValue = 2, + IgnoredIntegerValue = 42, + }; + + byte[] expected = { + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + [Test] public void SerializeBooleanType() { - var obj = new ObjectWithDefaultBooleanAttribute() { - IntegerValue = 1, + var obj = new TypeWithBooleanDefaultAttribute() { + BeforeValue = 1, BooleanValue = false, - IgnoredIntegerValue = 3, - AnotherIntegerValue = 4, + AfterValue = 3, }; byte[] expected = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, }; AssertSerialization(obj, expected); + + obj.BooleanValue = true; + expected[4] = 0x01; + AssertSerialization(obj, expected); } [Test] - public void SerializeBooleanWithCustomFalseValue() + public void SerializeBooleanWithDefinedValues() { - var obj = new ObjectWithCustomBooleanAttribute() { - IntegerValue = 1, + var falseObj = new TypeWithBooleanDefinedValue() { + BeforeValue = 1, BooleanValue = false, - IgnoredIntegerValue = 5, - AnotherIntegerValue = 4, + AfterValue = 3, + }; + byte[] expectedFalse = { + 0x01, 0x00, 0x00, 0x00, + 0xD6, 0xFF, + 0x03, 0x00, 0x00, 0x00, }; - byte[] expected = { + var trueObj = new TypeWithBooleanDefinedValue() { + BeforeValue = 1, + BooleanValue = true, + AfterValue = 3, + }; + byte[] expectedTrue = { 0x01, 0x00, 0x00, 0x00, - 0x66, 0x61, 0x6C, 0x73, 0x65, 0x00, // "false" - 0x04, 0x00, 0x00, 0x00, + 0x2A, 0x00, + 0x03, 0x00, 0x00, 0x00, }; - AssertSerialization(obj, expected); + AssertSerialization(falseObj, expectedFalse); + AssertSerialization(trueObj, expectedTrue); + } + + [Test] + public void SerializeBooleanWithTextValues() + { + var falseObj = new TypeWithBooleanTextValue() { + BeforeValue = 1, + BooleanValue = false, + AfterValue = 3, + }; + byte[] expectedFalse = { + 0x01, 0x00, 0x00, 0x00, + (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e', (byte)'\0', + 0x03, 0x00, 0x00, 0x00, + }; + + var trueObj = new TypeWithBooleanTextValue() { + BeforeValue = 1, + BooleanValue = true, + AfterValue = 3, + }; + byte[] expectedTrue = { + 0x01, 0x00, 0x00, 0x00, + (byte)'t', (byte)'r', (byte)'u', (byte)'e', (byte)'\0', + 0x03, 0x00, 0x00, 0x00, + }; + + AssertSerialization(falseObj, expectedFalse); + AssertSerialization(trueObj, expectedTrue); } [Test] public void TrySerializeBooleanWithoutAttributeThrowsException() { - var obj = new ObjectWithoutBooleanAttribute() { - IntegerValue = 1, + var obj = new TypeWithBooleanWithoutAttribute() { + BeforeValue = 1, BooleanValue = true, - IgnoredIntegerValue = 3, - AnotherIntegerValue = 4, + AfterValue = 3, }; using var stream = new DataStream(); diff --git a/src/Yarhl/IO/Serialization/Attributes/BinaryBooleanAttribute.cs b/src/Yarhl/IO/Serialization/Attributes/BinaryBooleanAttribute.cs index 1ede4b7c..c03ca802 100644 --- a/src/Yarhl/IO/Serialization/Attributes/BinaryBooleanAttribute.cs +++ b/src/Yarhl/IO/Serialization/Attributes/BinaryBooleanAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2020 SceneGate +// Copyright (c) 2020 SceneGate // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -33,24 +33,15 @@ public sealed class BinaryBooleanAttribute : Attribute /// public BinaryBooleanAttribute() { - ReadAs = typeof(int); - WriteAs = typeof(int); + UnderlyingType = typeof(int); TrueValue = 1; FalseValue = 0; } /// - /// Gets or sets the equivalent type for reading. + /// Gets or sets the underlying type to use to serialize and deserialize. /// - public Type ReadAs { - get; - set; - } - - /// - /// Gets or sets the equivalent type for writing. - /// - public Type WriteAs { + public Type UnderlyingType { get; set; } diff --git a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs index f8e368fc..67a335a4 100644 --- a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs +++ b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs @@ -116,7 +116,7 @@ private object DeserializePrimitiveField(PropertyInfo property) throw new FormatException("Properties of type 'bool' must have the attribute BinaryBoolean"); } - object value = reader.ReadByType(boolAttr!.ReadAs); + object value = reader.ReadByType(boolAttr.UnderlyingType); return value.Equals(boolAttr.TrueValue); } diff --git a/src/Yarhl/IO/Serialization/BinarySerializer.cs b/src/Yarhl/IO/Serialization/BinarySerializer.cs index ae9f46c0..c556ab32 100644 --- a/src/Yarhl/IO/Serialization/BinarySerializer.cs +++ b/src/Yarhl/IO/Serialization/BinarySerializer.cs @@ -120,7 +120,7 @@ private void SerializePrimitiveField(PropertyInfo property, object value) } object typeValue = (bool)value ? boolAttr.TrueValue : boolAttr.FalseValue; - writer.WriteOfType(boolAttr.WriteAs, typeValue); + writer.WriteOfType(boolAttr.UnderlyingType, typeValue); return; } From da5f48e47dd2eb6400f9c66a338e9dff07f87e9c Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Mon, 29 Jan 2024 17:28:37 +0100 Subject: [PATCH 06/10] :shirt: Refactor enum binary attribute --- .editorconfig | 1 + .../Serialization/BinaryDeserializerTests.cs | 32 +++++++++++++++-- .../Serialization/BinarySerializableTypes.cs | 25 +++++++++---- .../IO/Serialization/BinarySerializerTests.cs | 35 ++++++++++++++++--- .../Attributes/BinaryEnumAttribute.cs | 23 +++++------- .../IO/Serialization/BinaryDeserializer.cs | 8 ++--- .../IO/Serialization/BinarySerializer.cs | 8 ++--- 7 files changed, 95 insertions(+), 37 deletions(-) diff --git a/.editorconfig b/.editorconfig index ea53a85b..ad69ddfa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -267,6 +267,7 @@ dotnet_diagnostic.SA0001.severity = none # Disable documentation dotnet_diagnostic.SA1402.severity = none # Multiple types in the same file dotnet_diagnostic.SA1600.severity = none # Disable documentation dotnet_diagnostic.SA1601.severity = none # Disable documentation +dotnet_diagnostic.SA1602.severity = none # Disable documentation dotnet_diagnostic.SA1201.severity = none # Allow enums inside classes dotnet_diagnostic.S1144.severity = none # Remove unused setter dotnet_diagnostic.S2094.severity = none # Remove empty class diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs index 24ece501..14393f1f 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs @@ -350,11 +350,37 @@ public void DeserializeObjectWithSpecificEndianness() } [Test] - public void DeserializeEnum() + public void DeserializeEnumNoAttribute() { - byte[] data = { 0x01 }; + byte[] data = { 0x2A, 0x00, }; - var expected = new ObjectWithEnum { EnumValue = Enum1.Value2 }; + var expected = new TypeWithEnumNoAttribute { + EnumValue = SerializableEnum.Value42, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeEnumDefaultAttribute() + { + byte[] data = { 0x2A, 0x00, }; + + var expected = new TypeWithEnumDefaultAttribute { + EnumValue = SerializableEnum.Value42, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeEnumOverwritingType() + { + byte[] data = { 0x01, 0x00, 0x00, 0x00 }; + + var expected = new TypeWithEnumWithOverwrittenType { + EnumValue = SerializableEnum.None, + }; AssertDeserialization(data, expected); } diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs index 3bcb9719..bd33e9e3 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs @@ -193,10 +193,21 @@ public class ObjectWithForcedEndianness public int DefaultEndianInteger { get; set; } } -public class ObjectWithEnum +public class TypeWithEnumNoAttribute { - [BinaryEnum(ReadAs = typeof(byte), WriteAs = typeof(short))] - public Enum1 EnumValue { get; set; } + public SerializableEnum EnumValue { get; set; } +} + +public class TypeWithEnumDefaultAttribute +{ + [BinaryEnum] + public SerializableEnum EnumValue { get; set; } +} + +public class TypeWithEnumWithOverwrittenType +{ + [BinaryEnum(UnderlyingType = typeof(uint))] + public SerializableEnum EnumValue { get; set; } } public class ObjectWithInt24 @@ -210,9 +221,9 @@ public class ObjectWithNullable public int? NullValue { get; set; } } -public enum Enum1 +[System.Diagnostics.CodeAnalysis.SuppressMessage("", "S2344", Justification = "Test type")] +public enum SerializableEnum : short { - Value1, - Value2, - Value3, + None = 1, + Value42 = 42, } diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs index f21a1cd9..5d577b8c 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -344,16 +344,41 @@ public void SerializeObjectWithSpecificEndianness() AssertSerialization(obj, expected); } + [Test] - public void SerializeEnum() + public void SerializeEnumNoAttribute() { - var obj = new ObjectWithEnum() { - EnumValue = Enum1.Value2, + byte[] data = { 0x2A, 0x00, }; + + var obj = new TypeWithEnumNoAttribute { + EnumValue = SerializableEnum.Value42, }; - byte[] expected = { 0x01, 0x00 }; + AssertSerialization(obj, data); + } - AssertSerialization(obj, expected); + [Test] + public void SerializeEnumDefaultAttribute() + { + byte[] data = { 0x2A, 0x00, }; + + var obj = new TypeWithEnumDefaultAttribute { + EnumValue = SerializableEnum.Value42, + }; + + AssertSerialization(obj, data); + } + + [Test] + public void SerializeEnumOverwritingType() + { + byte[] data = { 0x01, 0x00, 0x00, 0x00 }; + + var obj = new TypeWithEnumWithOverwrittenType { + EnumValue = SerializableEnum.None, + }; + + AssertSerialization(obj, data); } private static void AssertSerialization(T obj, byte[] expected) diff --git a/src/Yarhl/IO/Serialization/Attributes/BinaryEnumAttribute.cs b/src/Yarhl/IO/Serialization/Attributes/BinaryEnumAttribute.cs index b22ec75d..8c197936 100644 --- a/src/Yarhl/IO/Serialization/Attributes/BinaryEnumAttribute.cs +++ b/src/Yarhl/IO/Serialization/Attributes/BinaryEnumAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2020 SceneGate +// Copyright (c) 2020 SceneGate // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ namespace Yarhl.IO.Serialization.Attributes /// /// Define how to read and write a Enum value. - /// Default type is + /// Default type is defined in the enum type /// [AttributeUsage(AttributeTargets.Property)] public sealed class BinaryEnumAttribute : Attribute @@ -33,22 +33,17 @@ public sealed class BinaryEnumAttribute : Attribute /// public BinaryEnumAttribute() { - ReadAs = typeof(int); - WriteAs = typeof(int); + UnderlyingType = null; } /// - /// Gets or sets the equivalent type for reading. + /// Gets or sets the underlying type to use to serialize and deserialize. /// - public Type ReadAs { - get; - set; - } - - /// - /// Gets or sets the equivalent type for writing. - /// - public Type WriteAs { + /// + /// If set to null (default), it will use the defined underlying type + /// in the enumaration type. + /// + public Type? UnderlyingType { get; set; } diff --git a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs index 67a335a4..c13b9cf3 100644 --- a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs +++ b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs @@ -129,11 +129,11 @@ private object DeserializePrimitiveField(PropertyInfo property) private object DeserializeEnumField(PropertyInfo property) { - if (property.GetCustomAttribute() is not { } enumAttr) { - throw new FormatException("Missing BinaryEnum attribute in property"); - } + var enumAttr = property.GetCustomAttribute(); + Type underlyingType = enumAttr?.UnderlyingType + ?? Enum.GetUnderlyingType(property.PropertyType); - object value = reader.ReadByType(enumAttr!.ReadAs); + object value = reader.ReadByType(underlyingType); return Enum.ToObject(property.PropertyType, value); } diff --git a/src/Yarhl/IO/Serialization/BinarySerializer.cs b/src/Yarhl/IO/Serialization/BinarySerializer.cs index c556ab32..1836c418 100644 --- a/src/Yarhl/IO/Serialization/BinarySerializer.cs +++ b/src/Yarhl/IO/Serialization/BinarySerializer.cs @@ -99,11 +99,11 @@ private void SerializeProperty(PropertyInfo property, object obj) if (property.PropertyType.IsPrimitive) { SerializePrimitiveField(property, value); } else if (property.PropertyType.IsEnum) { - if (property.GetCustomAttribute() is not { } enumAttr) { - throw new FormatException("Missing BinaryEnum attribute in property"); - } + var enumAttr = property.GetCustomAttribute(); + Type underlyingType = enumAttr?.UnderlyingType + ?? Enum.GetUnderlyingType(property.PropertyType); - writer.WriteOfType(enumAttr.WriteAs, value); + writer.WriteOfType(underlyingType, value); } else if (property.PropertyType == typeof(string)) { SerializeString(property, value); } else { From 178479356adda1d9dcfad9c35d64174476f9481d Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Mon, 29 Jan 2024 20:27:49 +0100 Subject: [PATCH 07/10] :shirt: Refactor and add binary serializer tests and support inherited types --- .../Serialization/BinaryDeserializerTests.cs | 116 ++++++++++--- .../Serialization/BinarySerializableTypes.cs | 161 +++++++++--------- .../IO/Serialization/BinarySerializerTests.cs | 122 +++++++++---- .../IO/Serialization/BinaryDeserializer.cs | 1 - .../IO/Serialization/BinarySerializer.cs | 9 +- 5 files changed, 267 insertions(+), 142 deletions(-) diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs index 14393f1f..b97f7cda 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs @@ -9,6 +9,73 @@ [TestFixture] public class BinaryDeserializerTests { + [Test] + public void DeserializeByGenericType() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var expected = new SimpleType { Value = 10 }; + using var stream = new DataStream(); + stream.Write(data); + + stream.Position = 0; + var deserializer = new BinaryDeserializer(stream); + SimpleType obj = deserializer.Deserialize(); + + _ = obj.Should().BeEquivalentTo(expected); + } + + [Test] + public void DeserializeByTypeArg() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var expected = new SimpleType { Value = 10 }; + using var stream = new DataStream(); + stream.Write(data); + + stream.Position = 0; + var deserializer = new BinaryDeserializer(stream); + object obj = deserializer.Deserialize(typeof(SimpleType)); + + _ = obj.Should().BeEquivalentTo(expected); + } + + [Test] + public void DeserializeStaticByGenericType() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var expected = new SimpleType { Value = 10 }; + using var stream = new DataStream(); + stream.Write(data); + + stream.Position = 0; + SimpleType obj = BinaryDeserializer.Deserialize(stream); + + _ = obj.Should().BeEquivalentTo(expected); + } + + [Test] + public void DeserializeStaticByTypeArg() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var expected = new SimpleType { Value = 10 }; + using var stream = new DataStream(); + stream.Write(data); + + stream.Position = 0; + object obj = BinaryDeserializer.Deserialize(stream, typeof(SimpleType)); + + _ = obj.Should().BeEquivalentTo(expected); + } + + [Test] + public void DeserializeIncludesInheritedFields() + { + byte[] data = { 0xFE, 0xCA, 0x0A, 0x00, 0x00, 0x00 }; + var obj = new InheritedType { Value = 0x0A, NewValue = 0xCAFE }; + + AssertDeserialization(data, obj); + } + [Test] public void DeserializeIntegerTypes() { @@ -23,7 +90,7 @@ public void DeserializeIntegerTypes() 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }; - var expected = new ClassTypeWithIntegerProperties { + var expected = new TypeWithIntegers { CharValue = 'Ω', ByteValue = 0x84, SByteValue = -12, @@ -45,7 +112,7 @@ public void DeserializeDecimalTypes() 0xC3, 0xF5, 0x48, 0x40, 0x1F, 0x85, 0xEB, 0x51, 0xB8, 0x1E, 0x09, 0xC0, }; - var obj = new ClassTypeWithDecimalProperties { + var obj = new TypeWithDecimals { SingleValue = 3.14f, DoubleValue = -3.14d, }; @@ -209,7 +276,7 @@ public void DeserializeInt24() 0x01, 0x00, 0x00, }; - var expected = new ObjectWithInt24 { + var expected = new TypeWithInt24 { Int24Value = 1, }; @@ -225,11 +292,10 @@ public void DeserializeStringWithoutAttributeUsesDefaultReaderSettings() 0x03, 0x00, 0x00, 0x00, }; - var expected = new ObjectWithoutStringAttribute { - IntegerValue = 1, + var expected = new TypeWithStringWithoutAttribute { + BeforeValue = 1, StringValue = "あア", - IgnoredIntegerValue = 0, - AnotherIntegerValue = 3, + AfterValue = 3, }; AssertDeserialization(data, expected); @@ -244,11 +310,10 @@ public void DeserializeStringWithDefaultAttributeUsesDefaultReaderSettings() 0x03, 0x00, 0x00, 0x00, }; - var expected = new ObjectWithDefaultStringAttribute { - IntegerValue = 1, + var expected = new TypeWithStringDefaultAttribute { + BeforeValue = 1, StringValue = "あア", - IgnoredIntegerValue = 0, - AnotherIntegerValue = 3, + AfterValue = 3, }; AssertDeserialization(data, expected); @@ -263,11 +328,10 @@ public void DeserializeStringWithSizeType() 0x04, 0x00, 0x00, 0x00, }; - var expected = new ObjectWithCustomStringAttributeSizeUshort { - IntegerValue = 1, + var expected = new TypeWithStringVariableSize { + BeforeValue = 1, StringValue = "あ", - IgnoredIntegerValue = 0, - AnotherIntegerValue = 4, + AfterValue = 4, }; AssertDeserialization(data, expected); @@ -282,11 +346,10 @@ public void DeserializeStringWithFixedSize() 0x03, 0x00, 0x00, 0x00, }; - var expected = new ObjectWithCustomStringAttributeFixedSize { - IntegerValue = 1, + var expected = new TypeWithStringFixedSize { + BeforeValue = 1, StringValue = "あ", - IgnoredIntegerValue = 0, - AnotherIntegerValue = 3, + AfterValue = 3, }; AssertDeserialization(data, expected); @@ -301,11 +364,10 @@ public void DeserializeStringWithDifferentEncoding() 0x03, 0x00, 0x00, 0x00, }; - var expected = new ObjectWithCustomStringAttributeCustomEncoding { - IntegerValue = 1, + var expected = new TypeWithStringDefinedEncoding { + BeforeValue = 1, StringValue = "あア", - IgnoredIntegerValue = 0, - AnotherIntegerValue = 3, + AfterValue = 3, }; AssertDeserialization(data, expected); @@ -327,7 +389,7 @@ public void TryDeserializeStringWithUnknownEncodingThrowsException() var deserializer = new BinaryDeserializer(stream); Assert.That( - () => deserializer.Deserialize(), + () => deserializer.Deserialize(), Throws.InstanceOf()); } @@ -340,7 +402,7 @@ public void DeserializeObjectWithSpecificEndianness() 0x03, 0x00, 0x00, 0x00, }; - var expected = new ObjectWithForcedEndianness { + var expected = new TypeWithEndiannessChanges { LittleEndianInteger = 1, BigEndianInteger = 2, DefaultEndianInteger = 3, @@ -395,13 +457,13 @@ public void TryDeserializeNullableThrowsException() var deserializer = new BinaryDeserializer(stream); Assert.That( - () => deserializer.Deserialize(), + () => deserializer.Deserialize(), Throws.InstanceOf()); } private static void AssertDeserialization(byte[] data, T expected) { - var stream = new DataStream(); + using var stream = new DataStream(); stream.Write(data); stream.Position = 0; diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs index bd33e9e3..5098dd0f 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs @@ -6,33 +6,16 @@ // Disable file may only contain a single class since we aren't going // to create a file per test converter. #pragma warning disable SA1649 // File name match type name +#pragma warning disable SA1124 // do not use regions - I would agree but too many types -public class ClassTypeWithIntegerProperties +public class SimpleType { - public char CharValue { get; set; } - - public byte ByteValue { get; set; } - - public sbyte SByteValue { get; set; } - - public ushort UShortValue { get; set; } - - public short ShortValue { get; set; } - - public uint UIntValue { get; set; } - - public int IntegerValue { get; set; } - - public ulong ULongValue { get; set; } - - public long LongValue { get; set; } + public int Value { get; set; } } -public class ClassTypeWithDecimalProperties +public class InheritedType : SimpleType { - public float SingleValue { get; set; } - - public double DoubleValue { get; set; } + public ushort NewValue { get; set; } } public struct MultiPropertyStruct @@ -66,6 +49,59 @@ public sealed class NestedType } } +public class TypeWithEndiannessChanges +{ + [BinaryForceEndianness(EndiannessMode.LittleEndian)] + public int LittleEndianInteger { get; set; } + + [BinaryForceEndianness(EndiannessMode.BigEndian)] + public int BigEndianInteger { get; set; } + + public int DefaultEndianInteger { get; set; } +} + +public class TypeWithNullable +{ + public int? NullValue { get; set; } +} + +#region Integer types +public class TypeWithIntegers +{ + public char CharValue { get; set; } + + public byte ByteValue { get; set; } + + public sbyte SByteValue { get; set; } + + public ushort UShortValue { get; set; } + + public short ShortValue { get; set; } + + public uint UIntValue { get; set; } + + public int IntegerValue { get; set; } + + public ulong ULongValue { get; set; } + + public long LongValue { get; set; } +} + +public class TypeWithDecimals +{ + public float SingleValue { get; set; } + + public double DoubleValue { get; set; } +} + +public class TypeWithInt24 +{ + [BinaryInt24] + public int Int24Value { get; set; } +} +#endregion + +#region Boolean types public class TypeWithBooleanDefaultAttribute { public int BeforeValue { get; set; } @@ -104,95 +140,70 @@ public class TypeWithBooleanTextValue public int AfterValue { get; set; } } +#endregion -public class ObjectWithDefaultStringAttribute +#region String types +public class TypeWithStringDefaultAttribute { - public int IntegerValue { get; set; } + public int BeforeValue { get; set; } [BinaryString] public string StringValue { get; set; } - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } + public int AfterValue { get; set; } } -public class ObjectWithoutStringAttribute +public class TypeWithStringWithoutAttribute { - public int IntegerValue { get; set; } + public int BeforeValue { get; set; } public string StringValue { get; set; } - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } + public int AfterValue { get; set; } } -public class ObjectWithCustomStringAttributeSizeUshort +public class TypeWithStringVariableSize { - public int IntegerValue { get; set; } + public int BeforeValue { get; set; } [BinaryString(SizeType = typeof(ushort), Terminator = "")] public string StringValue { get; set; } - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } + public int AfterValue { get; set; } } -public class ObjectWithCustomStringAttributeFixedSize +public class TypeWithStringFixedSize { - public int IntegerValue { get; set; } + public int BeforeValue { get; set; } [BinaryString(FixedSize = 3, Terminator = "")] public string StringValue { get; set; } - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } + public int AfterValue { get; set; } } -public class ObjectWithCustomStringAttributeCustomEncoding +public class TypeWithStringDefinedEncoding { - public int IntegerValue { get; set; } + public int BeforeValue { get; set; } [BinaryString(CodePage = 932)] public string StringValue { get; set; } - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } + public int AfterValue { get; set; } } -public class ObjectWithCustomStringAttributeUnknownEncoding +public class TypeWithStringInvalidEncoding { - public int IntegerValue { get; set; } + public int BeforeValue { get; set; } [BinaryString(CodePage = 666)] public string StringValue { get; set; } - [BinaryIgnore] - public int IgnoredIntegerValue { get; set; } - - public int AnotherIntegerValue { get; set; } -} - -public class ObjectWithForcedEndianness -{ - [BinaryForceEndianness(EndiannessMode.LittleEndian)] - public int LittleEndianInteger { get; set; } - - [BinaryForceEndianness(EndiannessMode.BigEndian)] - public int BigEndianInteger { get; set; } - - public int DefaultEndianInteger { get; set; } + public int AfterValue { get; set; } } +#endregion +#region Enum types public class TypeWithEnumNoAttribute { public SerializableEnum EnumValue { get; set; } @@ -210,20 +221,10 @@ public class TypeWithEnumWithOverwrittenType public SerializableEnum EnumValue { get; set; } } -public class ObjectWithInt24 -{ - [BinaryInt24] - public int Int24Value { get; set; } -} - -public class ObjectWithNullable -{ - public int? NullValue { get; set; } -} - [System.Diagnostics.CodeAnalysis.SuppressMessage("", "S2344", Justification = "Test type")] public enum SerializableEnum : short { None = 1, Value42 = 42, } +#endregion diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs index 5d577b8c..86e4cb36 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -8,10 +8,81 @@ [TestFixture] public class BinarySerializerTests { + [Test] + public void SerializeByGenericType() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var obj = new SimpleType { Value = 0x0A, }; + + using var stream = new DataStream(); + var serializer = new BinarySerializer(stream); + serializer.Serialize(obj); + + AssertBinary(stream, data); + } + + [Test] + public void SerializeByTypeArg() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var obj = new SimpleType { Value = 0x0A, }; + + using var stream = new DataStream(); + var serializer = new BinarySerializer(stream); + serializer.Serialize(typeof(SimpleType), obj); + + AssertBinary(stream, data); + } + + [Test] + public void SerializeByStaticGenericType() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var obj = new SimpleType { Value = 0x0A, }; + + using var stream = new DataStream(); + BinarySerializer.Serialize(stream, obj); + + AssertBinary(stream, data); + } + + [Test] + public void SerializeByStaticTypeArg() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var obj = new SimpleType { Value = 0x0A, }; + + using var stream = new DataStream(); + BinarySerializer.Serialize(stream, typeof(SimpleType), obj); + + AssertBinary(stream, data); + } + + [Test] + public void SerializeIncludesInheritedFields() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00, 0xFE, 0xCA }; + var obj = new InheritedType { Value = 0x0A, NewValue = 0xCAFE }; + + AssertSerialization(obj, data); + } + + [Test] + public void SerializeBaseType() + { + byte[] data = { 0x0A, 0x00, 0x00, 0x00 }; + var obj = new InheritedType { Value = 0x0A, NewValue = 0xCAFE }; + + using var stream = new DataStream(); + BinarySerializer.Serialize(stream, typeof(SimpleType), obj); + + AssertBinary(stream, data); + } + [Test] public void SerializeIntegerTypes() { - var obj = new ClassTypeWithIntegerProperties { + var obj = new TypeWithIntegers { CharValue = 'Ω', ByteValue = 0x84, SByteValue = -12, @@ -45,7 +116,7 @@ public void SerializeDecimalTypes() 0xC3, 0xF5, 0x48, 0x40, 0x1F, 0x85, 0xEB, 0x51, 0xB8, 0x1E, 0x09, 0xC0, }; - var obj = new ClassTypeWithDecimalProperties { + var obj = new TypeWithDecimals { SingleValue = 3.14f, DoubleValue = -3.14d, }; @@ -204,7 +275,7 @@ public void TrySerializeBooleanWithoutAttributeThrowsException() [Test] public void SerializeInt24() { - var obj = new ObjectWithInt24 { + var obj = new TypeWithInt24 { Int24Value = 0x7F_FC0FFE, }; @@ -218,11 +289,10 @@ public void SerializeInt24() [Test] public void SerializeStringWithoutAttributeUsesDefaultWriterSettings() { - var obj = new ObjectWithoutStringAttribute { - IntegerValue = 1, + var obj = new TypeWithStringWithoutAttribute { + BeforeValue = 1, StringValue = "あア", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 3, + AfterValue = 3, }; byte[] expected = { @@ -237,11 +307,10 @@ public void SerializeStringWithoutAttributeUsesDefaultWriterSettings() [Test] public void SerializeStringWithDefaultAttributeUsesDefaultWriterSettings() { - var obj = new ObjectWithDefaultStringAttribute() { - IntegerValue = 1, + var obj = new TypeWithStringDefaultAttribute() { + BeforeValue = 1, StringValue = "あア", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 3, + AfterValue = 3, }; byte[] expected = { @@ -256,11 +325,10 @@ public void SerializeStringWithDefaultAttributeUsesDefaultWriterSettings() [Test] public void SerializeStringWithSizeType() { - var obj = new ObjectWithCustomStringAttributeSizeUshort() { - IntegerValue = 1, + var obj = new TypeWithStringVariableSize() { + BeforeValue = 1, StringValue = "あ", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 4, + AfterValue = 4, }; byte[] expected = { @@ -275,11 +343,10 @@ public void SerializeStringWithSizeType() [Test] public void SerializeStringWithFixedSize() { - var obj = new ObjectWithCustomStringAttributeFixedSize() { - IntegerValue = 1, + var obj = new TypeWithStringFixedSize() { + BeforeValue = 1, StringValue = "あ", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 4, + AfterValue = 4, }; byte[] expected = { @@ -294,11 +361,10 @@ public void SerializeStringWithFixedSize() [Test] public void SerializeStringWithDifferentEncoding() { - var obj = new ObjectWithCustomStringAttributeCustomEncoding() { - IntegerValue = 1, + var obj = new TypeWithStringDefinedEncoding() { + BeforeValue = 1, StringValue = "あア", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 4, + AfterValue = 4, }; byte[] expected = { @@ -313,11 +379,10 @@ public void SerializeStringWithDifferentEncoding() [Test] public void TrySerializeStringWithUnknownEncodingThrowsException() { - var obj = new ObjectWithCustomStringAttributeUnknownEncoding() { - IntegerValue = 1, + var obj = new TypeWithStringInvalidEncoding() { + BeforeValue = 1, StringValue = "あア", - IgnoredIntegerValue = 2, - AnotherIntegerValue = 4, + AfterValue = 4, }; using var stream = new DataStream(); @@ -329,7 +394,7 @@ public void TrySerializeStringWithUnknownEncodingThrowsException() [Test] public void SerializeObjectWithSpecificEndianness() { - var obj = new ObjectWithForcedEndianness() { + var obj = new TypeWithEndiannessChanges() { LittleEndianInteger = 1, BigEndianInteger = 2, DefaultEndianInteger = 3, @@ -344,7 +409,6 @@ public void SerializeObjectWithSpecificEndianness() AssertSerialization(obj, expected); } - [Test] public void SerializeEnumNoAttribute() { diff --git a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs index c13b9cf3..619e044f 100644 --- a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs +++ b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs @@ -73,7 +73,6 @@ public object Deserialize(Type objType) object obj = Activator.CreateInstance(objType)!; PropertyInfo[] properties = objType.GetProperties( - BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance); diff --git a/src/Yarhl/IO/Serialization/BinarySerializer.cs b/src/Yarhl/IO/Serialization/BinarySerializer.cs index 1836c418..c6f96f33 100644 --- a/src/Yarhl/IO/Serialization/BinarySerializer.cs +++ b/src/Yarhl/IO/Serialization/BinarySerializer.cs @@ -31,10 +31,10 @@ public BinarySerializer(Stream stream) /// /// Serialize the public properties of the object in binary data in the stream. /// - /// The object to serialize into the stream. /// The stream to write the binary data. + /// The object to serialize into the stream. /// The type of the object. - public static void Serialize(T obj, Stream stream) + public static void Serialize(Stream stream, T obj) { new BinarySerializer(stream).Serialize(obj); } @@ -42,10 +42,10 @@ public static void Serialize(T obj, Stream stream) /// /// Serialize the public properties of the object in binary data in the stream. /// + /// The stream to write the binary data. /// The type of object to serialize. /// The object to serialize into the stream. - /// The stream to write the binary data. - public static void Serialize(Type objType, object obj, Stream stream) + public static void Serialize(Stream stream, Type objType, object obj) { new BinarySerializer(stream).Serialize(objType, obj); } @@ -70,7 +70,6 @@ public void Serialize(T obj) public void Serialize(Type type, object obj) { PropertyInfo[] properties = type.GetProperties( - BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance); From b3e801025759c814d6d47ec72d7e33ac7e9038b3 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Mon, 29 Jan 2024 20:30:59 +0100 Subject: [PATCH 08/10] :shirt: Rename binary endianness attribute --- .../IO/Serialization/BinarySerializableTypes.cs | 4 ++-- ...annessAttribute.cs => BinaryEndiannessAttribute.cs} | 10 +++++----- src/Yarhl/IO/Serialization/BinaryDeserializer.cs | 2 +- src/Yarhl/IO/Serialization/BinarySerializer.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/Yarhl/IO/Serialization/Attributes/{BinaryForceEndiannessAttribute.cs => BinaryEndiannessAttribute.cs} (82%) diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs index 5098dd0f..963cebee 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs @@ -51,10 +51,10 @@ public sealed class NestedType public class TypeWithEndiannessChanges { - [BinaryForceEndianness(EndiannessMode.LittleEndian)] + [BinaryEndianness(EndiannessMode.LittleEndian)] public int LittleEndianInteger { get; set; } - [BinaryForceEndianness(EndiannessMode.BigEndian)] + [BinaryEndianness(EndiannessMode.BigEndian)] public int BigEndianInteger { get; set; } public int DefaultEndianInteger { get; set; } diff --git a/src/Yarhl/IO/Serialization/Attributes/BinaryForceEndiannessAttribute.cs b/src/Yarhl/IO/Serialization/Attributes/BinaryEndiannessAttribute.cs similarity index 82% rename from src/Yarhl/IO/Serialization/Attributes/BinaryForceEndiannessAttribute.cs rename to src/Yarhl/IO/Serialization/Attributes/BinaryEndiannessAttribute.cs index be9f2c7e..a1986677 100644 --- a/src/Yarhl/IO/Serialization/Attributes/BinaryForceEndiannessAttribute.cs +++ b/src/Yarhl/IO/Serialization/Attributes/BinaryEndiannessAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2020 SceneGate +// Copyright (c) 2020 SceneGate // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,16 +22,16 @@ namespace Yarhl.IO.Serialization.Attributes using System; /// - /// Set to force the endianness in automatic serialization. + /// Specify the endianness to serialize or deserialize a field. /// [AttributeUsage(AttributeTargets.Property)] - public sealed class BinaryForceEndiannessAttribute : Attribute + public sealed class BinaryEndiannessAttribute : Attribute { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Endianness mode for the property. - public BinaryForceEndiannessAttribute(EndiannessMode mode) + public BinaryEndiannessAttribute(EndiannessMode mode) { Mode = mode; } diff --git a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs index 619e044f..4f707f3e 100644 --- a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs +++ b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs @@ -92,7 +92,7 @@ public object Deserialize(Type objType) private object DeserializePropertyValue(PropertyInfo property) { reader.Endianness = DefaultEndianness; - var endiannessAttr = property.GetCustomAttribute(); + var endiannessAttr = property.GetCustomAttribute(); if (endiannessAttr is not null) { reader.Endianness = endiannessAttr.Mode; } diff --git a/src/Yarhl/IO/Serialization/BinarySerializer.cs b/src/Yarhl/IO/Serialization/BinarySerializer.cs index c6f96f33..72ecb65e 100644 --- a/src/Yarhl/IO/Serialization/BinarySerializer.cs +++ b/src/Yarhl/IO/Serialization/BinarySerializer.cs @@ -87,7 +87,7 @@ public void Serialize(Type type, object obj) private void SerializeProperty(PropertyInfo property, object obj) { writer.Endianness = DefaultEndianness; - var endiannessAttr = property.GetCustomAttribute(); + var endiannessAttr = property.GetCustomAttribute(); if (endiannessAttr is not null) { writer.Endianness = endiannessAttr.Mode; } From 9da7e72fa2fcf23b8c48e1af604f40dcf2bfdce5 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Tue, 30 Jan 2024 09:58:20 +0100 Subject: [PATCH 09/10] :sparkles: Add attribute to specify property order --- .../Attributes/BinaryFieldOrderAttribute.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs diff --git a/src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs b/src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs new file mode 100644 index 00000000..a9081cd3 --- /dev/null +++ b/src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs @@ -0,0 +1,29 @@ +namespace Yarhl.IO.Serialization.Attributes; + +using System; + +/// +/// Specify the order to serialize or deserialize the fields in binary format. +/// +[AttributeUsage(AttributeTargets.Property)] +public class BinaryFieldOrderAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The order of the field in the binary serialization. + /// The order is less than 0. + public BinaryFieldOrderAttribute(int order) + { + if (order < 0) { + throw new ArgumentOutOfRangeException(nameof(order)); + } + + Order = order; + } + + /// + /// Gets or sets the order of the field in the binary format. + /// + public int Order { get; set; } +} From 794cc7638e92d03fafe833e1654665508cef2d64 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Tue, 30 Jan 2024 11:24:57 +0100 Subject: [PATCH 10/10] :sparkles: Implement type navigator for binary serialization with ordering --- .../Serialization/BinaryDeserializerTests.cs | 4 +- .../Serialization/BinarySerializableTypes.cs | 59 ++++ .../IO/Serialization/BinarySerializerTests.cs | 2 +- .../DefaultTypePropertyNavigatorTests.cs | 253 ++++++++++++++++++ ...erAttribute.cs => BinaryOrderAttribute.cs} | 11 +- .../IO/Serialization/BinaryDeserializer.cs | 80 +++--- .../IO/Serialization/BinarySerializer.cs | 79 +++--- .../DefaultTypePropertyNavigator.cs | 62 +++++ src/Yarhl/IO/Serialization/FieldInfo.cs | 35 +++ .../IO/Serialization/ITypeFieldNavigator.cs | 18 ++ 10 files changed, 522 insertions(+), 81 deletions(-) create mode 100644 src/Yarhl.UnitTests/IO/Serialization/DefaultTypePropertyNavigatorTests.cs rename src/Yarhl/IO/Serialization/Attributes/{BinaryFieldOrderAttribute.cs => BinaryOrderAttribute.cs} (57%) create mode 100644 src/Yarhl/IO/Serialization/DefaultTypePropertyNavigator.cs create mode 100644 src/Yarhl/IO/Serialization/FieldInfo.cs create mode 100644 src/Yarhl/IO/Serialization/ITypeFieldNavigator.cs diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs index b97f7cda..ad8bf8ab 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs @@ -18,7 +18,7 @@ public void DeserializeByGenericType() stream.Write(data); stream.Position = 0; - var deserializer = new BinaryDeserializer(stream); + var deserializer = new BinaryDeserializer(stream, new DefaultTypePropertyNavigator()); SimpleType obj = deserializer.Deserialize(); _ = obj.Should().BeEquivalentTo(expected); @@ -70,7 +70,7 @@ public void DeserializeStaticByTypeArg() [Test] public void DeserializeIncludesInheritedFields() { - byte[] data = { 0xFE, 0xCA, 0x0A, 0x00, 0x00, 0x00 }; + byte[] data = { 0x0A, 0x00, 0x00, 0x00, 0xFE, 0xCA }; var obj = new InheritedType { Value = 0x0A, NewValue = 0xCAFE }; AssertDeserialization(data, obj); diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs index 963cebee..6b67857b 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs @@ -10,25 +10,31 @@ public class SimpleType { + [BinaryOrder(0)] public int Value { get; set; } } public class InheritedType : SimpleType { + [BinaryOrder(1)] public ushort NewValue { get; set; } } public struct MultiPropertyStruct { + [BinaryOrder(0)] public int IntegerValue { get; set; } + [BinaryOrder(1)] public long LongValue { get; set; } + [BinaryOrder(2)] public string TextValue { get; set; } } public class TypeWithIgnoredProperties { + [BinaryOrder(0)] public long LongValue { get; set; } [BinaryIgnore] @@ -37,65 +43,85 @@ public class TypeWithIgnoredProperties public class TypeWithNestedObject { + [BinaryOrder(0)] public int IntegerValue { get; set; } + [BinaryOrder(1)] public NestedType ComplexValue { get; set; } + [BinaryOrder(2)] public int AnotherIntegerValue { get; set; } public sealed class NestedType { + [BinaryOrder(0)] public int NestedValue { get; set; } } } public class TypeWithEndiannessChanges { + [BinaryOrder(0)] [BinaryEndianness(EndiannessMode.LittleEndian)] public int LittleEndianInteger { get; set; } + [BinaryOrder(1)] [BinaryEndianness(EndiannessMode.BigEndian)] public int BigEndianInteger { get; set; } + [BinaryOrder(2)] public int DefaultEndianInteger { get; set; } } public class TypeWithNullable { + [BinaryOrder(0)] public int? NullValue { get; set; } } #region Integer types public class TypeWithIntegers { + [BinaryOrder(0)] public char CharValue { get; set; } + [BinaryOrder(1)] public byte ByteValue { get; set; } + [BinaryOrder(2)] public sbyte SByteValue { get; set; } + [BinaryOrder(3)] public ushort UShortValue { get; set; } + [BinaryOrder(4)] public short ShortValue { get; set; } + [BinaryOrder(5)] public uint UIntValue { get; set; } + [BinaryOrder(6)] public int IntegerValue { get; set; } + [BinaryOrder(7)] public ulong ULongValue { get; set; } + [BinaryOrder(8)] public long LongValue { get; set; } } public class TypeWithDecimals { + [BinaryOrder(0)] public float SingleValue { get; set; } + [BinaryOrder(1)] public double DoubleValue { get; set; } } public class TypeWithInt24 { + [BinaryOrder(0)] [BinaryInt24] public int Int24Value { get; set; } } @@ -104,40 +130,52 @@ public class TypeWithInt24 #region Boolean types public class TypeWithBooleanDefaultAttribute { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryBoolean] public bool BooleanValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithBooleanWithoutAttribute { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] public bool BooleanValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithBooleanDefinedValue { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryBoolean(UnderlyingType = typeof(short), TrueValue = (short)42, FalseValue = (short)-42)] public bool BooleanValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithBooleanTextValue { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryBoolean(UnderlyingType = typeof(string), TrueValue = "true", FalseValue = "false")] public bool BooleanValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } #endregion @@ -145,60 +183,78 @@ public class TypeWithBooleanTextValue #region String types public class TypeWithStringDefaultAttribute { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringWithoutAttribute { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringVariableSize { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString(SizeType = typeof(ushort), Terminator = "")] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringFixedSize { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString(FixedSize = 3, Terminator = "")] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringDefinedEncoding { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString(CodePage = 932)] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringInvalidEncoding { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString(CodePage = 666)] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } #endregion @@ -206,17 +262,20 @@ public class TypeWithStringInvalidEncoding #region Enum types public class TypeWithEnumNoAttribute { + [BinaryOrder(0)] public SerializableEnum EnumValue { get; set; } } public class TypeWithEnumDefaultAttribute { + [BinaryOrder(0)] [BinaryEnum] public SerializableEnum EnumValue { get; set; } } public class TypeWithEnumWithOverwrittenType { + [BinaryOrder(0)] [BinaryEnum(UnderlyingType = typeof(uint))] public SerializableEnum EnumValue { get; set; } } diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs index 86e4cb36..a20ce233 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -15,7 +15,7 @@ public void SerializeByGenericType() var obj = new SimpleType { Value = 0x0A, }; using var stream = new DataStream(); - var serializer = new BinarySerializer(stream); + var serializer = new BinarySerializer(stream, new DefaultTypePropertyNavigator()); serializer.Serialize(obj); AssertBinary(stream, data); diff --git a/src/Yarhl.UnitTests/IO/Serialization/DefaultTypePropertyNavigatorTests.cs b/src/Yarhl.UnitTests/IO/Serialization/DefaultTypePropertyNavigatorTests.cs new file mode 100644 index 00000000..ead428b3 --- /dev/null +++ b/src/Yarhl.UnitTests/IO/Serialization/DefaultTypePropertyNavigatorTests.cs @@ -0,0 +1,253 @@ +namespace Yarhl.UnitTests.IO.Serialization; + +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using NUnit.Framework; +using Yarhl.IO.Serialization; +using Yarhl.IO.Serialization.Attributes; + +[TestFixture] +public class DefaultTypePropertyNavigatorTests +{ + [Test] + public void PropertiesReturnedInOrderViaAttributes() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(SimpleType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(2)); + Assert.Multiple(() => { + Assert.That(fields[0].Name, Is.EqualTo(nameof(SimpleType.Prop2))); + Assert.That(fields[1].Name, Is.EqualTo(nameof(SimpleType.Prop1))); + }); + } + + [Test] + public void IgnorePrivateProperties() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(IgnorePrivatePropertiesType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(1)); + Assert.That(fields[0].Name, Is.EqualTo(nameof(IgnorePrivatePropertiesType.Prop0))); + } + + [Test] + public void IgnoreStaticProperties() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(IgnoreStaticPropertiesType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(1)); + Assert.That(fields[0].Name, Is.EqualTo(nameof(IgnoreStaticPropertiesType.Prop1))); + } + + [Test] + public void IgnorePropertiesWithIgnoreAttribute() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(IgnorePropertiesWithIgnoreAttributeType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(2)); + Assert.Multiple(() => { + Assert.That(fields[0].Name, Is.EqualTo(nameof(IgnorePropertiesWithIgnoreAttributeType.Prop0))); + Assert.That(fields[1].Name, Is.EqualTo(nameof(IgnorePropertiesWithIgnoreAttributeType.Prop2))); + }); + } + + [Test] + public void IgnorePropertiesWithoutPublicGetterOrSetter() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(IgnorePropertiesWithoutPublicGetterOrSetterType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(1)); + Assert.That(fields[0].Name, Is.EqualTo(nameof(IgnorePropertiesWithoutPublicGetterOrSetterType.ValidProp))); + } + + [Test] + public void PropertyOrderWithInheritance() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(InheritedType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(5)); + Assert.Multiple(() => { + Assert.That(fields[0].Name, Is.EqualTo(nameof(InheritedType.Prop0))); + Assert.That(fields[1].Name, Is.EqualTo(nameof(InheritedType.PropBase0))); + Assert.That(fields[2].Name, Is.EqualTo(nameof(InheritedType.Prop1))); + Assert.That(fields[3].Name, Is.EqualTo(nameof(InheritedType.PropBase1))); + Assert.That(fields[4].Name, Is.EqualTo(nameof(InheritedType.Prop2))); + }); + } + + [Test] + public void TypeWithoutPropertyOrderAttributeThrowsInNet60() + { + if (!RuntimeInformation.FrameworkDescription.StartsWith(".NET 6")) { + Assert.Ignore("Test for another platform"); + return; + } + + var navigator = new DefaultTypePropertyNavigator(); + + Assert.That( + () => navigator.IterateFields(typeof(PropertiesWithoutOrderAttributeType)).ToArray(), + Throws.Exception.InstanceOf()); + } + + [Test] + public void TypeWithoutPropertyOrderAttributeWorksInNet80() + { + if (RuntimeInformation.FrameworkDescription.StartsWith(".NET 6")) { + Assert.Ignore("Test for another platform"); + return; + } + + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(PropertiesWithoutOrderAttributeType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(2)); + Assert.Multiple(() => { + Assert.That(fields[0].Name, Is.EqualTo(nameof(PropertiesWithoutOrderAttributeType.Prop0))); + Assert.That(fields[1].Name, Is.EqualTo(nameof(PropertiesWithoutOrderAttributeType.Prop1))); + }); + } + + [Test] + public void TypeWithSomePropertyMissingOrderAttributeThrows() + { + var navigator = new DefaultTypePropertyNavigator(); + + Assert.That( + () => navigator.IterateFields(typeof(SomePropertiesWithoutOrderAttributeType)).ToArray(), + Throws.Exception.InstanceOf()); + } + +#pragma warning disable S3459 // unused properties + + private sealed class SimpleType + { + [BinaryOrder(10)] + public int Prop1 { get; set; } + + [BinaryOrder(-5)] + public int Prop2 { get; set; } + } + + private sealed class IgnorePrivatePropertiesType + { + [BinaryOrder(0)] + public int Prop0 { get; set; } + + [BinaryOrder(1)] + private int Prop1 { get; set; } + } + + private sealed class IgnoreStaticPropertiesType + { + [BinaryOrder(-1)] + public static int Prop0 { get; set; } + + [BinaryOrder(0)] + public int Prop1 { get; set; } + } + + private sealed class IgnorePropertiesWithIgnoreAttributeType + { + [BinaryOrder(-1)] + public int Prop0 { get; set; } + + [BinaryOrder(0)] + [BinaryIgnore] + public int Prop1 { get; set; } + + [BinaryOrder(1)] + public int Prop2 { get; set; } + } + + private class IgnorePropertiesWithoutPublicGetterOrSetterType + { + private int val; + + [BinaryOrder(-1)] + public int Prop0 { private get; set; } + + [BinaryOrder(0)] + public int ValidProp { get; set; } + + [BinaryOrder(1)] + public int Prop2 { get; private set; } + + [BinaryOrder(2)] + public int Prop3 { internal get; set; } + + [BinaryOrder(3)] + public int Prop4 { get; protected set; } + + [BinaryOrder(4)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("", "S2376", Justification = "Test")] + public int Prop5 { + set => val = value; + } + + [BinaryOrder(5)] + public int Prop6 { + get => val; + } + + [BinaryOrder(6)] + internal int Prop7 { get; set; } + + [BinaryOrder(7)] + protected int Prop8 { get; set; } + + [BinaryOrder(8)] + private int Prop9 { get; set; } + } + + private class BaseType + { + [BinaryOrder(5)] + public int PropBase0 { get; set; } + + [BinaryOrder(10)] + public int PropBase1 { get; set; } + } + + private sealed class InheritedType : BaseType + { + [BinaryOrder(4)] + public int Prop0 { get; set; } + + [BinaryOrder(8)] + public int Prop1 { get; set; } + + [BinaryOrder(15)] + public int Prop2 { get; set; } + } + + private sealed class PropertiesWithoutOrderAttributeType + { + public int Prop0 { get; set; } + + public int Prop1 { get; set; } + } + + private sealed class SomePropertiesWithoutOrderAttributeType + { + [BinaryOrder(0)] + public int Prop0 { get; set; } + + public int Prop1 { get; set; } + } +} diff --git a/src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs b/src/Yarhl/IO/Serialization/Attributes/BinaryOrderAttribute.cs similarity index 57% rename from src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs rename to src/Yarhl/IO/Serialization/Attributes/BinaryOrderAttribute.cs index a9081cd3..67b38508 100644 --- a/src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs +++ b/src/Yarhl/IO/Serialization/Attributes/BinaryOrderAttribute.cs @@ -6,19 +6,14 @@ /// Specify the order to serialize or deserialize the fields in binary format. /// [AttributeUsage(AttributeTargets.Property)] -public class BinaryFieldOrderAttribute : Attribute +public class BinaryOrderAttribute : Attribute { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The order of the field in the binary serialization. - /// The order is less than 0. - public BinaryFieldOrderAttribute(int order) + public BinaryOrderAttribute(int order) { - if (order < 0) { - throw new ArgumentOutOfRangeException(nameof(order)); - } - Order = order; } diff --git a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs index 4f707f3e..e51ce1a0 100644 --- a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs +++ b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs @@ -2,7 +2,6 @@ using System; using System.IO; -using System.Reflection; using System.Text; using Yarhl.IO.Serialization.Attributes; @@ -13,6 +12,7 @@ public class BinaryDeserializer { private readonly DataReader reader; + private readonly ITypeFieldNavigator fieldNavigator; /// /// Initializes a new instance of the class. @@ -20,7 +20,24 @@ public class BinaryDeserializer /// The stream to read from. public BinaryDeserializer(Stream stream) { + ArgumentNullException.ThrowIfNull(stream); + reader = new DataReader(stream); + fieldNavigator = new DefaultTypePropertyNavigator(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The stream to read from. + /// The strategy to iterate the field's type. + public BinaryDeserializer(Stream stream, ITypeFieldNavigator fieldNavigator) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(fieldNavigator); + + reader = new DataReader(stream); + this.fieldNavigator = fieldNavigator; } /// @@ -67,51 +84,40 @@ public T Deserialize() /// A new object deserialized. public object Deserialize(Type objType) { - // It returns null for Nullable, but as that is a class and - // it won't have the serializable attribute, it will throw an - // unsupported exception before. So this can't be null at this point. - object obj = Activator.CreateInstance(objType)!; - - PropertyInfo[] properties = objType.GetProperties( - BindingFlags.Public | - BindingFlags.Instance); - - foreach (PropertyInfo property in properties) { - bool ignore = Attribute.IsDefined(property, typeof(BinaryIgnoreAttribute)); - if (ignore) { - continue; - } + object obj = Activator.CreateInstance(objType) + ?? throw new FormatException("Nullable types are not supported"); - object propertyValue = DeserializePropertyValue(property); - property.SetValue(obj, propertyValue); + foreach (FieldInfo fieldInfo in fieldNavigator.IterateFields(objType)) { + object propertyValue = DeserializePropertyValue(fieldInfo); + fieldInfo.SetValueFunc(obj, propertyValue); } return obj; } - private object DeserializePropertyValue(PropertyInfo property) + private object DeserializePropertyValue(FieldInfo fieldInfo) { reader.Endianness = DefaultEndianness; - var endiannessAttr = property.GetCustomAttribute(); + var endiannessAttr = fieldInfo.GetAttribute(); if (endiannessAttr is not null) { reader.Endianness = endiannessAttr.Mode; } - if (property.PropertyType.IsPrimitive) { - return DeserializePrimitiveField(property); - } else if (property.PropertyType.IsEnum) { - return DeserializeEnumField(property); - } else if (property.PropertyType == typeof(string)) { - return DeserializeStringField(property); + if (fieldInfo.Type.IsPrimitive) { + return DeserializePrimitiveField(fieldInfo); + } else if (fieldInfo.Type.IsEnum) { + return DeserializeEnumField(fieldInfo); + } else if (fieldInfo.Type == typeof(string)) { + return DeserializeStringField(fieldInfo); } else { - return Deserialize(property.PropertyType); + return Deserialize(fieldInfo.Type); } } - private object DeserializePrimitiveField(PropertyInfo property) + private object DeserializePrimitiveField(FieldInfo fieldInfo) { - if (property.PropertyType == typeof(bool)) { - if (property.GetCustomAttribute() is not { } boolAttr) { + if (fieldInfo.Type == typeof(bool)) { + if (fieldInfo.GetAttribute() is not { } boolAttr) { throw new FormatException("Properties of type 'bool' must have the attribute BinaryBoolean"); } @@ -119,26 +125,26 @@ private object DeserializePrimitiveField(PropertyInfo property) return value.Equals(boolAttr.TrueValue); } - if (property.PropertyType == typeof(int) && Attribute.IsDefined(property, typeof(BinaryInt24Attribute))) { + if (fieldInfo.Type == typeof(int) && fieldInfo.GetAttribute() is not null) { return reader.ReadInt24(); } - return reader.ReadByType(property.PropertyType); + return reader.ReadByType(fieldInfo.Type); } - private object DeserializeEnumField(PropertyInfo property) + private object DeserializeEnumField(FieldInfo fieldInfo) { - var enumAttr = property.GetCustomAttribute(); + var enumAttr = fieldInfo.GetAttribute(); Type underlyingType = enumAttr?.UnderlyingType - ?? Enum.GetUnderlyingType(property.PropertyType); + ?? Enum.GetUnderlyingType(fieldInfo.Type); object value = reader.ReadByType(underlyingType); - return Enum.ToObject(property.PropertyType, value); + return Enum.ToObject(fieldInfo.Type, value); } - private string DeserializeStringField(PropertyInfo property) + private string DeserializeStringField(FieldInfo fieldInfo) { - if (property.GetCustomAttribute() is not { } stringAttr) { + if (fieldInfo.GetAttribute() is not { } stringAttr) { // Use default settings if not specified. return reader.ReadString(); } diff --git a/src/Yarhl/IO/Serialization/BinarySerializer.cs b/src/Yarhl/IO/Serialization/BinarySerializer.cs index 72ecb65e..0fc35361 100644 --- a/src/Yarhl/IO/Serialization/BinarySerializer.cs +++ b/src/Yarhl/IO/Serialization/BinarySerializer.cs @@ -2,7 +2,7 @@ using System; using System.IO; -using System.Reflection; +using System.Linq; using System.Text; using Yarhl.IO.Serialization.Attributes; @@ -12,6 +12,7 @@ /// public class BinarySerializer { + private readonly ITypeFieldNavigator fieldNavigator; private readonly DataWriter writer; /// @@ -20,7 +21,24 @@ public class BinarySerializer /// The stream to write the binary data. public BinarySerializer(Stream stream) { + ArgumentNullException.ThrowIfNull(stream); + writer = new DataWriter(stream); + fieldNavigator = new DefaultTypePropertyNavigator(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The stream to write the binary data. + /// Strategy to iterate over type fields. + public BinarySerializer(Stream stream, ITypeFieldNavigator fieldNavigator) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(fieldNavigator); + + writer = new DataWriter(stream); + this.fieldNavigator = fieldNavigator; } /// @@ -69,52 +87,38 @@ public void Serialize(T obj) /// The object to serialize into the stream. public void Serialize(Type type, object obj) { - PropertyInfo[] properties = type.GetProperties( - BindingFlags.Public | - BindingFlags.Instance); - - // TODO: Introduce property to sort - foreach (PropertyInfo property in properties) { - bool ignore = Attribute.IsDefined(property, typeof(BinaryIgnoreAttribute)); - if (ignore) { - continue; - } - + foreach (FieldInfo property in fieldNavigator.IterateFields(type)) { SerializeProperty(property, obj); } } - private void SerializeProperty(PropertyInfo property, object obj) + private void SerializeProperty(FieldInfo fieldInfo, object obj) { writer.Endianness = DefaultEndianness; - var endiannessAttr = property.GetCustomAttribute(); + var endiannessAttr = fieldInfo.GetAttribute(); if (endiannessAttr is not null) { writer.Endianness = endiannessAttr.Mode; } - object value = property.GetValue(obj) + object value = fieldInfo.GetValueFunc(obj) ?? throw new FormatException("Cannot serialize nullable values"); - if (property.PropertyType.IsPrimitive) { - SerializePrimitiveField(property, value); - } else if (property.PropertyType.IsEnum) { - var enumAttr = property.GetCustomAttribute(); - Type underlyingType = enumAttr?.UnderlyingType - ?? Enum.GetUnderlyingType(property.PropertyType); - - writer.WriteOfType(underlyingType, value); - } else if (property.PropertyType == typeof(string)) { - SerializeString(property, value); + if (fieldInfo.Type.IsPrimitive) { + SerializePrimitiveField(fieldInfo, value); + } else if (fieldInfo.Type.IsEnum) { + SerializeEnumField(fieldInfo, value); + } else if (fieldInfo.Type == typeof(string)) { + SerializeString(fieldInfo, value); } else { - Serialize(property.PropertyType, value); + Serialize(fieldInfo.Type, value); } } - private void SerializePrimitiveField(PropertyInfo property, object value) + private void SerializePrimitiveField(FieldInfo fieldInfo, object value) { // Handle first the special cases - if (property.PropertyType == typeof(bool)) { - if (property.GetCustomAttribute() is not { } boolAttr) { + if (fieldInfo.Type == typeof(bool)) { + if (fieldInfo.GetAttribute() is not { } boolAttr) { throw new FormatException("Properties of type 'bool' must have the attribute BinaryBoolean"); } @@ -123,18 +127,27 @@ private void SerializePrimitiveField(PropertyInfo property, object value) return; } - if (property.PropertyType == typeof(int) && Attribute.IsDefined(property, typeof(BinaryInt24Attribute))) { + if (fieldInfo.Type == typeof(int) && fieldInfo.Attributes.Any(a => a is BinaryInt24Attribute)) { writer.WriteInt24((int)value); return; } // Fallback to DataWriter primitive write - writer.WriteOfType(property.PropertyType, value); + writer.WriteOfType(fieldInfo.Type, value); + } + + private void SerializeEnumField(FieldInfo fieldInfo, object value) + { + var enumAttr = fieldInfo.GetAttribute(); + Type underlyingType = enumAttr?.UnderlyingType + ?? Enum.GetUnderlyingType(fieldInfo.Type); + + writer.WriteOfType(underlyingType, value); } - private void SerializeString(PropertyInfo property, object value) + private void SerializeString(FieldInfo fieldInfo, object value) { - if (property.GetCustomAttribute() is not { } stringAttr) { + if (fieldInfo.GetAttribute() is not { } stringAttr) { // Use default settings if not specified. writer.Write((string)value); return; diff --git a/src/Yarhl/IO/Serialization/DefaultTypePropertyNavigator.cs b/src/Yarhl/IO/Serialization/DefaultTypePropertyNavigator.cs new file mode 100644 index 00000000..0f681e01 --- /dev/null +++ b/src/Yarhl/IO/Serialization/DefaultTypePropertyNavigator.cs @@ -0,0 +1,62 @@ +namespace Yarhl.IO.Serialization; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Yarhl.IO.Serialization.Attributes; + +/// +/// Field navigator for types that iterate over public non-static properties only. +/// It includes inherited properties. It follows the order given by the order attribute. +/// +public class DefaultTypePropertyNavigator : ITypeFieldNavigator +{ + /// + public virtual IEnumerable IterateFields(Type type) + { + PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && (p.GetGetMethod(false)?.IsPublic ?? false)) + .Where(p => p.CanWrite && (p.GetSetMethod(false)?.IsPublic ?? false)) + .Where(p => p.GetCustomAttribute() is null) + .ToArray(); + + SortProperties(properties); + + foreach (PropertyInfo property in properties) { + var info = new FieldInfo( + property.Name, + property.PropertyType, + property.GetValue, + property.SetValue, + property.GetCustomAttributes()); + + yield return info; + } + } + + private static void SortProperties(PropertyInfo[] properties) + { + int[] orderKeys = properties + .Select(p => p.GetCustomAttribute()) + .Where(p => p is not null) + .Select(p => p!.Order) + .ToArray(); + +#if NET6_0 + if (orderKeys.Length != properties.Length) { + throw new FormatException("Prior .NET 8.0, every property must have the BinaryFieldOrder attribute"); + } + + Array.Sort(orderKeys, properties); +#elif NET8_0_OR_GREATER + if (orderKeys.Length > 0 && orderKeys.Length != properties.Length) { + throw new FormatException("BinaryFieldOrder must be applied to none or all properties"); + } + + if (orderKeys.Length > 0) { + Array.Sort(orderKeys, properties); + } +#endif + } +} diff --git a/src/Yarhl/IO/Serialization/FieldInfo.cs b/src/Yarhl/IO/Serialization/FieldInfo.cs new file mode 100644 index 00000000..10c50478 --- /dev/null +++ b/src/Yarhl/IO/Serialization/FieldInfo.cs @@ -0,0 +1,35 @@ +namespace Yarhl.IO.Serialization; + +using System; +using System.Collections.Generic; +using System.Linq; + +/// +/// Information of a field member of a type. +/// +/// Name of the field. +/// Type of the field. +/// Function that returns the fields' value given the object. +/// +/// Function that sets the fields'value on the given object. +/// The first argument is the object and the second the value to set. +/// +/// Optional collection of attributes on the field. +public record FieldInfo( + string Name, + Type Type, + Func GetValueFunc, + Action SetValueFunc, + IEnumerable Attributes) +{ + /// + /// Returns the first attribute if any of the given type. + /// + /// The attribute type to search. + /// The attribute on the given type if any or null otherwise. + public T? GetAttribute() + where T : Attribute + { + return Attributes.OfType().FirstOrDefault(); + } +} diff --git a/src/Yarhl/IO/Serialization/ITypeFieldNavigator.cs b/src/Yarhl/IO/Serialization/ITypeFieldNavigator.cs new file mode 100644 index 00000000..1e7769c1 --- /dev/null +++ b/src/Yarhl/IO/Serialization/ITypeFieldNavigator.cs @@ -0,0 +1,18 @@ +namespace Yarhl.IO.Serialization; + +using System; +using System.Collections.Generic; +using System.Reflection; + +/// +/// Interface to provide implementations that iterate over fields of types. +/// +public interface ITypeFieldNavigator +{ + /// + /// Iterate over the fields of a given type using reflection with enumerables. + /// + /// The type to iterate over fields. + /// Enumerable to iterate over its fields. + IEnumerable IterateFields(Type type); +}