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/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 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/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/BinaryDeserializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs new file mode 100644 index 00000000..ad8bf8ab --- /dev/null +++ b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs @@ -0,0 +1,475 @@ +namespace Yarhl.UnitTests.IO.Serialization; + +using System; +using FluentAssertions; +using NUnit.Framework; +using Yarhl.IO; +using Yarhl.IO.Serialization; + +[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, new DefaultTypePropertyNavigator()); + 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 = { 0x0A, 0x00, 0x00, 0x00, 0xFE, 0xCA }; + var obj = new InheritedType { Value = 0x0A, NewValue = 0xCAFE }; + + AssertDeserialization(data, obj); + } + + [Test] + 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 TypeWithIntegers { + 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 TypeWithDecimals { + 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, + (byte)'y', (byte)'a', (byte)'r', (byte)'h', (byte)'l', (byte)'\0', + }; + var expected = new MultiPropertyStruct { + IntegerValue = 1, + LongValue = 2L, + TextValue = "yarhl", + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeNestedObject() + { + byte[] data = { + 0x0A, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, + }; + var expected = new TypeWithNestedObject { + IntegerValue = 10, + ComplexValue = new TypeWithNestedObject.NestedType { + NestedValue = 1, + }, + AnotherIntegerValue = 20, + }; + + AssertDeserialization(data, expected); + } + + [Test] + 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, + 0x00, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new TypeWithBooleanDefaultAttribute { + BeforeValue = 1, + BooleanValue = false, + AfterValue = 3, + }; + + AssertDeserialization(data, expected); + + data[4] = 0x01; + expected.BooleanValue = true; + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeBooleanWithDefinedValues() + { + var falseObj = new TypeWithBooleanDefinedValue() { + BeforeValue = 1, + BooleanValue = false, + AfterValue = 3, + }; + byte[] serializedFalse = { + 0x01, 0x00, 0x00, 0x00, + 0xD6, 0xFF, + 0x03, 0x00, 0x00, 0x00, + }; + + var trueObj = new TypeWithBooleanDefinedValue() { + BeforeValue = 1, + BooleanValue = true, + AfterValue = 3, + }; + byte[] serializedTrue = { + 0x01, 0x00, 0x00, 0x00, + 0x2A, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + 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] + 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 TypeWithInt24 { + 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 TypeWithStringWithoutAttribute { + BeforeValue = 1, + StringValue = "あア", + AfterValue = 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 TypeWithStringDefaultAttribute { + BeforeValue = 1, + StringValue = "あア", + AfterValue = 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 TypeWithStringVariableSize { + BeforeValue = 1, + StringValue = "あ", + AfterValue = 4, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeStringWithFixedSize() + { + byte[] data = { + 0x01, 0x00, 0x00, 0x00, + 0xE3, 0x81, 0x82, + 0x03, 0x00, 0x00, 0x00, + }; + + var expected = new TypeWithStringFixedSize { + BeforeValue = 1, + StringValue = "あ", + AfterValue = 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 TypeWithStringDefinedEncoding { + BeforeValue = 1, + StringValue = "あア", + AfterValue = 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 TypeWithEndiannessChanges { + LittleEndianInteger = 1, + BigEndianInteger = 2, + DefaultEndianInteger = 3, + }; + + AssertDeserialization(data, expected); + } + + [Test] + public void DeserializeEnumNoAttribute() + { + byte[] data = { 0x2A, 0x00, }; + + 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); + } + + [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) + { + using var stream = new DataStream(); + stream.Write(data); + + stream.Position = 0; + var deserializer = new BinaryDeserializer(stream); + T obj = deserializer.Deserialize(); + + _ = obj.Should().BeEquivalentTo(expected); + } +} diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs new file mode 100644 index 00000000..6b67857b --- /dev/null +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs @@ -0,0 +1,289 @@ +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 +#pragma warning disable SA1124 // do not use regions - I would agree but too many types + +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] + public int IgnoredIntegerValue { get; set; } +} + +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; } +} +#endregion + +#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 + +#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 + +#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; } +} + +[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 new file mode 100644 index 00000000..a20ce233 --- /dev/null +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -0,0 +1,471 @@ +namespace Yarhl.UnitTests.IO.Serialization; + +using System; +using NUnit.Framework; +using Yarhl.IO; +using Yarhl.IO.Serialization; + +[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, new DefaultTypePropertyNavigator()); + 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 TypeWithIntegers { + 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 TypeWithDecimals { + SingleValue = 3.14f, + DoubleValue = -3.14d, + }; + + AssertSerialization(obj, data); + } + + [Test] + public void SerializeStruct() + { + var obj = new MultiPropertyStruct { + IntegerValue = 1, + LongValue = 2, + TextValue = "yarhl", + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + (byte)'y', (byte)'a', (byte)'r', (byte)'h', (byte)'l', (byte)'\0', + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeNestedObject() + { + var obj = new TypeWithNestedObject() { + IntegerValue = 10, + ComplexValue = new TypeWithNestedObject.NestedType { + NestedValue = 1, + }, + AnotherIntegerValue = 20, + }; + + byte[] expected = { + 0x0A, 0x00, 0x00, 0x00, + 0x01, 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 TypeWithBooleanDefaultAttribute() { + BeforeValue = 1, + BooleanValue = false, + AfterValue = 3, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + + obj.BooleanValue = true; + expected[4] = 0x01; + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeBooleanWithDefinedValues() + { + var falseObj = new TypeWithBooleanDefinedValue() { + BeforeValue = 1, + BooleanValue = false, + AfterValue = 3, + }; + byte[] expectedFalse = { + 0x01, 0x00, 0x00, 0x00, + 0xD6, 0xFF, + 0x03, 0x00, 0x00, 0x00, + }; + + var trueObj = new TypeWithBooleanDefinedValue() { + BeforeValue = 1, + BooleanValue = true, + AfterValue = 3, + }; + byte[] expectedTrue = { + 0x01, 0x00, 0x00, 0x00, + 0x2A, 0x00, + 0x03, 0x00, 0x00, 0x00, + }; + + 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 TypeWithBooleanWithoutAttribute() { + BeforeValue = 1, + BooleanValue = true, + AfterValue = 3, + }; + + using var stream = new DataStream(); + var serializer = new BinarySerializer(stream); + + _ = Assert.Throws(() => serializer.Serialize(obj)); + } + + [Test] + public void SerializeInt24() + { + var obj = new TypeWithInt24 { + Int24Value = 0x7F_FC0FFE, + }; + + byte[] expected = { + 0xFE, 0x0F, 0xFC, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeStringWithoutAttributeUsesDefaultWriterSettings() + { + var obj = new TypeWithStringWithoutAttribute { + BeforeValue = 1, + StringValue = "あア", + AfterValue = 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 TypeWithStringDefaultAttribute() { + BeforeValue = 1, + StringValue = "あア", + AfterValue = 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 TypeWithStringVariableSize() { + BeforeValue = 1, + StringValue = "あ", + AfterValue = 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 TypeWithStringFixedSize() { + BeforeValue = 1, + StringValue = "あ", + AfterValue = 4, + }; + + byte[] expected = { + 0x01, 0x00, 0x00, 0x00, + 0xE3, 0x81, 0x82, + 0x04, 0x00, 0x00, 0x00, + }; + + AssertSerialization(obj, expected); + } + + [Test] + public void SerializeStringWithDifferentEncoding() + { + var obj = new TypeWithStringDefinedEncoding() { + BeforeValue = 1, + StringValue = "あア", + AfterValue = 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 TypeWithStringInvalidEncoding() { + BeforeValue = 1, + StringValue = "あア", + AfterValue = 4, + }; + + using var stream = new DataStream(); + var serializer = new BinarySerializer(stream); + + _ = Assert.Throws(() => serializer.Serialize(obj)); + } + + [Test] + public void SerializeObjectWithSpecificEndianness() + { + var obj = new TypeWithEndiannessChanges() { + 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 SerializeEnumNoAttribute() + { + byte[] data = { 0x2A, 0x00, }; + + var obj = new TypeWithEnumNoAttribute { + EnumValue = SerializableEnum.Value42, + }; + + AssertSerialization(obj, data); + } + + [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) + { + 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(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), "Read mismatch"); + Assert.That(actualData, Is.EquivalentTo(expected)); + }); + } +} 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/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/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/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/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/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/Attributes/BinaryOrderAttribute.cs b/src/Yarhl/IO/Serialization/Attributes/BinaryOrderAttribute.cs new file mode 100644 index 00000000..67b38508 --- /dev/null +++ b/src/Yarhl/IO/Serialization/Attributes/BinaryOrderAttribute.cs @@ -0,0 +1,24 @@ +namespace Yarhl.IO.Serialization.Attributes; + +using System; + +/// +/// Specify the order to serialize or deserialize the fields in binary format. +/// +[AttributeUsage(AttributeTargets.Property)] +public class BinaryOrderAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The order of the field in the binary serialization. + public BinaryOrderAttribute(int order) + { + Order = order; + } + + /// + /// Gets or sets the order of the field in the binary format. + /// + public int Order { 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 - { - } -} diff --git a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs new file mode 100644 index 00000000..e51ce1a0 --- /dev/null +++ b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs @@ -0,0 +1,165 @@ +namespace Yarhl.IO.Serialization; + +using System; +using System.IO; +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; + private readonly ITypeFieldNavigator fieldNavigator; + + /// + /// Initializes a new instance of the class. + /// + /// 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; + } + + /// + /// 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) + { + object obj = Activator.CreateInstance(objType) + ?? throw new FormatException("Nullable types are not supported"); + + foreach (FieldInfo fieldInfo in fieldNavigator.IterateFields(objType)) { + object propertyValue = DeserializePropertyValue(fieldInfo); + fieldInfo.SetValueFunc(obj, propertyValue); + } + + return obj; + } + + private object DeserializePropertyValue(FieldInfo fieldInfo) + { + reader.Endianness = DefaultEndianness; + var endiannessAttr = fieldInfo.GetAttribute(); + if (endiannessAttr is not null) { + reader.Endianness = endiannessAttr.Mode; + } + + 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(fieldInfo.Type); + } + } + + private object DeserializePrimitiveField(FieldInfo fieldInfo) + { + if (fieldInfo.Type == typeof(bool)) { + if (fieldInfo.GetAttribute() is not { } boolAttr) { + throw new FormatException("Properties of type 'bool' must have the attribute BinaryBoolean"); + } + + object value = reader.ReadByType(boolAttr.UnderlyingType); + return value.Equals(boolAttr.TrueValue); + } + + if (fieldInfo.Type == typeof(int) && fieldInfo.GetAttribute() is not null) { + return reader.ReadInt24(); + } + + return reader.ReadByType(fieldInfo.Type); + } + + private object DeserializeEnumField(FieldInfo fieldInfo) + { + var enumAttr = fieldInfo.GetAttribute(); + Type underlyingType = enumAttr?.UnderlyingType + ?? Enum.GetUnderlyingType(fieldInfo.Type); + + object value = reader.ReadByType(underlyingType); + return Enum.ToObject(fieldInfo.Type, value); + } + + private string DeserializeStringField(FieldInfo fieldInfo) + { + if (fieldInfo.GetAttribute() 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); + } +} diff --git a/src/Yarhl/IO/Serialization/BinarySerializer.cs b/src/Yarhl/IO/Serialization/BinarySerializer.cs new file mode 100644 index 00000000..0fc35361 --- /dev/null +++ b/src/Yarhl/IO/Serialization/BinarySerializer.cs @@ -0,0 +1,173 @@ +namespace Yarhl.IO.Serialization; + +using System; +using System.IO; +using System.Linq; +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 ITypeFieldNavigator fieldNavigator; + private readonly DataWriter writer; + + /// + /// Initializes a new instance of the class. + /// + /// 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; + } + + /// + /// 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 stream to write the binary data. + /// The object to serialize into the stream. + /// The type of the object. + public static void Serialize(Stream stream, T obj) + { + new BinarySerializer(stream).Serialize(obj); + } + + /// + /// 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. + public static void Serialize(Stream stream, Type objType, object obj) + { + 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) + { + foreach (FieldInfo property in fieldNavigator.IterateFields(type)) { + SerializeProperty(property, obj); + } + } + + private void SerializeProperty(FieldInfo fieldInfo, object obj) + { + writer.Endianness = DefaultEndianness; + var endiannessAttr = fieldInfo.GetAttribute(); + if (endiannessAttr is not null) { + writer.Endianness = endiannessAttr.Mode; + } + + object value = fieldInfo.GetValueFunc(obj) + ?? throw new FormatException("Cannot serialize nullable values"); + + 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(fieldInfo.Type, value); + } + } + + private void SerializePrimitiveField(FieldInfo fieldInfo, object value) + { + // Handle first the special cases + if (fieldInfo.Type == typeof(bool)) { + if (fieldInfo.GetAttribute() 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.UnderlyingType, typeValue); + return; + } + + if (fieldInfo.Type == typeof(int) && fieldInfo.Attributes.Any(a => a is BinaryInt24Attribute)) { + writer.WriteInt24((int)value); + return; + } + + // Fallback to DataWriter primitive write + 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(FieldInfo fieldInfo, object value) + { + if (fieldInfo.GetAttribute() 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); + } + } +} 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); +}