diff --git a/BinarySerializer.Test/Offset/BoundOffsetClass.cs b/BinarySerializer.Test/Offset/BoundOffsetClass.cs index cc5c5092..2bbde837 100644 --- a/BinarySerializer.Test/Offset/BoundOffsetClass.cs +++ b/BinarySerializer.Test/Offset/BoundOffsetClass.cs @@ -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; } } } \ No newline at end of file diff --git a/BinarySerializer.Test/Offset/BoundOffsetCurrentNoRewindClass.cs b/BinarySerializer.Test/Offset/BoundOffsetCurrentNoRewindClass.cs new file mode 100644 index 00000000..c26e209a --- /dev/null +++ b/BinarySerializer.Test/Offset/BoundOffsetCurrentNoRewindClass.cs @@ -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; } +} +} \ No newline at end of file diff --git a/BinarySerializer.Test/Offset/BoundOffsetJumpyNoRewindClass.cs b/BinarySerializer.Test/Offset/BoundOffsetJumpyNoRewindClass.cs new file mode 100644 index 00000000..d0daaca2 --- /dev/null +++ b/BinarySerializer.Test/Offset/BoundOffsetJumpyNoRewindClass.cs @@ -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; } + } +} \ No newline at end of file diff --git a/BinarySerializer.Test/Offset/ConstOffsetClass.cs b/BinarySerializer.Test/Offset/ConstOffsetClass.cs index 0b2cdc1b..1e0b129f 100644 --- a/BinarySerializer.Test/Offset/ConstOffsetClass.cs +++ b/BinarySerializer.Test/Offset/ConstOffsetClass.cs @@ -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; } } } \ No newline at end of file diff --git a/BinarySerializer.Test/Offset/OffsetTests.cs b/BinarySerializer.Test/Offset/OffsetTests.cs index b810a63b..e4ca1a4a 100644 --- a/BinarySerializer.Test/Offset/OffsetTests.cs +++ b/BinarySerializer.Test/Offset/OffsetTests.cs @@ -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); } } } \ No newline at end of file diff --git a/BinarySerializer/FieldOffsetAttribute.cs b/BinarySerializer/FieldOffsetAttribute.cs index df8fd804..e04b2954 100644 --- a/BinarySerializer/FieldOffsetAttribute.cs +++ b/BinarySerializer/FieldOffsetAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.IO; namespace BinarySerialization { @@ -11,17 +12,45 @@ public sealed class FieldOffsetAttribute : FieldBindingBaseAttribute, IConstAttr /// /// Initializes a new instance of the FieldOffset attribute with a fixed offset. /// - /// - public FieldOffsetAttribute(ulong offset) + /// Offset position from + /// Specifies the position in a stream to use for seeking the field + /// If true it will seek back to position where it was before seek, otherwise stream will continue from the current position + public FieldOffsetAttribute(ulong offset, SeekOrigin seekOrigin = SeekOrigin.Begin, bool rewind = true) { ConstOffset = offset; + SeekOrigin = seekOrigin; + Rewind = rewind; + } + + /// + /// Initializes a new instance of the FieldOffset attribute with a fixed offset. + /// + /// Offset position from + /// If true it will seek back to position where it was before seek, otherwise stream will continue from the current position + /// Specifies the position in a stream to use for seeking the field + public FieldOffsetAttribute(ulong offset, bool rewind, SeekOrigin seekOrigin = SeekOrigin.Begin) : this(offset, seekOrigin, rewind) + { + } + + /// + /// Initializes a new instance of the FieldOffset attribute with a path pointing to a source binding member. + /// + /// A path to the source member + /// Specifies the position in a stream to use for seeking the field + /// If true it will seek back to position where it was before seek, otherwise stream will continue from the current position + public FieldOffsetAttribute(string path, SeekOrigin seekOrigin = SeekOrigin.Begin, bool rewind = true) : base(path) + { + SeekOrigin = seekOrigin; + Rewind = rewind; } /// /// Initializes a new instance of the FieldOffset attribute with a path pointing to a source binding member. /// - /// A path to the source member. - public FieldOffsetAttribute(string path) : base(path) + /// A path to the source member + /// If true it will seek back to position where it was before seek, otherwise stream will continue from the current position + /// Specifies the position in a stream to use for seeking the field + public FieldOffsetAttribute(string path, bool rewind, SeekOrigin seekOrigin = SeekOrigin.Begin) : this(path, seekOrigin, rewind) { } @@ -37,5 +66,15 @@ public object GetConstValue() { return ConstOffset; } + + /// + /// Specifies the position in a stream to use for seeking the field + /// + public SeekOrigin SeekOrigin { get; set; } + + /// + /// If true it will seek back to position where it was before seek, otherwise stream will continue from the current position + /// + public bool Rewind { get; set; } } } \ No newline at end of file diff --git a/BinarySerializer/Graph/TypeGraph/TypeNode.cs b/BinarySerializer/Graph/TypeGraph/TypeNode.cs index feb87905..6193f003 100644 --- a/BinarySerializer/Graph/TypeGraph/TypeNode.cs +++ b/BinarySerializer/Graph/TypeGraph/TypeNode.cs @@ -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; @@ -142,6 +143,13 @@ protected TypeNode(TypeNode parent, Type parentType, MemberInfo memberInfo, Type Order = fieldOrderAttribute.Order; } + var offsetAttribute = attributes.OfType().SingleOrDefault(); + if (offsetAttribute != null) + { + OffsetSeekOrigin = offsetAttribute.SeekOrigin; + OffsetRewind = offsetAttribute.Rewind; + } + var serializeAsAttribute = attributes.OfType().SingleOrDefault(); if (serializeAsAttribute != null) { @@ -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; } diff --git a/BinarySerializer/Graph/ValueGraph/ValueNode.cs b/BinarySerializer/Graph/ValueGraph/ValueNode.cs index 6f3d1f3c..0a43c205 100644 --- a/BinarySerializer/Graph/ValueGraph/ValueNode.cs +++ b/BinarySerializer/Graph/ValueGraph/ValueNode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -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 @@ -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 @@ -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 @@ -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 @@ -951,4 +958,4 @@ private BinarySerializationContext CreateSerializationContext() parent?.CreateSerializationContext(), TypeNode.MemberInfo); } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 419c819a..5afb173b 100644 --- a/README.md +++ b/README.md @@ -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 ###