diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs index b97f7cd..ad8bf8a 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinaryDeserializerTests.cs @@ -18,7 +18,7 @@ public void DeserializeByGenericType() stream.Write(data); stream.Position = 0; - var deserializer = new BinaryDeserializer(stream); + var deserializer = new BinaryDeserializer(stream, new DefaultTypePropertyNavigator()); SimpleType obj = deserializer.Deserialize(); _ = obj.Should().BeEquivalentTo(expected); @@ -70,7 +70,7 @@ public void DeserializeStaticByTypeArg() [Test] public void DeserializeIncludesInheritedFields() { - byte[] data = { 0xFE, 0xCA, 0x0A, 0x00, 0x00, 0x00 }; + byte[] data = { 0x0A, 0x00, 0x00, 0x00, 0xFE, 0xCA }; var obj = new InheritedType { Value = 0x0A, NewValue = 0xCAFE }; AssertDeserialization(data, obj); diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs index 963cebe..6b67857 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializableTypes.cs @@ -10,25 +10,31 @@ public class SimpleType { + [BinaryOrder(0)] public int Value { get; set; } } public class InheritedType : SimpleType { + [BinaryOrder(1)] public ushort NewValue { get; set; } } public struct MultiPropertyStruct { + [BinaryOrder(0)] public int IntegerValue { get; set; } + [BinaryOrder(1)] public long LongValue { get; set; } + [BinaryOrder(2)] public string TextValue { get; set; } } public class TypeWithIgnoredProperties { + [BinaryOrder(0)] public long LongValue { get; set; } [BinaryIgnore] @@ -37,65 +43,85 @@ public class TypeWithIgnoredProperties public class TypeWithNestedObject { + [BinaryOrder(0)] public int IntegerValue { get; set; } + [BinaryOrder(1)] public NestedType ComplexValue { get; set; } + [BinaryOrder(2)] public int AnotherIntegerValue { get; set; } public sealed class NestedType { + [BinaryOrder(0)] public int NestedValue { get; set; } } } public class TypeWithEndiannessChanges { + [BinaryOrder(0)] [BinaryEndianness(EndiannessMode.LittleEndian)] public int LittleEndianInteger { get; set; } + [BinaryOrder(1)] [BinaryEndianness(EndiannessMode.BigEndian)] public int BigEndianInteger { get; set; } + [BinaryOrder(2)] public int DefaultEndianInteger { get; set; } } public class TypeWithNullable { + [BinaryOrder(0)] public int? NullValue { get; set; } } #region Integer types public class TypeWithIntegers { + [BinaryOrder(0)] public char CharValue { get; set; } + [BinaryOrder(1)] public byte ByteValue { get; set; } + [BinaryOrder(2)] public sbyte SByteValue { get; set; } + [BinaryOrder(3)] public ushort UShortValue { get; set; } + [BinaryOrder(4)] public short ShortValue { get; set; } + [BinaryOrder(5)] public uint UIntValue { get; set; } + [BinaryOrder(6)] public int IntegerValue { get; set; } + [BinaryOrder(7)] public ulong ULongValue { get; set; } + [BinaryOrder(8)] public long LongValue { get; set; } } public class TypeWithDecimals { + [BinaryOrder(0)] public float SingleValue { get; set; } + [BinaryOrder(1)] public double DoubleValue { get; set; } } public class TypeWithInt24 { + [BinaryOrder(0)] [BinaryInt24] public int Int24Value { get; set; } } @@ -104,40 +130,52 @@ public class TypeWithInt24 #region Boolean types public class TypeWithBooleanDefaultAttribute { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryBoolean] public bool BooleanValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithBooleanWithoutAttribute { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] public bool BooleanValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithBooleanDefinedValue { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryBoolean(UnderlyingType = typeof(short), TrueValue = (short)42, FalseValue = (short)-42)] public bool BooleanValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithBooleanTextValue { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryBoolean(UnderlyingType = typeof(string), TrueValue = "true", FalseValue = "false")] public bool BooleanValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } #endregion @@ -145,60 +183,78 @@ public class TypeWithBooleanTextValue #region String types public class TypeWithStringDefaultAttribute { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringWithoutAttribute { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringVariableSize { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString(SizeType = typeof(ushort), Terminator = "")] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringFixedSize { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString(FixedSize = 3, Terminator = "")] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringDefinedEncoding { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString(CodePage = 932)] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } public class TypeWithStringInvalidEncoding { + [BinaryOrder(0)] public int BeforeValue { get; set; } + [BinaryOrder(1)] [BinaryString(CodePage = 666)] public string StringValue { get; set; } + [BinaryOrder(2)] public int AfterValue { get; set; } } #endregion @@ -206,17 +262,20 @@ public class TypeWithStringInvalidEncoding #region Enum types public class TypeWithEnumNoAttribute { + [BinaryOrder(0)] public SerializableEnum EnumValue { get; set; } } public class TypeWithEnumDefaultAttribute { + [BinaryOrder(0)] [BinaryEnum] public SerializableEnum EnumValue { get; set; } } public class TypeWithEnumWithOverwrittenType { + [BinaryOrder(0)] [BinaryEnum(UnderlyingType = typeof(uint))] public SerializableEnum EnumValue { get; set; } } diff --git a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs index 86e4cb3..a20ce23 100644 --- a/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs +++ b/src/Yarhl.UnitTests/IO/Serialization/BinarySerializerTests.cs @@ -15,7 +15,7 @@ public void SerializeByGenericType() var obj = new SimpleType { Value = 0x0A, }; using var stream = new DataStream(); - var serializer = new BinarySerializer(stream); + var serializer = new BinarySerializer(stream, new DefaultTypePropertyNavigator()); serializer.Serialize(obj); AssertBinary(stream, data); diff --git a/src/Yarhl.UnitTests/IO/Serialization/DefaultTypePropertyNavigatorTests.cs b/src/Yarhl.UnitTests/IO/Serialization/DefaultTypePropertyNavigatorTests.cs new file mode 100644 index 0000000..ead428b --- /dev/null +++ b/src/Yarhl.UnitTests/IO/Serialization/DefaultTypePropertyNavigatorTests.cs @@ -0,0 +1,253 @@ +namespace Yarhl.UnitTests.IO.Serialization; + +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using NUnit.Framework; +using Yarhl.IO.Serialization; +using Yarhl.IO.Serialization.Attributes; + +[TestFixture] +public class DefaultTypePropertyNavigatorTests +{ + [Test] + public void PropertiesReturnedInOrderViaAttributes() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(SimpleType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(2)); + Assert.Multiple(() => { + Assert.That(fields[0].Name, Is.EqualTo(nameof(SimpleType.Prop2))); + Assert.That(fields[1].Name, Is.EqualTo(nameof(SimpleType.Prop1))); + }); + } + + [Test] + public void IgnorePrivateProperties() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(IgnorePrivatePropertiesType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(1)); + Assert.That(fields[0].Name, Is.EqualTo(nameof(IgnorePrivatePropertiesType.Prop0))); + } + + [Test] + public void IgnoreStaticProperties() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(IgnoreStaticPropertiesType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(1)); + Assert.That(fields[0].Name, Is.EqualTo(nameof(IgnoreStaticPropertiesType.Prop1))); + } + + [Test] + public void IgnorePropertiesWithIgnoreAttribute() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(IgnorePropertiesWithIgnoreAttributeType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(2)); + Assert.Multiple(() => { + Assert.That(fields[0].Name, Is.EqualTo(nameof(IgnorePropertiesWithIgnoreAttributeType.Prop0))); + Assert.That(fields[1].Name, Is.EqualTo(nameof(IgnorePropertiesWithIgnoreAttributeType.Prop2))); + }); + } + + [Test] + public void IgnorePropertiesWithoutPublicGetterOrSetter() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(IgnorePropertiesWithoutPublicGetterOrSetterType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(1)); + Assert.That(fields[0].Name, Is.EqualTo(nameof(IgnorePropertiesWithoutPublicGetterOrSetterType.ValidProp))); + } + + [Test] + public void PropertyOrderWithInheritance() + { + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(InheritedType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(5)); + Assert.Multiple(() => { + Assert.That(fields[0].Name, Is.EqualTo(nameof(InheritedType.Prop0))); + Assert.That(fields[1].Name, Is.EqualTo(nameof(InheritedType.PropBase0))); + Assert.That(fields[2].Name, Is.EqualTo(nameof(InheritedType.Prop1))); + Assert.That(fields[3].Name, Is.EqualTo(nameof(InheritedType.PropBase1))); + Assert.That(fields[4].Name, Is.EqualTo(nameof(InheritedType.Prop2))); + }); + } + + [Test] + public void TypeWithoutPropertyOrderAttributeThrowsInNet60() + { + if (!RuntimeInformation.FrameworkDescription.StartsWith(".NET 6")) { + Assert.Ignore("Test for another platform"); + return; + } + + var navigator = new DefaultTypePropertyNavigator(); + + Assert.That( + () => navigator.IterateFields(typeof(PropertiesWithoutOrderAttributeType)).ToArray(), + Throws.Exception.InstanceOf()); + } + + [Test] + public void TypeWithoutPropertyOrderAttributeWorksInNet80() + { + if (RuntimeInformation.FrameworkDescription.StartsWith(".NET 6")) { + Assert.Ignore("Test for another platform"); + return; + } + + var navigator = new DefaultTypePropertyNavigator(); + + FieldInfo[] fields = navigator.IterateFields(typeof(PropertiesWithoutOrderAttributeType)).ToArray(); + + Assert.That(fields, Has.Length.EqualTo(2)); + Assert.Multiple(() => { + Assert.That(fields[0].Name, Is.EqualTo(nameof(PropertiesWithoutOrderAttributeType.Prop0))); + Assert.That(fields[1].Name, Is.EqualTo(nameof(PropertiesWithoutOrderAttributeType.Prop1))); + }); + } + + [Test] + public void TypeWithSomePropertyMissingOrderAttributeThrows() + { + var navigator = new DefaultTypePropertyNavigator(); + + Assert.That( + () => navigator.IterateFields(typeof(SomePropertiesWithoutOrderAttributeType)).ToArray(), + Throws.Exception.InstanceOf()); + } + +#pragma warning disable S3459 // unused properties + + private sealed class SimpleType + { + [BinaryOrder(10)] + public int Prop1 { get; set; } + + [BinaryOrder(-5)] + public int Prop2 { get; set; } + } + + private sealed class IgnorePrivatePropertiesType + { + [BinaryOrder(0)] + public int Prop0 { get; set; } + + [BinaryOrder(1)] + private int Prop1 { get; set; } + } + + private sealed class IgnoreStaticPropertiesType + { + [BinaryOrder(-1)] + public static int Prop0 { get; set; } + + [BinaryOrder(0)] + public int Prop1 { get; set; } + } + + private sealed class IgnorePropertiesWithIgnoreAttributeType + { + [BinaryOrder(-1)] + public int Prop0 { get; set; } + + [BinaryOrder(0)] + [BinaryIgnore] + public int Prop1 { get; set; } + + [BinaryOrder(1)] + public int Prop2 { get; set; } + } + + private class IgnorePropertiesWithoutPublicGetterOrSetterType + { + private int val; + + [BinaryOrder(-1)] + public int Prop0 { private get; set; } + + [BinaryOrder(0)] + public int ValidProp { get; set; } + + [BinaryOrder(1)] + public int Prop2 { get; private set; } + + [BinaryOrder(2)] + public int Prop3 { internal get; set; } + + [BinaryOrder(3)] + public int Prop4 { get; protected set; } + + [BinaryOrder(4)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("", "S2376", Justification = "Test")] + public int Prop5 { + set => val = value; + } + + [BinaryOrder(5)] + public int Prop6 { + get => val; + } + + [BinaryOrder(6)] + internal int Prop7 { get; set; } + + [BinaryOrder(7)] + protected int Prop8 { get; set; } + + [BinaryOrder(8)] + private int Prop9 { get; set; } + } + + private class BaseType + { + [BinaryOrder(5)] + public int PropBase0 { get; set; } + + [BinaryOrder(10)] + public int PropBase1 { get; set; } + } + + private sealed class InheritedType : BaseType + { + [BinaryOrder(4)] + public int Prop0 { get; set; } + + [BinaryOrder(8)] + public int Prop1 { get; set; } + + [BinaryOrder(15)] + public int Prop2 { get; set; } + } + + private sealed class PropertiesWithoutOrderAttributeType + { + public int Prop0 { get; set; } + + public int Prop1 { get; set; } + } + + private sealed class SomePropertiesWithoutOrderAttributeType + { + [BinaryOrder(0)] + public int Prop0 { get; set; } + + public int Prop1 { get; set; } + } +} diff --git a/src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs b/src/Yarhl/IO/Serialization/Attributes/BinaryOrderAttribute.cs similarity index 57% rename from src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs rename to src/Yarhl/IO/Serialization/Attributes/BinaryOrderAttribute.cs index a9081cd..67b3850 100644 --- a/src/Yarhl/IO/Serialization/Attributes/BinaryFieldOrderAttribute.cs +++ b/src/Yarhl/IO/Serialization/Attributes/BinaryOrderAttribute.cs @@ -6,19 +6,14 @@ /// Specify the order to serialize or deserialize the fields in binary format. /// [AttributeUsage(AttributeTargets.Property)] -public class BinaryFieldOrderAttribute : Attribute +public class BinaryOrderAttribute : Attribute { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The order of the field in the binary serialization. - /// The order is less than 0. - public BinaryFieldOrderAttribute(int order) + public BinaryOrderAttribute(int order) { - if (order < 0) { - throw new ArgumentOutOfRangeException(nameof(order)); - } - Order = order; } diff --git a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs index 4f707f3..e51ce1a 100644 --- a/src/Yarhl/IO/Serialization/BinaryDeserializer.cs +++ b/src/Yarhl/IO/Serialization/BinaryDeserializer.cs @@ -2,7 +2,6 @@ using System; using System.IO; -using System.Reflection; using System.Text; using Yarhl.IO.Serialization.Attributes; @@ -13,6 +12,7 @@ public class BinaryDeserializer { private readonly DataReader reader; + private readonly ITypeFieldNavigator fieldNavigator; /// /// Initializes a new instance of the class. @@ -20,7 +20,24 @@ public class BinaryDeserializer /// The stream to read from. public BinaryDeserializer(Stream stream) { + ArgumentNullException.ThrowIfNull(stream); + reader = new DataReader(stream); + fieldNavigator = new DefaultTypePropertyNavigator(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The stream to read from. + /// The strategy to iterate the field's type. + public BinaryDeserializer(Stream stream, ITypeFieldNavigator fieldNavigator) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(fieldNavigator); + + reader = new DataReader(stream); + this.fieldNavigator = fieldNavigator; } /// @@ -67,51 +84,40 @@ public T Deserialize() /// A new object deserialized. public object Deserialize(Type objType) { - // It returns null for Nullable, but as that is a class and - // it won't have the serializable attribute, it will throw an - // unsupported exception before. So this can't be null at this point. - object obj = Activator.CreateInstance(objType)!; - - PropertyInfo[] properties = objType.GetProperties( - BindingFlags.Public | - BindingFlags.Instance); - - foreach (PropertyInfo property in properties) { - bool ignore = Attribute.IsDefined(property, typeof(BinaryIgnoreAttribute)); - if (ignore) { - continue; - } + object obj = Activator.CreateInstance(objType) + ?? throw new FormatException("Nullable types are not supported"); - object propertyValue = DeserializePropertyValue(property); - property.SetValue(obj, propertyValue); + foreach (FieldInfo fieldInfo in fieldNavigator.IterateFields(objType)) { + object propertyValue = DeserializePropertyValue(fieldInfo); + fieldInfo.SetValueFunc(obj, propertyValue); } return obj; } - private object DeserializePropertyValue(PropertyInfo property) + private object DeserializePropertyValue(FieldInfo fieldInfo) { reader.Endianness = DefaultEndianness; - var endiannessAttr = property.GetCustomAttribute(); + var endiannessAttr = fieldInfo.GetAttribute(); if (endiannessAttr is not null) { reader.Endianness = endiannessAttr.Mode; } - if (property.PropertyType.IsPrimitive) { - return DeserializePrimitiveField(property); - } else if (property.PropertyType.IsEnum) { - return DeserializeEnumField(property); - } else if (property.PropertyType == typeof(string)) { - return DeserializeStringField(property); + if (fieldInfo.Type.IsPrimitive) { + return DeserializePrimitiveField(fieldInfo); + } else if (fieldInfo.Type.IsEnum) { + return DeserializeEnumField(fieldInfo); + } else if (fieldInfo.Type == typeof(string)) { + return DeserializeStringField(fieldInfo); } else { - return Deserialize(property.PropertyType); + return Deserialize(fieldInfo.Type); } } - private object DeserializePrimitiveField(PropertyInfo property) + private object DeserializePrimitiveField(FieldInfo fieldInfo) { - if (property.PropertyType == typeof(bool)) { - if (property.GetCustomAttribute() is not { } boolAttr) { + if (fieldInfo.Type == typeof(bool)) { + if (fieldInfo.GetAttribute() is not { } boolAttr) { throw new FormatException("Properties of type 'bool' must have the attribute BinaryBoolean"); } @@ -119,26 +125,26 @@ private object DeserializePrimitiveField(PropertyInfo property) return value.Equals(boolAttr.TrueValue); } - if (property.PropertyType == typeof(int) && Attribute.IsDefined(property, typeof(BinaryInt24Attribute))) { + if (fieldInfo.Type == typeof(int) && fieldInfo.GetAttribute() is not null) { return reader.ReadInt24(); } - return reader.ReadByType(property.PropertyType); + return reader.ReadByType(fieldInfo.Type); } - private object DeserializeEnumField(PropertyInfo property) + private object DeserializeEnumField(FieldInfo fieldInfo) { - var enumAttr = property.GetCustomAttribute(); + var enumAttr = fieldInfo.GetAttribute(); Type underlyingType = enumAttr?.UnderlyingType - ?? Enum.GetUnderlyingType(property.PropertyType); + ?? Enum.GetUnderlyingType(fieldInfo.Type); object value = reader.ReadByType(underlyingType); - return Enum.ToObject(property.PropertyType, value); + return Enum.ToObject(fieldInfo.Type, value); } - private string DeserializeStringField(PropertyInfo property) + private string DeserializeStringField(FieldInfo fieldInfo) { - if (property.GetCustomAttribute() is not { } stringAttr) { + if (fieldInfo.GetAttribute() is not { } stringAttr) { // Use default settings if not specified. return reader.ReadString(); } diff --git a/src/Yarhl/IO/Serialization/BinarySerializer.cs b/src/Yarhl/IO/Serialization/BinarySerializer.cs index 72ecb65..0fc3536 100644 --- a/src/Yarhl/IO/Serialization/BinarySerializer.cs +++ b/src/Yarhl/IO/Serialization/BinarySerializer.cs @@ -2,7 +2,7 @@ using System; using System.IO; -using System.Reflection; +using System.Linq; using System.Text; using Yarhl.IO.Serialization.Attributes; @@ -12,6 +12,7 @@ /// public class BinarySerializer { + private readonly ITypeFieldNavigator fieldNavigator; private readonly DataWriter writer; /// @@ -20,7 +21,24 @@ public class BinarySerializer /// The stream to write the binary data. public BinarySerializer(Stream stream) { + ArgumentNullException.ThrowIfNull(stream); + writer = new DataWriter(stream); + fieldNavigator = new DefaultTypePropertyNavigator(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The stream to write the binary data. + /// Strategy to iterate over type fields. + public BinarySerializer(Stream stream, ITypeFieldNavigator fieldNavigator) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(fieldNavigator); + + writer = new DataWriter(stream); + this.fieldNavigator = fieldNavigator; } /// @@ -69,52 +87,38 @@ public void Serialize(T obj) /// The object to serialize into the stream. public void Serialize(Type type, object obj) { - PropertyInfo[] properties = type.GetProperties( - BindingFlags.Public | - BindingFlags.Instance); - - // TODO: Introduce property to sort - foreach (PropertyInfo property in properties) { - bool ignore = Attribute.IsDefined(property, typeof(BinaryIgnoreAttribute)); - if (ignore) { - continue; - } - + foreach (FieldInfo property in fieldNavigator.IterateFields(type)) { SerializeProperty(property, obj); } } - private void SerializeProperty(PropertyInfo property, object obj) + private void SerializeProperty(FieldInfo fieldInfo, object obj) { writer.Endianness = DefaultEndianness; - var endiannessAttr = property.GetCustomAttribute(); + var endiannessAttr = fieldInfo.GetAttribute(); if (endiannessAttr is not null) { writer.Endianness = endiannessAttr.Mode; } - object value = property.GetValue(obj) + object value = fieldInfo.GetValueFunc(obj) ?? throw new FormatException("Cannot serialize nullable values"); - if (property.PropertyType.IsPrimitive) { - SerializePrimitiveField(property, value); - } else if (property.PropertyType.IsEnum) { - var enumAttr = property.GetCustomAttribute(); - Type underlyingType = enumAttr?.UnderlyingType - ?? Enum.GetUnderlyingType(property.PropertyType); - - writer.WriteOfType(underlyingType, value); - } else if (property.PropertyType == typeof(string)) { - SerializeString(property, value); + if (fieldInfo.Type.IsPrimitive) { + SerializePrimitiveField(fieldInfo, value); + } else if (fieldInfo.Type.IsEnum) { + SerializeEnumField(fieldInfo, value); + } else if (fieldInfo.Type == typeof(string)) { + SerializeString(fieldInfo, value); } else { - Serialize(property.PropertyType, value); + Serialize(fieldInfo.Type, value); } } - private void SerializePrimitiveField(PropertyInfo property, object value) + private void SerializePrimitiveField(FieldInfo fieldInfo, object value) { // Handle first the special cases - if (property.PropertyType == typeof(bool)) { - if (property.GetCustomAttribute() is not { } boolAttr) { + if (fieldInfo.Type == typeof(bool)) { + if (fieldInfo.GetAttribute() is not { } boolAttr) { throw new FormatException("Properties of type 'bool' must have the attribute BinaryBoolean"); } @@ -123,18 +127,27 @@ private void SerializePrimitiveField(PropertyInfo property, object value) return; } - if (property.PropertyType == typeof(int) && Attribute.IsDefined(property, typeof(BinaryInt24Attribute))) { + if (fieldInfo.Type == typeof(int) && fieldInfo.Attributes.Any(a => a is BinaryInt24Attribute)) { writer.WriteInt24((int)value); return; } // Fallback to DataWriter primitive write - writer.WriteOfType(property.PropertyType, value); + writer.WriteOfType(fieldInfo.Type, value); + } + + private void SerializeEnumField(FieldInfo fieldInfo, object value) + { + var enumAttr = fieldInfo.GetAttribute(); + Type underlyingType = enumAttr?.UnderlyingType + ?? Enum.GetUnderlyingType(fieldInfo.Type); + + writer.WriteOfType(underlyingType, value); } - private void SerializeString(PropertyInfo property, object value) + private void SerializeString(FieldInfo fieldInfo, object value) { - if (property.GetCustomAttribute() is not { } stringAttr) { + if (fieldInfo.GetAttribute() is not { } stringAttr) { // Use default settings if not specified. writer.Write((string)value); return; diff --git a/src/Yarhl/IO/Serialization/DefaultTypePropertyNavigator.cs b/src/Yarhl/IO/Serialization/DefaultTypePropertyNavigator.cs new file mode 100644 index 0000000..0f681e0 --- /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 0000000..10c5047 --- /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 0000000..1e7769c --- /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); +}