Skip to content

Commit

Permalink
Merge pull request #28 from Kermalis/modern-C#
Browse files Browse the repository at this point in the history
  • Loading branch information
Kermalis authored Aug 30, 2022
2 parents d06dbb8 + 3c50ae1 commit f7581f8
Show file tree
Hide file tree
Showing 18 changed files with 3,172 additions and 3,411 deletions.
172 changes: 91 additions & 81 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@
[![NuGet](https://img.shields.io/nuget/v/EndianBinaryIO.svg)](https://www.nuget.org/packages/EndianBinaryIO)
[![NuGet 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 to streams using specified endianness, string encoding, and boolean sizes.
Objects can also be read from/written to streams via reflection and attributes.
This .NET library provides a simple API to read/write bytes from/to streams and spans using user-specified endianness.
By default, supported types include primitives, enums, arrays, strings, and some common .NET struct types.
Objects can also be read/written from/to streams via reflection and attributes.
The developer can use the API even if their target behavior or data is not directly supported by using the `IBinarySerializable` interface, inheritting from the reader/writer, or using the manual `Span<T>`/`ReadOnlySpan<T>` methods without streams.
Performance is the focus when not using reflection; no allocations unless absolutely necessary!

The `IBinarySerializable` interface allows an object to be read and written in a customizable fashion.
The `IBinarySerializable` interface allows an object to be read and written in a customizable fashion during reflection.
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.

There is also an `EndianBitConverter` static class which resembles `System.BitConverter`. With it you can convert to/from data types using arrays rather than streams, all with specific endianness.
The `EndianBinaryPrimitives` static class which resembles `System.Buffers.Binary.BinaryPrimitives` is an API that converts to/from data types using `Span<T>`/`ReadOnlySpan<T>` with specific endianness, rather than streams.

----
## Changelog For v2.0.0.0
Be sure to check the comment at https://github.com/Kermalis/EndianBinaryIO/pull/28!

----
## 🚀 Usage:
Expand All @@ -23,47 +30,46 @@ Assume we have the following definitions:
```cs
enum ByteSizedEnum : byte
{
Val1 = 0x20,
Val2 = 0x80
Val1 = 0x20,
Val2 = 0x80,
}
enum ShortSizedEnum : short
{
Val1 = 0x40,
Val2 = 0x800
Val1 = 0x40,
Val2 = 0x800,
}

class MyBasicObj
{
// Properties
public ShortSizedEnum Type { get; set; }
public short Version { get; set; }
public DateTime Date { get; set; }

// Property that is ignored when reading and writing
[BinaryIgnore(true)]
public ByteSizedEnum DoNotReadOrWrite { get; set; }

// 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("ASCII")]
[BinaryStringNullTerminated(true)]
public string NullTerminatedASCIIString { get; set; }

// String encoded in UTF16-LE that will only read/write 10 chars
// The BinaryStringTrimNullTerminatorsAttribute will indicate that every char from the first \0 will be removed from the string. This attribute also works with char arrays
[BinaryEncoding("UTF-16")]
[BinaryStringFixedLength(10)]
[BinaryStringTrimNullTerminators(true)]
public string UTF16String { get; set; }
// Properties
public ShortSizedEnum Type { get; set; }
public short Version { get; set; }
public DateTime Date { get; set; }

// Property that is ignored when reading and writing
[BinaryIgnore]
public ByteSizedEnum DoNotReadOrWrite { get; set; }

// 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
[BinaryASCII]
[BinaryStringNullTerminated]
public string NullTerminatedASCIIString { get; set; }

// String encoded in UTF16-LE that will only read/write 10 chars
// The BinaryStringTrimNullTerminatorsAttribute will indicate that every char from the first \0 will be removed from the string. This attribute also works with char arrays
[BinaryStringFixedLength(10)]
[BinaryStringTrimNullTerminators]
public string UTF16String { get; set; }
}
```
And assume these are our input bytes (in little endian):
Expand Down Expand Up @@ -107,89 +113,93 @@ obj.Type = reader.ReadEnum<ShortSizedEnum>(); // Reads the enum type based on th
obj.Version = reader.ReadInt16(); // Reads a 'short' (2 bytes)
obj.Date = reader.ReadDateTime(); // Reads a 'DateTime' (8 bytes)
obj.ArrayWith16Elements = reader.ReadUInt32s(16); // Reads 16 'uint's (4 bytes each)
obj.ArrayWith16Elements = new uint[16];
reader.ReadUInt32s(obj.ArrayWith16Elements); // Reads 16 'uint's (4 bytes each)
obj.Bool32 = reader.ReadBoolean(); // Reads a 'bool' (4 bytes in this case, since the reader was initiated with a default of BooleanSize.U32, but there is an overload to pass in one)
obj.Bool32 = reader.ReadBoolean(); // Reads a 'bool' (4 bytes in this case, since the reader's current bool state is BooleanSize.U32)
obj.NullTerminatedASCIIString = reader.ReadStringNullTerminated(Encoding.ASCII); // Reads ASCII chars until a '\0' is read, then returns a 'string'
obj.UTF16String = reader.ReadString(10, true, Encoding.Unicode); // Reads 10 UTF16-LE chars as a 'string' with the '\0's removed
reader.ASCII = true; // Set the reader's ASCII state to true
obj.NullTerminatedASCIIString = reader.ReadString_NullTerminated(); // Reads ASCII chars until a '\0' is read, then returns a 'string'
reader.ASCII = false; // Set the reader's ASCII state to false (UTF16-LE)
obj.UTF16String = reader.ReadString_Count_TrimNullTerminators(10); // Reads 10 UTF16-LE chars as a 'string' with the '\0's removed
```
### Reading Automatically (With Reflection):
```cs
var reader = new EndianBinaryReader(stream, endianness: Endianness.LittleEndian);
var obj = reader.ReadObject<MyBasicObj>(); // Create a 'MyBasicObj' and read all properties in order, ignoring any with a 'BinaryIgnoreAttribute'
// Other objects that are properties in this object will also be read in the same way recursively
// Other objects that are properties in this object will also be read in the same way recursively
```

### Writing Manually:
```cs
var obj = new MyBasicObj
{
Type = ShortSizedEnum.Val2,
Version = 511,
Date = new DateTime(1998, 12, 30),
Type = ShortSizedEnum.Val2,
Version = 511,
Date = new DateTime(1998, 12, 30),

DoNotReadOrWrite = ByteSizedEnum.Val1,
DoNotReadOrWrite = ByteSizedEnum.Val1,

ArrayWith16Elements = new uint[16]
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
},
ArrayWith16Elements = new uint[16]
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
},

Bool32 = false,
Bool32 = false,

NullTerminatedASCIIString = "EndianBinaryIO",
UTF16String = "Kermalis"
NullTerminatedASCIIString = "EndianBinaryIO",
UTF16String = "Kermalis",
};

var writer = new EndianBinaryWriter(stream, endianness: Endianness.LittleEndian, booleanSize: BooleanSize.U32);
writer.Write(obj.Type); // Writes the enum type based on the amount of bytes of the enum's underlying type (short/2 in this case)
writer.Write(obj.Version); // Writes a 'short' (2 bytes)
writer.Write(obj.Date); // Writes a 'DateTime' (8 bytes)
writer.Write(obj.ArrayWith16Elements); // Writes 16 'uint's (4 bytes each)
writer.Write(obj.Bool32); // Writes a 'bool' (4 bytes in this case, since the reader was initiated with a default of BooleanSize.U32, but there is an overload to pass in one)
writer.Write(obj.NullTerminatedASCIIString, true, Encoding.ASCII); // Writes the chars in the 'string' as ASCII and appends a '\0' at the end
writer.Write(obj.UTF16String, 10, Encoding.Unicode); // Writes 10 UTF16-LE chars as a 'string'. If the string has more than 10 chars, it is truncated; if it has less, it is padded with '\0'
writer.WriteEnum(obj.Type); // Writes the enum type based on the amount of bytes of the enum's underlying type (short/2 in this case)
writer.WriteInt16(obj.Version); // Writes a 'short' (2 bytes)
writer.WriteDateTime(obj.Date); // Writes a 'DateTime' (8 bytes)
writer.WriteUInt32s(obj.ArrayWith16Elements); // Writes 16 'uint's (4 bytes each)
writer.WriteBoolean(obj.Bool32); // Writes a 'bool' (4 bytes in this case, since the reader's current bool state is BooleanSize.U32)
writer.ASCII = true; // Set the reader's ASCII state to true
writer.WriteChars_NullTerminated(obj.NullTerminatedASCIIString); // Writes the chars in the 'string' as ASCII and appends a '\0' at the end
writer.ASCII = false; // Set the reader's ASCII state to false (UTF16-LE)
writer.WriteChars_Count(obj.UTF16String, 10); // Writes 10 UTF16-LE chars as a 'string'. If the string has more than 10 chars, it is truncated; if it has less, it is padded with '\0'
```
### Writing Automatically (With Reflection):
```cs
var obj = new MyBasicObj
{
Type = ShortSizedEnum.Val2,
Version = 511,
Date = new DateTime(1998, 12, 30),
Type = ShortSizedEnum.Val2,
Version = 511,
Date = new DateTime(1998, 12, 30),

DoNotReadOrWrite = ByteSizedEnum.Val1,
DoNotReadOrWrite = ByteSizedEnum.Val1,

ArrayWith16Elements = new uint[16]
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
},
ArrayWith16Elements = new uint[16]
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
},

Bool32 = false,
Bool32 = false,

NullTerminatedASCIIString = "EndianBinaryIO",
UTF16String = "Kermalis"
NullTerminatedASCIIString = "EndianBinaryIO",
UTF16String = "Kermalis",
};

var writer = new EndianBinaryWriter(stream, endianness: Endianness.LittleEndian);
writer.Write(obj); // Write all properties in the 'MyBasicObj' in order, ignoring any with a 'BinaryIgnoreAttribute'
// Other objects that are properties in this object will also be written in the same way recursively
// Other objects that are properties in this object will also be written in the same way recursively
```

### EndianBitConverter Example:
### EndianBinaryPrimitives Example:
```cs
byte[] bytes = new byte[] { 0xFF, 0x00, 0x00, 0x00 };
uint value = (uint)EndianBitConverter.BytesToInt32(bytes, 0, Endianness.LittleEndian); // Will return (int)255
byte[] bytes = new byte[] { 0xFF, 0x00, 0x00, 0x00, 0xBB, 0xEE, 0xEE, 0xFF };
uint value = EndianBinaryPrimitives.ReadUInt32(bytes, Endianness.LittleEndian); // Will return 255
value = 128;
bytes = EndianBitConverter.Int32ToBytes((int)value, Endianness.LittleEndian); // Will return (byte[]){ 0x80, 0x00, 0x00, 0x00 }
EndianBinaryPrimitives.WriteUInt32(bytes.AsSpan(4, 4), value, Endianness.LittleEndian); // bytes is now { 0xFF, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00 }
```

----
## To Do:
* Documentation

----
## EndianBinaryIOTests Uses:
* [xUnit.net](https://github.com/xunit/xunit)
Loading

0 comments on commit f7581f8

Please sign in to comment.