diff --git a/README.md b/README.md index 2dd627e..ac14c52 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,174 @@ -# EndianBinaryIO +[![NuGet](https://img.shields.io/nuget/v/EndianBinaryIO.svg)](https://www.nuget.org/packages/EndianBinaryIO) [![downloads](https://img.shields.io/nuget/dt/EndianBinaryIO)](https://www.nuget.org/packages/EndianBinaryIO) -A C# library that can read and write primitives, enums, arrays, and strings with specified endianness, string encoding, and boolean sizes. +# 📖 EndianBinaryIO + +A C# library that can read and write primitives, enums, arrays, and strings to streams using specified endianness, string encoding, and boolean sizes. +Objects can also be read from/written to streams via reflection and attributes. The IBinarySerializable interface allows an object to be read and written in a customizable fashion. Also included are attributes that can make reading and writing objects less of a headache. For example, classes and structs in C# cannot have ignored members when marshalling, but EndianBinaryIO has a BinaryIgnoreAttribute that will ignore properties when reading and writing. ---- -## Example: -### Class: +## 🚀 Usage: +Add the [EndianBinaryIO](https://www.nuget.org/packages/EndianBinaryIO) NuGet package to your project or download the .dll from [the releases tab](https://github.com/Kermalis/EndianBinaryIO/releases). + +---- +## Examples: +Assume we have the following definitions: +### C#: ```cs - enum ShortSizedEnum : short - { - Val1 = 0x40, - Val2 = 0x800 - } +enum ShortSizedEnum : short +{ + Val1 = 0x40, + Val2 = 0x800 +} + +class MyBasicObj +{ + // Properties + public ShortSizedEnum Type { get; set; } + public short Version { get; set; } + + // Property that is ignored when reading and writing + [BinaryIgnore(true)] + public double DoNotReadOrWrite { get; set; } = Math.PI; + + // Arrays work as well + [BinaryArrayFixedLength(16)] + public uint[] ArrayWith16Elements { get; set; } + + // Boolean that occupies 4 bytes instead of one + [BinaryBooleanSize(BooleanSize.U32)] + public bool Bool32 { get; set; } + + // String encoded in ASCII + // Reads chars until the stream encounters a '\0' + // Writing will append a '\0' at the end of the string + [BinaryEncoding(EncodingType.ASCII)] + [BinaryStringNullTerminated(true)] + public string NullTerminatedASCIIString { get; set; } + + // String encoded in UTF-16 that will only read/write 10 chars + [BinaryEncoding(EncodingType.UTF16)] + [BinaryStringFixedLength(10)] + public string UTF16String { get; set; } +} +``` +And assume these are our input bytes (in little endian): +### Input Bytes (Little Endian): +```cs +0x00, 0x08, +0xFF, 0x01, + +0x00, 0x00, 0x00, 0x00, +0x01, 0x00, 0x00, 0x00, +0x02, 0x00, 0x00, 0x00, +0x03, 0x00, 0x00, 0x00, +0x04, 0x00, 0x00, 0x00, +0x05, 0x00, 0x00, 0x00, +0x06, 0x00, 0x00, 0x00, +0x07, 0x00, 0x00, 0x00, +0x08, 0x00, 0x00, 0x00, +0x09, 0x00, 0x00, 0x00, +0x0A, 0x00, 0x00, 0x00, +0x0B, 0x00, 0x00, 0x00, +0x0C, 0x00, 0x00, 0x00, +0x0D, 0x00, 0x00, 0x00, +0x0E, 0x00, 0x00, 0x00, +0x0F, 0x00, 0x00, 0x00, + +0x00, 0x00, 0x00, 0x00, + +0x45, 0x6E, 0x64, 0x69, 0x61, 0x6E, 0x42, 0x69, 0x6E, 0x61, 0x72, 0x79, 0x49, 0x4F, 0x00, + +0x4B, 0x00, 0x65, 0x00, 0x72, 0x00, 0x6D, 0x00, 0x61, 0x00, 0x6C, 0x00, 0x69, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00 +``` + +We can read/write the object manually or automatically (with reflection): +### Reading Manually: +```cs +MyBasicObj obj; +using (var reader = new EndianBinaryReader(stream, endianness: Endianness.LittleEndian, booleanSize: BooleanSize.U32)) +{ + obj = new MyBasicObj(); + + obj.Type = reader.ReadEnum(); // Enum works + obj.Version = reader.ReadInt16(); // short works - class MyBasicStruct + obj.ArrayWith16Elements = reader.ReadUInt32s(16); // Array works + + obj.Bool32 = reader.ReadBoolean(); // bool32 works + + obj.NullTerminatedASCIIString = reader.ReadStringNullTerminated(EncodingType.ASCII); // Stops reading at null terminator + obj.UTF16String = reader.ReadString(10, EncodingType.UTF16); // Fixed size (10 chars) utf16 +} +``` +### Reading Automatically (With Reflection): +```cs +MyBasicObj obj; +using (var reader = new EndianBinaryReader(stream, endianness: Endianness.LittleEndian)) +{ + obj = reader.ReadObject(); +} +``` + +### Writing Manually: +```cs +using (var writer = new EndianBinaryWriter(stream, endianness: Endianness.LittleEndian)) +{ + var obj = new MyBasicObj { - // Properties - public ShortSizedEnum Type { get; set; } - public short Version { get; set; } - - // Property that is ignored when reading and writing - [BinaryIgnore(true)] - public double DoNotReadOrWrite { get; set; } = Math.PI; - - // Arrays work as well - [BinaryArrayFixedLength(16)] - public uint[] ArrayWith16Elements { get; set; } - - // Boolean that occupies 4 bytes instead of one - [BinaryBooleanSize(BooleanSize.U32)] - public bool Bool32 { get; set; } - - // String encoded in ASCII - // Reads chars until the stream encounters a '\0' - // Writing will append a '\0' at the end of the string - [BinaryEncoding(EncodingType.ASCII)] - [BinaryStringNullTerminated(true)] - public string NullTerminatedASCIIString { get; set; } - - // String encoded in UTF-16 that will only read/write 10 chars - [BinaryEncoding(EncodingType.UTF16)] - [BinaryStringFixedLength(10)] - public string UTF16String { get; set; } - } + Type = ShortSizedEnum.Val2, + Version = 511, + + DoNotReadOrWrite = ByteSizedEnum.Val1, + + ArrayWith16Elements = new uint[16] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + }, + + Bool32 = false, + + NullTerminatedASCIIString = "EndianBinaryIO", + + UTF16String = "Kermalis" + }; + + writer.Write(obj); +} ``` -### Byte Representation (Little Endian): +### Writing Automatically (With Reflection): ```cs - 0x00, 0x08, - 0xFF, 0x01, - - 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, - 0x0A, 0x00, 0x00, 0x00, - 0x0B, 0x00, 0x00, 0x00, - 0x0C, 0x00, 0x00, 0x00, - 0x0D, 0x00, 0x00, 0x00, - 0x0E, 0x00, 0x00, 0x00, - 0x0F, 0x00, 0x00, 0x00, - - 0x00, 0x00, 0x00, 0x00, - - 0x45, 0x6E, 0x64, 0x69, 0x61, 0x6E, 0x42, 0x69, 0x6E, 0x61, 0x72, 0x79, 0x49, 0x4F, 0x00, - - 0x4B, 0x00, 0x65, 0x00, 0x72, 0x00, 0x6D, 0x00, 0x61, 0x00, 0x6C, 0x00, 0x69, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00 +using (var writer = new EndianBinaryWriter(stream, endianness: Endianness.LittleEndian, booleanSize: BooleanSize.U32)) +{ + var obj = new MyBasicObj + { + Type = ShortSizedEnum.Val2, + Version = 511, + + DoNotReadOrWrite = ByteSizedEnum.Val1, + + ArrayWith16Elements = new uint[16] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + }, + + Bool32 = false, + + NullTerminatedASCIIString = "EndianBinaryIO", + + UTF16String = "Kermalis" + }; + + writer.Write(obj.Type); + writer.Write(obj.Version); + writer.Write(obj.ArrayWith16Elements); + writer.Write(obj.Bool32); + writer.Write(obj.NullTerminatedASCIIString, true, EncodingType.ASCII); + writer.Write(obj.UTF16String, 10, EncodingType.UTF16); +} ``` ---- diff --git a/Source/EndianBinaryIO.csproj b/Source/EndianBinaryIO.csproj index fbb89db..ae1c4f8 100644 --- a/Source/EndianBinaryIO.csproj +++ b/Source/EndianBinaryIO.csproj @@ -9,10 +9,18 @@ EndianBinaryIO EndianBinaryIO EndianBinaryIO - 1.0.0.0 + 1.0.1.0 https://github.com/Kermalis/EndianBinaryIO + git true + + A .NET Standard library that can read and write primitives, enums, arrays, and strings to streams using specified endianness, string encoding, and boolean sizes. +Objects can also be read from/written to streams via reflection and attributes. +Project URL ― https://github.com/Kermalis/EndianBinaryIO + https://github.com/Kermalis/EndianBinaryIO + en-001 + Serialization;Reflection;Endianness;LittleEndian;BigEndian;EndianBinaryIO diff --git a/Source/EndianBinaryReader.cs b/Source/EndianBinaryReader.cs index 1bf6ff1..af9c3a2 100644 --- a/Source/EndianBinaryReader.cs +++ b/Source/EndianBinaryReader.cs @@ -616,188 +616,164 @@ public void ReadIntoObject(object obj) } Type objType = obj.GetType(); Utils.ThrowIfCannotReadWriteType(objType); - if (typeof(IBinarySerializable).IsAssignableFrom(objType)) + if (obj is IBinarySerializable bs) { - ((IBinarySerializable)obj).Read(this); + bs.Read(this); + return; } - else + + // Get public non-static properties + foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { - foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + if (Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryIgnoreAttribute), false)) { - if (!Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryIgnoreAttribute), false)) - { - BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryBooleanSizeAttribute), BooleanSize); - EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); - bool nullTerminated = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryStringNullTerminatedAttribute), true); + continue; // Skip properties with BinaryIgnoreAttribute + } - int arrayFixedLength = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryArrayFixedLengthAttribute), 0); - int stringFixedLength = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryStringFixedLengthAttribute), 0); - string arrayVariableLengthAnchor = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryArrayVariableLengthAttribute), string.Empty); - int arrayVariableLength = 0; - if (!string.IsNullOrEmpty(arrayVariableLengthAnchor)) + Type propertyType = propertyInfo.PropertyType; + object value; + + if (propertyType.IsArray) + { + int arrayLength = Utils.GetArrayLength(obj, objType, propertyInfo); + // Get array type + Type elementType = propertyType.GetElementType(); + if (elementType.IsEnum) + { + elementType = Enum.GetUnderlyingType(elementType); + } + switch (elementType.FullName) + { + case "System.Boolean": { - PropertyInfo anchor = objType.GetProperty(arrayVariableLengthAnchor, BindingFlags.Instance | BindingFlags.Public); - if (anchor is null) - { - throw new MissingMemberException($"A property in \"{objType.FullName}\" has an invalid variable array length anchor ({arrayVariableLengthAnchor})."); - } - else + BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryBooleanSizeAttribute), BooleanSize); + value = ReadBooleans(arrayLength, booleanSize); + break; + } + case "System.Byte": value = ReadBytes(arrayLength); break; + case "System.SByte": value = ReadSBytes(arrayLength); break; + case "System.Char": + { + EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); + value = ReadChars(arrayLength, encodingType); + break; + } + case "System.Int16": value = ReadInt16s(arrayLength); break; + case "System.UInt16": value = ReadUInt16s(arrayLength); break; + case "System.Int32": value = ReadInt32s(arrayLength); break; + case "System.UInt32": value = ReadUInt32s(arrayLength); break; + case "System.Int64": value = ReadInt64s(arrayLength); break; + case "System.UInt64": value = ReadUInt64s(arrayLength); break; + case "System.Single": value = ReadSingles(arrayLength); break; + case "System.Double": value = ReadDoubles(arrayLength); break; + case "System.Decimal": value = ReadDecimals(arrayLength); break; + case "System.String": + { + Utils.GetStringLength(obj, objType, propertyInfo, true, out bool? nullTerminated, out int stringLength); + EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); + value = Array.CreateInstance(elementType, arrayLength); + for (int i = 0; i < arrayLength; i++) { - arrayVariableLength = Convert.ToInt32(anchor.GetValue(obj)); + string str; + if (nullTerminated == true) + { + str = ReadStringNullTerminated(encodingType); + } + else + { + str = ReadString(stringLength, encodingType); + } + ((Array)value).SetValue(str, i); } + break; } - string stringVariableLengthAnchor = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryStringVariableLengthAttribute), string.Empty); - int stringVariableLength = 0; - if (!string.IsNullOrEmpty(stringVariableLengthAnchor)) + default: { - PropertyInfo anchor = objType.GetProperty(stringVariableLengthAnchor, BindingFlags.Instance | BindingFlags.Public); - if (anchor is null) + value = Array.CreateInstance(elementType, arrayLength); + if (typeof(IBinarySerializable).IsAssignableFrom(elementType)) { - throw new MissingMemberException($"A property in \"{objType.FullName}\" has an invalid variable string length anchor ({stringVariableLengthAnchor})."); + for (int i = 0; i < arrayLength; i++) + { + var serializable = (IBinarySerializable)Activator.CreateInstance(elementType); + serializable.Read(this); + ((Array)value).SetValue(serializable, i); + } } - else + else // Element's type is not supported so try to read the array's objects { - stringVariableLength = Convert.ToInt32(anchor.GetValue(obj)); + for (int i = 0; i < arrayLength; i++) + { + object elementObj = ReadObject(elementType); + ((Array)value).SetValue(elementObj, i); + } } + break; } - if ((arrayFixedLength > 0 && arrayVariableLength > 0) - || (stringFixedLength > 0 && stringVariableLength > 0)) + } + } + else + { + if (propertyType.IsEnum) + { + propertyType = Enum.GetUnderlyingType(propertyType); + } + switch (propertyType.FullName) + { + case "System.Boolean": { - throw new ArgumentException($"A property in \"{objType.FullName}\" has two length attributes."); + BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryBooleanSizeAttribute), BooleanSize); + value = ReadBoolean(booleanSize); + break; } - // One will be 0 and the other will be the intended length, so it is safe to use Math.Max to get the intended length - int arrayLength = Math.Max(arrayFixedLength, arrayVariableLength); - int stringLength = Math.Max(stringFixedLength, stringVariableLength); - if (stringLength > 0) + case "System.Byte": value = ReadByte(); break; + case "System.SByte": value = ReadSByte(); break; + case "System.Char": { - nullTerminated = false; + EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); + value = ReadChar(encodingType); + break; } - - Type propertyType = propertyInfo.PropertyType; - object value; - - if (propertyType.IsArray) + case "System.Int16": value = ReadInt16(); break; + case "System.UInt16": value = ReadUInt16(); break; + case "System.Int32": value = ReadInt32(); break; + case "System.UInt32": value = ReadUInt32(); break; + case "System.Int64": value = ReadInt64(); break; + case "System.UInt64": value = ReadUInt64(); break; + case "System.Single": value = ReadSingle(); break; + case "System.Double": value = ReadDouble(); break; + case "System.Decimal": value = ReadDecimal(); break; + case "System.String": { - if (arrayLength < 0) - { - throw new ArgumentOutOfRangeException($"An array in \"{objType.FullName}\" attempted to be read with an invalid length ({arrayLength})."); - } - // Get array type - Type elementType = propertyType.GetElementType(); - if (elementType.IsEnum) + Utils.GetStringLength(obj, objType, propertyInfo, true, out bool? nullTerminated, out int stringLength); + EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); + if (nullTerminated == true) { - elementType = Enum.GetUnderlyingType(elementType); + value = ReadStringNullTerminated(encodingType); } - switch (elementType.FullName) + else { - case "System.Boolean": value = ReadBooleans(arrayLength, booleanSize); break; - case "System.Byte": value = ReadBytes(arrayLength); break; - case "System.SByte": value = ReadSBytes(arrayLength); break; - case "System.Char": value = ReadChars(arrayLength, encodingType); break; - case "System.Int16": value = ReadInt16s(arrayLength); break; - case "System.UInt16": value = ReadUInt16s(arrayLength); break; - case "System.Int32": value = ReadInt32s(arrayLength); break; - case "System.UInt32": value = ReadUInt32s(arrayLength); break; - case "System.Int64": value = ReadInt64s(arrayLength); break; - case "System.UInt64": value = ReadUInt64s(arrayLength); break; - case "System.Single": value = ReadSingles(arrayLength); break; - case "System.Double": value = ReadDoubles(arrayLength); break; - case "System.Decimal": value = ReadDecimals(arrayLength); break; - case "System.String": - { - value = Array.CreateInstance(elementType, arrayLength); - for (int i = 0; i < arrayLength; i++) - { - string str; - if (nullTerminated) - { - str = ReadStringNullTerminated(encodingType); - } - else - { - str = ReadString(stringLength, encodingType); - } - ((Array)value).SetValue(str, i); - } - break; - } - default: - { - value = Array.CreateInstance(elementType, arrayLength); - if (typeof(IBinarySerializable).IsAssignableFrom(elementType)) - { - for (int i = 0; i < arrayLength; i++) - { - var serializable = (IBinarySerializable)Activator.CreateInstance(elementType); - serializable.Read(this); - ((Array)value).SetValue(serializable, i); - } - } - else // Element's type is not supported so try to read the array's objects - { - for (int i = 0; i < arrayLength; i++) - { - object elementObj = ReadObject(elementType); - ((Array)value).SetValue(elementObj, i); - } - } - break; - } + value = ReadString(stringLength, encodingType); } + break; } - else + default: { - if (propertyType.IsEnum) + if (typeof(IBinarySerializable).IsAssignableFrom(propertyType)) { - propertyType = Enum.GetUnderlyingType(propertyType); + value = Activator.CreateInstance(propertyType); + ((IBinarySerializable)value).Read(this); } - switch (propertyType.FullName) + else // The property's type is not supported so try to read the object { - case "System.Boolean": value = ReadBoolean(booleanSize); break; - case "System.Byte": value = ReadByte(); break; - case "System.SByte": value = ReadSByte(); break; - case "System.Char": value = ReadChar(encodingType); break; - case "System.Int16": value = ReadInt16(); break; - case "System.UInt16": value = ReadUInt16(); break; - case "System.Int32": value = ReadInt32(); break; - case "System.UInt32": value = ReadUInt32(); break; - case "System.Int64": value = ReadInt64(); break; - case "System.UInt64": value = ReadUInt64(); break; - case "System.Single": value = ReadSingle(); break; - case "System.Double": value = ReadDouble(); break; - case "System.Decimal": value = ReadDecimal(); break; - case "System.String": - { - if (nullTerminated) - { - value = ReadStringNullTerminated(encodingType); - } - else - { - value = ReadString(stringLength, encodingType); - } - break; - } - default: - { - if (typeof(IBinarySerializable).IsAssignableFrom(propertyType)) - { - value = Activator.CreateInstance(propertyType); - ((IBinarySerializable)value).Read(this); - } - else // The property's type is not supported so try to read the object - { - value = ReadObject(propertyType); - } - break; - } + value = ReadObject(propertyType); } + break; } - - // Set the value into the property - propertyInfo.SetValue(obj, value); } } + + // Set the value into the property + propertyInfo.SetValue(obj, value); } } public void ReadIntoObject(object obj, long offset) diff --git a/Source/EndianBinaryWriter.cs b/Source/EndianBinaryWriter.cs index 2629b41..f0df4f4 100644 --- a/Source/EndianBinaryWriter.cs +++ b/Source/EndianBinaryWriter.cs @@ -317,6 +317,25 @@ public void Write(string value, bool nullTerminated, EncodingType encodingType, BaseStream.Position = offset; Write(value, nullTerminated, encodingType); } + public void Write(string value, int charCount) + { + Write(value, charCount, Encoding); + } + public void Write(string value, int charCount, long offset) + { + BaseStream.Position = offset; + Write(value, charCount, Encoding); + } + public void Write(string value, int charCount, EncodingType encodingType) + { + TruncateString(value, charCount, out char[] chars); + Write(chars, encodingType); + } + public void Write(string value, int charCount, EncodingType encodingType, long offset) + { + BaseStream.Position = offset; + Write(value, charCount, encodingType); + } public void Write(short value) { SetBufferSize(2); @@ -781,179 +800,152 @@ public void Write(object obj) } Type objType = obj.GetType(); Utils.ThrowIfCannotReadWriteType(objType); - if (typeof(IBinarySerializable).IsAssignableFrom(objType)) + if (obj is IBinarySerializable bs) { - ((IBinarySerializable)obj).Write(this); + bs.Write(this); + return; } - else + + // Get public non-static properties + foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { - // Get public non-static properties - foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + if (Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryIgnoreAttribute), false)) { - if (!Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryIgnoreAttribute), false)) - { - BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryBooleanSizeAttribute), BooleanSize); - EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); - bool nullTerminated = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryStringNullTerminatedAttribute), true); + continue; // Skip properties with BinaryIgnoreAttribute + } + + Type propertyType = propertyInfo.PropertyType; + object value = propertyInfo.GetValue(obj); - int arrayFixedLength = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryArrayFixedLengthAttribute), 0); - int stringFixedLength = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryStringFixedLengthAttribute), 0); - string arrayVariableLengthAnchor = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryArrayVariableLengthAttribute), string.Empty); - int arrayVariableLength = 0; - if (!string.IsNullOrEmpty(arrayVariableLengthAnchor)) + if (propertyType.IsArray) + { + int arrayLength = Utils.GetArrayLength(obj, objType, propertyInfo); + // Get array type + Type elementType = propertyType.GetElementType(); + if (elementType.IsEnum) + { + elementType = Enum.GetUnderlyingType(elementType); + } + switch (elementType.FullName) + { + case "System.Boolean": { - PropertyInfo anchor = objType.GetProperty(arrayVariableLengthAnchor, BindingFlags.Instance | BindingFlags.Public); - if (anchor is null) - { - throw new MissingMemberException($"A property in \"{objType.FullName}\" has an invalid variable array length anchor ({arrayVariableLengthAnchor})."); - } - else + BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryBooleanSizeAttribute), BooleanSize); + Write((bool[])value, 0, arrayLength, booleanSize); + break; + } + case "System.Byte": Write((byte[])value, 0, arrayLength); break; + case "System.SByte": Write((sbyte[])value, 0, arrayLength); break; + case "System.Char": + { + EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); + Write((char[])value, 0, arrayLength, encodingType); + break; + } + case "System.Int16": Write((short[])value, 0, arrayLength); break; + case "System.UInt16": Write((ushort[])value, 0, arrayLength); break; + case "System.Int32": Write((int[])value, 0, arrayLength); break; + case "System.UInt32": Write((uint[])value, 0, arrayLength); break; + case "System.Int64": Write((long[])value, 0, arrayLength); break; + case "System.UInt64": Write((ulong[])value, 0, arrayLength); break; + case "System.Single": Write((float[])value, 0, arrayLength); break; + case "System.Double": Write((double[])value, 0, arrayLength); break; + case "System.Decimal": Write((decimal[])value, 0, arrayLength); break; + case "System.String": + { + Utils.GetStringLength(obj, objType, propertyInfo, false, out bool? nullTerminated, out int stringLength); + EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); + for (int i = 0; i < arrayLength; i++) { - arrayVariableLength = Convert.ToInt32(anchor.GetValue(obj)); + string str = (string)((Array)value).GetValue(i); + if (nullTerminated.HasValue) + { + Write(str, nullTerminated.Value, encodingType); + } + else + { + Write(str, stringLength, encodingType); + } } + break; } - string stringVariableLengthAnchor = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryStringVariableLengthAttribute), string.Empty); - int stringVariableLength = 0; - if (!string.IsNullOrEmpty(stringVariableLengthAnchor)) + default: { - PropertyInfo anchor = objType.GetProperty(stringVariableLengthAnchor, BindingFlags.Instance | BindingFlags.Public); - if (anchor is null) + if (typeof(IBinarySerializable).IsAssignableFrom(elementType)) { - throw new MissingMemberException($"A property in \"{objType.FullName}\" has an invalid variable string length anchor ({stringVariableLengthAnchor})."); + for (int i = 0; i < arrayLength; i++) + { + var serializable = (IBinarySerializable)((Array)value).GetValue(i); + serializable.Write(this); + } } - else + else // Element's type is not supported so try to write the array's objects { - stringVariableLength = Convert.ToInt32(anchor.GetValue(obj)); + for (int i = 0; i < arrayLength; i++) + { + Write(((Array)value).GetValue(i)); + } } + break; } - if ((arrayFixedLength > 0 && arrayVariableLength > 0) - || (stringFixedLength > 0 && stringVariableLength > 0)) + } + } + else + { + if (propertyType.IsEnum) + { + propertyType = Enum.GetUnderlyingType(propertyType); + } + switch (propertyType.FullName) + { + case "System.Boolean": { - throw new ArgumentException($"A property in \"{objType.FullName}\" has two length attributes."); + BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryBooleanSizeAttribute), BooleanSize); + Write((bool)value, booleanSize); + break; } - // One will be 0 and the other will be the intended length, so it is safe to use Math.Max to get the intended length - int arrayLength = Math.Max(arrayFixedLength, arrayVariableLength); - int stringLength = Math.Max(stringFixedLength, stringVariableLength); - if (stringLength > 0) + case "System.Byte": Write((byte)value); break; + case "System.SByte": Write((sbyte)value); break; + case "System.Char": { - nullTerminated = false; + EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); + Write((char)value, encodingType); + break; } - - Type propertyType = propertyInfo.PropertyType; - object value = propertyInfo.GetValue(obj); - - if (propertyType.IsArray) + case "System.Int16": Write((short)value); break; + case "System.UInt16": Write((ushort)value); break; + case "System.Int32": Write((int)value); break; + case "System.UInt32": Write((uint)value); break; + case "System.Int64": Write((long)value); break; + case "System.UInt64": Write((ulong)value); break; + case "System.Single": Write((float)value); break; + case "System.Double": Write((double)value); break; + case "System.Decimal": Write((decimal)value); break; + case "System.String": { - if (arrayLength < 0) + Utils.GetStringLength(obj, objType, propertyInfo, false, out bool? nullTerminated, out int stringLength); + EncodingType encodingType = Utils.AttributeValueOrDefault(propertyInfo, typeof(BinaryEncodingAttribute), Encoding); + if (nullTerminated.HasValue) { - throw new ArgumentOutOfRangeException($"An array in \"{objType.FullName}\" attempted to be written with an invalid length ({arrayLength})."); + Write((string)value, nullTerminated.Value, encodingType); } - // Get array type - Type elementType = propertyType.GetElementType(); - if (elementType.IsEnum) + else { - elementType = Enum.GetUnderlyingType(elementType); - } - switch (elementType.FullName) - { - case "System.Boolean": Write((bool[])value, 0, arrayLength, booleanSize); break; - case "System.Byte": Write((byte[])value, 0, arrayLength); break; - case "System.SByte": Write((sbyte[])value, 0, arrayLength); break; - case "System.Char": Write((char[])value, 0, arrayLength, encodingType); break; - case "System.Int16": Write((short[])value, 0, arrayLength); break; - case "System.UInt16": Write((ushort[])value, 0, arrayLength); break; - case "System.Int32": Write((int[])value, 0, arrayLength); break; - case "System.UInt32": Write((uint[])value, 0, arrayLength); break; - case "System.Int64": Write((long[])value, 0, arrayLength); break; - case "System.UInt64": Write((ulong[])value, 0, arrayLength); break; - case "System.Single": Write((float[])value, 0, arrayLength); break; - case "System.Double": Write((double[])value, 0, arrayLength); break; - case "System.Decimal": Write((decimal[])value, 0, arrayLength); break; - case "System.String": - { - for (int i = 0; i < arrayLength; i++) - { - string str = (string)((Array)value).GetValue(i); - if (nullTerminated) - { - Write(str, true, encodingType); - } - else - { - TruncateString(str, stringLength, out char[] chars); - Write(chars, encodingType); - } - } - break; - } - default: - { - if (typeof(IBinarySerializable).IsAssignableFrom(elementType)) - { - for (int i = 0; i < arrayLength; i++) - { - var serializable = (IBinarySerializable)((Array)value).GetValue(i); - serializable.Write(this); - } - } - else // Element's type is not supported so try to write the array's objects - { - for (int i = 0; i < arrayLength; i++) - { - Write(((Array)value).GetValue(i)); - } - } - break; - } + Write((string)value, stringLength, encodingType); } + break; } - else + default: { - if (propertyType.IsEnum) + if (typeof(IBinarySerializable).IsAssignableFrom(propertyType)) { - propertyType = Enum.GetUnderlyingType(propertyType); + ((IBinarySerializable)value).Write(this); } - switch (propertyType.FullName) + else // property's type is not supported so try to write the object { - case "System.Boolean": Write((bool)value, booleanSize); break; - case "System.Byte": Write((byte)value); break; - case "System.SByte": Write((sbyte)value); break; - case "System.Char": Write((char)value, encodingType); break; - case "System.Int16": Write((short)value); break; - case "System.UInt16": Write((ushort)value); break; - case "System.Int32": Write((int)value); break; - case "System.UInt32": Write((uint)value); break; - case "System.Int64": Write((long)value); break; - case "System.UInt64": Write((ulong)value); break; - case "System.Single": Write((float)value); break; - case "System.Double": Write((double)value); break; - case "System.Decimal": Write((decimal)value); break; - case "System.String": - { - if (nullTerminated) - { - Write((string)value, true, encodingType); - } - else - { - TruncateString((string)value, stringLength, out char[] chars); - Write(chars, encodingType); - } - break; - } - default: - { - if (typeof(IBinarySerializable).IsAssignableFrom(propertyType)) - { - ((IBinarySerializable)value).Write(this); - } - else // property's type is not supported so try to write the object - { - Write(value); - } - break; - } + Write(value); } + break; } } } diff --git a/Source/Utils.cs b/Source/Utils.cs index 2ccbe36..4c89961 100644 --- a/Source/Utils.cs +++ b/Source/Utils.cs @@ -187,5 +187,86 @@ public static void ThrowIfCannotReadWriteType(Type type) throw new ArgumentException(nameof(type), $"Cannot read/write \"{type.FullName}\" objects."); } } + + public static int GetArrayLength(object obj, Type objType, PropertyInfo propertyInfo) + { + int Validate(int value) + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException($"An array property in \"{objType.FullName}\" has an invalid length attribute ({value})"); + } + return value; + } + + int? fixedLength = AttributeValueOrDefault(propertyInfo, typeof(BinaryArrayFixedLengthAttribute), (int?)null); + string variableLength = AttributeValueOrDefault(propertyInfo, typeof(BinaryArrayVariableLengthAttribute), (string)null); + if (fixedLength.HasValue) + { + if (variableLength != null) + { + throw new ArgumentException($"An array property in \"{objType.FullName}\" has two array length attributes. Only one should be provided."); + } + return Validate(fixedLength.Value); + } + if (variableLength is null) + { + throw new MissingMemberException($"An array property in \"{objType.FullName}\" is missing an array length attribute. One should be provided."); + } + PropertyInfo anchor = objType.GetProperty(variableLength, BindingFlags.Instance | BindingFlags.Public); + if (anchor is null) + { + throw new MissingMemberException($"An array property in \"{objType.FullName}\" has an invalid {nameof(BinaryArrayVariableLengthAttribute)} ({variableLength})."); + } + return Validate(Convert.ToInt32(anchor.GetValue(obj))); + } + public static void GetStringLength(object obj, Type objType, PropertyInfo propertyInfo, bool forReads, out bool? nullTerminated, out int stringLength) + { + int Validate(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException($"A string property in \"{objType.FullName}\" has an invalid length attribute ({value})"); + } + return value; + } + + nullTerminated = AttributeValueOrDefault(propertyInfo, typeof(BinaryStringNullTerminatedAttribute), (bool?)null); + int? fixedLength = AttributeValueOrDefault(propertyInfo, typeof(BinaryStringFixedLengthAttribute), (int?)null); + string variableLength = AttributeValueOrDefault(propertyInfo, typeof(BinaryStringVariableLengthAttribute), (string)null); + if (nullTerminated.HasValue) + { + if (fixedLength.HasValue || variableLength != null) + { + throw new ArgumentException($"A string property in \"{objType.FullName}\" has a string length attribute and a {nameof(BinaryStringNullTerminatedAttribute)}; cannot use both."); + } + if (forReads && !nullTerminated.Value) + { + throw new ArgumentException($"A string property in \"{objType.FullName}\" has a {nameof(BinaryStringNullTerminatedAttribute)} that's set to false." + + $" Must use null termination or provide a string length when reading."); + } + stringLength = -1; + return; + } + if (fixedLength.HasValue) + { + if (variableLength != null) + { + throw new ArgumentException($"A string property in \"{objType.FullName}\" has two string length attributes. Only one should be provided."); + } + stringLength = Validate(fixedLength.Value); + return; + } + if (variableLength is null) + { + throw new MissingMemberException($"A string property in \"{objType.FullName}\" is missing a string length attribute and has no {nameof(BinaryStringNullTerminatedAttribute)}. One should be provided."); + } + PropertyInfo anchor = objType.GetProperty(variableLength, BindingFlags.Instance | BindingFlags.Public); + if (anchor is null) + { + throw new MissingMemberException($"A string property in \"{objType.FullName}\" has an invalid {nameof(BinaryStringVariableLengthAttribute)} ({variableLength})."); + } + stringLength = Validate(Convert.ToInt32(anchor.GetValue(obj))); + } } } diff --git a/Testing/BasicTests.cs b/Testing/BasicTests.cs index ecfcb4d..485b88a 100644 --- a/Testing/BasicTests.cs +++ b/Testing/BasicTests.cs @@ -5,9 +5,9 @@ namespace Kermalis.EndianBinaryIOTests { - public sealed class BasicReaderTest + public sealed class BasicTests { - private sealed class MyBasicStruct + private sealed class MyBasicObj { // Properties public ShortSizedEnum Type { get; set; } @@ -68,13 +68,13 @@ private sealed class MyBasicStruct }; [Fact] - public void TestReads() + public void ReadObject() { - MyBasicStruct obj; + MyBasicObj obj; using (var stream = new MemoryStream(_bytes)) - using (var reader = new EndianBinaryReader(stream, Endianness.LittleEndian)) + using (var reader = new EndianBinaryReader(stream, endianness: Endianness.LittleEndian)) { - obj = reader.ReadObject(); + obj = reader.ReadObject(); } Assert.True(obj.Type == ShortSizedEnum.Val2); // Enum works @@ -95,13 +95,44 @@ public void TestReads() } [Fact] - public void TestWrites() + public void ReadManually() + { + MyBasicObj obj; + using (var stream = new MemoryStream(_bytes)) + using (var reader = new EndianBinaryReader(stream, endianness: Endianness.LittleEndian, booleanSize: BooleanSize.U32)) + { + obj = new MyBasicObj(); + + obj.Type = reader.ReadEnum(); + Assert.True(obj.Type == ShortSizedEnum.Val2); // Enum works + obj.Version = reader.ReadInt16(); + Assert.True(obj.Version == 0x1FF); // short works + + obj.ArrayWith16Elements = reader.ReadUInt32s(16); + Assert.True(obj.ArrayWith16Elements.Length == 16); // Fixed size array works + for (uint i = 0; i < 16; i++) + { + Assert.True(obj.ArrayWith16Elements[i] == i); // Array works + } + + obj.Bool32 = reader.ReadBoolean(); + Assert.False(obj.Bool32); // bool32 works + + obj.NullTerminatedASCIIString = reader.ReadStringNullTerminated(EncodingType.ASCII); + Assert.True(obj.NullTerminatedASCIIString == "EndianBinaryIO"); // Stops reading at null terminator + obj.UTF16String = reader.ReadString(10, EncodingType.UTF16); + Assert.True(obj.UTF16String == "Kermalis\0\0"); // Fixed size (10 chars) utf16 + } + } + + [Fact] + public void WriteObject() { byte[] bytes = new byte[107]; using (var stream = new MemoryStream(bytes)) - using (var writer = new EndianBinaryWriter(stream)) + using (var writer = new EndianBinaryWriter(stream, endianness: Endianness.LittleEndian)) { - var obj = new MyBasicStruct + var obj = new MyBasicObj { Type = ShortSizedEnum.Val2, Version = 511, @@ -124,5 +155,42 @@ public void TestWrites() Assert.True(bytes.SequenceEqual(_bytes)); } + + [Fact] + public void WriteManually() + { + byte[] bytes = new byte[107]; + using (var stream = new MemoryStream(bytes)) + using (var writer = new EndianBinaryWriter(stream, endianness: Endianness.LittleEndian, booleanSize: BooleanSize.U32)) + { + var obj = new MyBasicObj + { + Type = ShortSizedEnum.Val2, + Version = 511, + + DoNotReadOrWrite = ByteSizedEnum.Val1, + + ArrayWith16Elements = new uint[16] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + }, + + Bool32 = false, + + NullTerminatedASCIIString = "EndianBinaryIO", + + UTF16String = "Kermalis\0\0" + }; + + writer.Write(obj.Type); + writer.Write(obj.Version); + writer.Write(obj.ArrayWith16Elements); + writer.Write(obj.Bool32); + writer.Write(obj.NullTerminatedASCIIString, true, EncodingType.ASCII); + writer.Write(obj.UTF16String, 10, EncodingType.UTF16); + } + + Assert.True(bytes.SequenceEqual(_bytes)); + } } } diff --git a/Testing/LengthsTests.cs b/Testing/LengthsTests.cs index 2ef0eba..9701515 100644 --- a/Testing/LengthsTests.cs +++ b/Testing/LengthsTests.cs @@ -7,7 +7,7 @@ namespace Kermalis.EndianBinaryIOTests { public sealed class LengthsTests { - private sealed class MyLengthyStruct + private sealed class MyLengthyObj { [BinaryArrayFixedLength(3)] [BinaryEncoding(EncodingType.ASCII)] @@ -40,13 +40,13 @@ private sealed class MyLengthyStruct }; [Fact] - public void TestReads() + public void ReadObject() { - MyLengthyStruct obj; + MyLengthyObj obj; using (var stream = new MemoryStream(_bytes)) using (var reader = new EndianBinaryReader(stream, Endianness.LittleEndian)) { - obj = reader.ReadObject(); + obj = reader.ReadObject(); } Assert.True(obj.NullTerminatedStringArray.Length == 3); // Fixed size array works @@ -66,13 +66,13 @@ public void TestReads() } [Fact] - public void TestWrites() + public void WriteObject() { byte[] bytes = new byte[34]; using (var stream = new MemoryStream(bytes)) using (var writer = new EndianBinaryWriter(stream, Endianness.LittleEndian)) { - writer.Write(new MyLengthyStruct + writer.Write(new MyLengthyObj { NullTerminatedStringArray = new string[3] {