Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the FieldOffset #193

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions BinarySerializer.Test/Offset/BoundOffsetClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
public class BoundOffsetClass
{
[FieldOrder(0)]
public int FieldOffsetField { get; set; }
public uint FieldOffsetField { get; set; }

[FieldOrder(1)]
[FieldOffset("FieldOffsetField")]
public string Field { get; set; }
[FieldOffset(nameof(FieldOffsetField))]
public string FieldString { get; set; }

[FieldOrder(2)]
public uint FieldUint { get; set; }
}
}
17 changes: 17 additions & 0 deletions BinarySerializer.Test/Offset/BoundOffsetCurrentNoRewindClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.IO;

namespace BinarySerialization.Test.Offset
{
public class BoundOffsetCurrentNoRewindClass
{
[FieldOrder(0)]
public uint FieldOffsetField { get; set; }

[FieldOrder(1)]
[FieldOffset(nameof(FieldOffsetField), SeekOrigin.Current, false)]
public uint Field { get; set; }

[FieldOrder(2)]
public uint LastUInt { get; set; }
}
}
28 changes: 28 additions & 0 deletions BinarySerializer.Test/Offset/BoundOffsetJumpyNoRewindClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.IO;

namespace BinarySerialization.Test.Offset
{
public class BoundOffsetJumpyNoRewindClass
{
[FieldOrder(0)]
public uint FieldOffsetField1 { get; set; }

[FieldOrder(1)]
[FieldOffset(nameof(FieldOffsetField1), false)]
public uint Field1 { get; set; }

[FieldOrder(2)]
public uint FieldOffsetField2 { get; set; }

[FieldOrder(3)]
[FieldOffset(nameof(FieldOffsetField2), false)]
public uint Field2 { get; set; }

[FieldOrder(4)]
public uint FieldOffsetField3 { get; set; }

[FieldOrder(5)]
[FieldOffset(nameof(FieldOffsetField3), SeekOrigin.Current, false)]
public uint Field3 { get; set; }
}
}
10 changes: 9 additions & 1 deletion BinarySerializer.Test/Offset/ConstOffsetClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
{
public class ConstOffsetClass
{
[FieldOrder(0)]
public uint FieldStringLength { get; set; }

[FieldOrder(1)]
[FieldOffset(100)]
public string Field { get; set; }
[FieldLength(nameof(FieldStringLength))]
public string FieldString { get; set; }

[FieldOrder(2)]
public uint FieldUint { get; set; }
}
}
34 changes: 29 additions & 5 deletions BinarySerializer.Test/Offset/OffsetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,41 @@ public class OffsetTests : TestBase
[TestMethod]
public void ConstOffsetTest()
{
var expected = new ConstOffsetClass {Field = "FieldValue"};
var actual = Roundtrip(expected, 100 + expected.Field.Length + 1);
Assert.AreEqual(expected.Field, actual.Field);
var stringVal = "FieldValue"; // 10 length
var expected = new ConstOffsetClass {FieldStringLength = (uint)stringVal.Length, FieldString = stringVal, FieldUint = 100};
var actual = Roundtrip(expected, 100 + expected.FieldStringLength);
Assert.AreEqual(expected.FieldStringLength, actual.FieldStringLength);
Assert.AreEqual(expected.FieldString, actual.FieldString);
Assert.AreEqual(expected.FieldUint, actual.FieldUint);
}

[TestMethod]
public void BoundOffsetTest()
{
var expected = new BoundOffsetClass {FieldOffsetField = 1000, Field = "FieldValue"};
var actual = Roundtrip(expected, expected.FieldOffsetField + expected.Field.Length + 1);
var expected = new BoundOffsetClass {FieldOffsetField = 1000, FieldString = "FieldValue", FieldUint = 100};
var actual = Roundtrip(expected, expected.FieldOffsetField + expected.FieldString.Length + 1);
Assert.AreEqual(expected.FieldOffsetField, actual.FieldOffsetField);
Assert.AreEqual(expected.FieldString, actual.FieldString);
Assert.AreEqual(expected.FieldUint, actual.FieldUint);
}

[TestMethod]
public void BoundOffsetCurrentNoRewindTest()
{
var expected = new BoundOffsetCurrentNoRewindClass { FieldOffsetField = 50, Field = 404, LastUInt = 9};
var actual = Roundtrip(expected, sizeof(uint) + expected.FieldOffsetField + sizeof(uint) + sizeof(uint));
Assert.AreEqual(expected.Field, actual.Field);
Assert.AreEqual(expected.LastUInt, actual.LastUInt);
}

[TestMethod]
public void BoundOffsetJumpyNoRewindTest()
{
var expected = new BoundOffsetJumpyNoRewindClass { FieldOffsetField1 = 100, Field1 = 104, FieldOffsetField2 = 200, Field2 = 204, FieldOffsetField3 = sizeof(uint), Field3 = 304 };
var actual = Roundtrip(expected, expected.FieldOffsetField2 + sizeof(uint) + sizeof(uint) + expected.FieldOffsetField3 + sizeof(uint));
Assert.AreEqual(expected.Field1, actual.Field1);
Assert.AreEqual(expected.Field2, actual.Field2);
Assert.AreEqual(expected.Field3, actual.Field3);
}
}
}
47 changes: 43 additions & 4 deletions BinarySerializer/FieldOffsetAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;

namespace BinarySerialization
{
Expand All @@ -11,17 +12,45 @@ public sealed class FieldOffsetAttribute : FieldBindingBaseAttribute, IConstAttr
/// <summary>
/// Initializes a new instance of the FieldOffset attribute with a fixed offset.
/// </summary>
/// <param name="offset"></param>
public FieldOffsetAttribute(ulong offset)
/// <param name="offset">Offset position from <see cref="SeekOrigin"/></param>
/// <param name="seekOrigin">Specifies the position in a stream to use for seeking the field</param>
/// <param name="rewind">If true it will seek back to position where it was before seek, otherwise stream will continue from the current position</param>
public FieldOffsetAttribute(ulong offset, SeekOrigin seekOrigin = SeekOrigin.Begin, bool rewind = true)
{
ConstOffset = offset;
SeekOrigin = seekOrigin;
Rewind = rewind;
}

/// <summary>
/// Initializes a new instance of the FieldOffset attribute with a fixed offset.
/// </summary>
/// <param name="offset">Offset position from <see cref="SeekOrigin"/></param>
/// <param name="rewind">If true it will seek back to position where it was before seek, otherwise stream will continue from the current position</param>
/// <param name="seekOrigin">Specifies the position in a stream to use for seeking the field</param>
public FieldOffsetAttribute(ulong offset, bool rewind, SeekOrigin seekOrigin = SeekOrigin.Begin) : this(offset, seekOrigin, rewind)
{
}

/// <summary>
/// Initializes a new instance of the FieldOffset attribute with a path pointing to a source binding member.
/// </summary>
/// <param name="path">A path to the source member</param>
/// <param name="seekOrigin">Specifies the position in a stream to use for seeking the field</param>
/// <param name="rewind">If true it will seek back to position where it was before seek, otherwise stream will continue from the current position</param>
public FieldOffsetAttribute(string path, SeekOrigin seekOrigin = SeekOrigin.Begin, bool rewind = true) : base(path)
{
SeekOrigin = seekOrigin;
Rewind = rewind;
}

/// <summary>
/// Initializes a new instance of the FieldOffset attribute with a path pointing to a source binding member.
/// </summary>
/// <param name="path">A path to the source member.</param>
public FieldOffsetAttribute(string path) : base(path)
/// <param name="path">A path to the source member</param>
/// <param name="rewind">If true it will seek back to position where it was before seek, otherwise stream will continue from the current position</param>
/// <param name="seekOrigin">Specifies the position in a stream to use for seeking the field</param>
public FieldOffsetAttribute(string path, bool rewind, SeekOrigin seekOrigin = SeekOrigin.Begin) : this(path, seekOrigin, rewind)
{
}

Expand All @@ -37,5 +66,15 @@ public object GetConstValue()
{
return ConstOffset;
}

/// <summary>
/// Specifies the position in a stream to use for seeking the field
/// </summary>
public SeekOrigin SeekOrigin { get; set; }

/// <summary>
/// If true it will seek back to position where it was before seek, otherwise stream will continue from the current position
/// </summary>
public bool Rewind { get; set; }
}
}
11 changes: 11 additions & 0 deletions BinarySerializer/Graph/TypeGraph/TypeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
Expand Down Expand Up @@ -142,6 +143,13 @@ protected TypeNode(TypeNode parent, Type parentType, MemberInfo memberInfo, Type
Order = fieldOrderAttribute.Order;
}

var offsetAttribute = attributes.OfType<FieldOffsetAttribute>().SingleOrDefault();
if (offsetAttribute != null)
{
OffsetSeekOrigin = offsetAttribute.SeekOrigin;
OffsetRewind = offsetAttribute.Rewind;
}

var serializeAsAttribute = attributes.OfType<SerializeAsAttribute>().SingleOrDefault();
if (serializeAsAttribute != null)
{
Expand Down Expand Up @@ -362,6 +370,9 @@ protected TypeNode(TypeNode parent, Type parentType, MemberInfo memberInfo, Type

public int? Order { get; }

public SeekOrigin OffsetSeekOrigin { get; } = SeekOrigin.Begin;
public bool OffsetRewind { get; } = true;

public bool AreStringsTerminated { get; }

public char StringTerminator { get; }
Expand Down
41 changes: 24 additions & 17 deletions BinarySerializer/Graph/ValueGraph/ValueNode.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -206,10 +206,12 @@ internal void Serialize(BoundedStream stream, EventShuttle eventShuttle, bool al

if (offset != null)
{
using (new StreamResetter(stream))
var rewindPosition = stream.Position;
stream.Seek(offset.Value, TypeNode.OffsetSeekOrigin);
SerializeInternal(stream, GetConstFieldLength, eventShuttle, measuring);
if (TypeNode.OffsetRewind)
{
stream.Position = offset.Value;
SerializeInternal(stream, GetConstFieldLength, eventShuttle, measuring);
stream.Position = rewindPosition;
}
}
else
Expand Down Expand Up @@ -252,14 +254,15 @@ internal async Task SerializeAsync(BoundedStream stream, EventShuttle eventShutt
}

var offset = GetFieldOffset();

if (offset != null)
{
using (new StreamResetter(stream))
{
stream.Position = offset.Value;
await SerializeInternalAsync(stream, GetConstFieldLength, eventShuttle, cancellationToken)
var rewindPosition = stream.Position;
stream.Seek(offset.Value, TypeNode.OffsetSeekOrigin);
await SerializeInternalAsync(stream, GetConstFieldLength, eventShuttle, cancellationToken)
.ConfigureAwait(false);
if (TypeNode.OffsetRewind)
{
stream.Position = rewindPosition;
}
}
else
Expand Down Expand Up @@ -312,10 +315,12 @@ internal void Deserialize(BoundedStream stream, SerializationOptions options, Ev

if (offset != null)
{
using (new StreamResetter(stream))
var rewindPosition = stream.Position;
stream.Seek(offset.Value, TypeNode.OffsetSeekOrigin);
DeserializeInternal(stream, GetFieldLength, options, eventShuttle);
if (TypeNode.OffsetRewind)
{
stream.Position = offset.Value;
DeserializeInternal(stream, GetFieldLength, options, eventShuttle);
stream.Position = rewindPosition;
}
}
else
Expand Down Expand Up @@ -358,11 +363,13 @@ internal async Task DeserializeAsync(BoundedStream stream, SerializationOptions

if (offset != null)
{
using (new StreamResetter(stream))
var rewindPosition = stream.Position;
stream.Seek(offset.Value, TypeNode.OffsetSeekOrigin);
await DeserializeInternalAsync(stream, GetFieldLength, options, eventShuttle, cancellationToken)
.ConfigureAwait(false);
if (TypeNode.OffsetRewind)
{
stream.Position = offset.Value;
await DeserializeInternalAsync(stream, GetFieldLength, options, eventShuttle, cancellationToken)
.ConfigureAwait(false);
stream.Position = rewindPosition;
}
}
else
Expand Down Expand Up @@ -951,4 +958,4 @@ private BinarySerializationContext CreateSerializationContext()
parent?.CreateSerializationContext(), TypeNode.MemberInfo);
}
}
}
}
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,75 @@ The FieldCrc32 is identical to the FieldCrc16 with the difference that it operat

### FieldOffsetAttribute ###

The FieldOffset attribute should be used sparingly but can be used if an absolute offset is required. In most cases implicit offset (e.g. just define the structure) is preferable. After moving to the offset the serializer will reset to the origin so subsequent fields must manage their own offsets. This attribute is not supported when serializing to non-seekable streams.
The FieldOffset attribute should be used sparingly but can be used if an absolute(default) or incremental offset is required.
In most cases implicit offset (e.g. just define the structure) is preferable.
By default and after moving to the offset the serializer will reset to the origin so subsequent fields must manage their own offsets, otherwise set `Rewind` to `false`.
This attribute is not supported when serializing to non-seekable streams.

**Note:** `SeekOrigin.End` is highly inadvisable since you don't have the stream total size while serialization.
If you want to use the `SeekOrigin.End` make sure it's only to deserialize a object or else you will need to set the correct size first with `stream.SetLength()`.

```c#
public class FileSpec
{
[FieldOrder(0)]
[FieldLength(12)]
[SerializeAs(SerializedType.TerminatedString)]
public string FileVersion { get; set; } = "FileMarking";

[FieldOrder(1)]
public uint FileVersion { get; set; }

[FieldOrder(2)]
public uint HeaderAddress { get; set; }

[FieldOrder(3)]
public uint SettingsAddress { get; set; }

[FieldOrder(4)]
public uint PreviewAddress { get; set; }

[FieldOrder(5)]
public uint LayersAddress { get; set; }

[FieldOrder(6)]
[FieldOffset(nameof(HeaderAddress), false)]
public uint HeaderField1 { get; set; }

[FieldOrder(7)]
public uint HeaderField2 { get; set; }

[FieldOrder(8)]
public uint HeaderField3 { get; set; }

[FieldOrder(9)]
[FieldOffset(nameof(SettingsAddress), false)]
public uint SettingsField1 { get; set; }

[FieldOrder(10)]
public uint SettingsField2 { get; set; }

[FieldOrder(11)]
[FieldOffset(nameof(PreviewAddress), false)]
public uint PreviewSize { get; set; }

[FieldOrder(12)]
[FieldCount(nameof(PreviewSize))]
public byte[] PreviewData {get; set; }

[FieldOrder(13)]
[FieldOffset(nameof(LayersAddress), false)]
public uint LayerCount { get; set; }

[FieldOrder(14)]
[FieldCount(nameof(LayerCount))]
public Layer[] Layers { get; set; }

[FieldOrder(15)]
[FieldOffset(4, SeekOrigin.Current, false)] // Skip unused 4 bytes first, same as declaring uint Padding
public uint FileChecksum { get; set; }
}
```

### SubtypeAttribute ###

Expand Down