From cb1369c3041758c6d54894ee955a31c16c3d371b Mon Sep 17 00:00:00 2001 From: Bevan Weiss Date: Sun, 21 May 2023 01:33:15 +1000 Subject: [PATCH] Add new SerializeAs option for TerminatedSizedString. This allows to keep the existing TerminatedString behaviour when a field length constraint is applied, where it essentially becomes a SizedString. Whilst also allowing new behaviour that would keep the terminated string aspect, but truncate / pad the terminated string around the length constraint Test case shows non-standard terminator (\n or 0x0A), and non-standard padding byte 0x0D just to prove that these work as expected also. Possibly fixes #171 (confirmation required) Signed-off-by: Bevan Weiss --- .../SerializeAs/SerializeAsTest.cs | 19 ++++ .../SerializeAs/TerminatedSizedStringClass.cs | 9 ++ .../Graph/TypeGraph/EnumTypeNode.cs | 3 +- BinarySerializer/Graph/TypeGraph/TypeNode.cs | 7 +- .../Graph/ValueGraph/ValueValueNode.cs | 98 ++++++++++++++----- BinarySerializer/SerializedType.cs | 8 +- 6 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 BinarySerializer.Test/SerializeAs/TerminatedSizedStringClass.cs diff --git a/BinarySerializer.Test/SerializeAs/SerializeAsTest.cs b/BinarySerializer.Test/SerializeAs/SerializeAsTest.cs index 9a9e4a3b..2319ab45 100644 --- a/BinarySerializer.Test/SerializeAs/SerializeAsTest.cs +++ b/BinarySerializer.Test/SerializeAs/SerializeAsTest.cs @@ -50,5 +50,24 @@ public void CollectionPaddingValue() var actualItems = actual.Items.Select(i => i.Trim()).ToList(); CollectionAssert.AreEqual(expected.Items, actualItems); } + + [TestMethod] + public void SerializeAsTerminatedStringWithPadding() + { + var expected = new TerminatedSizedStringClass { Value = "hi" }; + var actual = Roundtrip(expected, new byte[] { 0x68, 0x69, 0x0A, 0x0D, 0x0D }); + + Assert.AreEqual(expected.Value, actual.Value); + } + + [TestMethod] + public void SerializeAsTerminatedStringWithTruncation() + { + var expected = new TerminatedSizedStringClass { Value = "hi test" }; + var actual = Roundtrip(expected, new byte[] { 0x68, 0x69, 0x20, 0x74, 0x0A }); + + // we expect to have truncated to only have 4 characters (and the terminator) + Assert.AreEqual(expected.Value.Substring(0,4), actual.Value); + } } } \ No newline at end of file diff --git a/BinarySerializer.Test/SerializeAs/TerminatedSizedStringClass.cs b/BinarySerializer.Test/SerializeAs/TerminatedSizedStringClass.cs new file mode 100644 index 00000000..4fe7eece --- /dev/null +++ b/BinarySerializer.Test/SerializeAs/TerminatedSizedStringClass.cs @@ -0,0 +1,9 @@ +namespace BinarySerialization.Test.SerializeAs +{ + class TerminatedSizedStringClass + { + [FieldLength(5)] + [SerializeAs(SerializedType.TerminatedSizedString, StringTerminator = '\n', PaddingValue = 0x0D)] + public string Value { get; set; } + } +} diff --git a/BinarySerializer/Graph/TypeGraph/EnumTypeNode.cs b/BinarySerializer/Graph/TypeGraph/EnumTypeNode.cs index 87dcccff..23212d5e 100644 --- a/BinarySerializer/Graph/TypeGraph/EnumTypeNode.cs +++ b/BinarySerializer/Graph/TypeGraph/EnumTypeNode.cs @@ -46,7 +46,8 @@ private void InitializeEnumValues() if (enumAttributes.Any(enumAttribute => enumAttribute.Value != null) || serializedType == SerializedType.TerminatedString || serializedType == SerializedType.SizedString || - serializedType == SerializedType.LengthPrefixedString) + serializedType == SerializedType.LengthPrefixedString || + serializedType == SerializedType.TerminatedSizedString) { EnumInfo.EnumValues = enumAttributes.ToDictionary(enumAttribute => enumAttribute.Key, enumAttribute => diff --git a/BinarySerializer/Graph/TypeGraph/TypeNode.cs b/BinarySerializer/Graph/TypeGraph/TypeNode.cs index feb87905..36c34742 100644 --- a/BinarySerializer/Graph/TypeGraph/TypeNode.cs +++ b/BinarySerializer/Graph/TypeGraph/TypeNode.cs @@ -48,6 +48,7 @@ internal abstract class TypeNode : Node {SerializedType.TerminatedString, default(string)}, {SerializedType.SizedString, default(string)}, {SerializedType.LengthPrefixedString, default(string)}, + {SerializedType.TerminatedSizedString, default(string)}, {SerializedType.ByteArray, default(byte[])} }; @@ -154,7 +155,8 @@ protected TypeNode(TypeNode parent, Type parentType, MemberInfo memberInfo, Type } #pragma warning restore 618 - if (_serializedType.Value == SerializedType.TerminatedString) + if (_serializedType.Value == SerializedType.TerminatedString || + _serializedType.Value == SerializedType.TerminatedSizedString) { AreStringsTerminated = true; StringTerminator = serializeAsAttribute.StringTerminator; @@ -170,7 +172,8 @@ protected TypeNode(TypeNode parent, Type parentType, MemberInfo memberInfo, Type IsNullable = serializedType == SerializedType.Default || serializedType == SerializedType.ByteArray || serializedType == SerializedType.TerminatedString || - serializedType == SerializedType.SizedString; + serializedType == SerializedType.SizedString || + serializedType == SerializedType.TerminatedSizedString; } // setup bindings diff --git a/BinarySerializer/Graph/ValueGraph/ValueValueNode.cs b/BinarySerializer/Graph/ValueGraph/ValueValueNode.cs index edd1607b..666da3b2 100644 --- a/BinarySerializer/Graph/ValueGraph/ValueValueNode.cs +++ b/BinarySerializer/Graph/ValueGraph/ValueValueNode.cs @@ -133,16 +133,19 @@ public void Serialize(AsyncBinaryWriter writer, object value, SerializedType ser // see if we're dealing with something that needs to be padded if (serializedType != SerializedType.ByteArray && - serializedType != SerializedType.TerminatedString) + serializedType != SerializedType.TerminatedString && + serializedType != SerializedType.TerminatedSizedString) { return; } var data = new byte[constLength.TotalByteCount]; - if (serializedType == SerializedType.TerminatedString && data.Length > 0) + if ((serializedType == SerializedType.TerminatedString || + serializedType == SerializedType.TerminatedSizedString) + && data.Length > 0) { - var terminatorData = GetNullTermination(); + var terminatorData = GetTermination(); Array.Copy(terminatorData, data, terminatorData.Length); } @@ -224,7 +227,7 @@ public void Serialize(AsyncBinaryWriter writer, object value, SerializedType ser var encoding = GetFieldEncoding(); var data = encoding.GetBytes(value.ToString()); - var dataLength = GetNullTerminatedArrayLength(data, constLength, maxLength); + var dataLength = GetTerminatedArrayLength(data, constLength, maxLength); writer.Write(data, dataLength); @@ -234,7 +237,8 @@ public void Serialize(AsyncBinaryWriter writer, object value, SerializedType ser } case SerializedType.SizedString: { - var data = GetFieldEncoding().GetBytes(value.ToString()); + var encoding = GetFieldEncoding(); + var data = encoding.GetBytes(value.ToString()); var dataLength = GetArrayLength(data, constLength, maxLength); @@ -251,6 +255,25 @@ public void Serialize(AsyncBinaryWriter writer, object value, SerializedType ser writer.Write(value.ToString()); break; } + case SerializedType.TerminatedSizedString: + { + var encoding = GetFieldEncoding(); + var data = encoding.GetBytes(value.ToString()); + + var dataLength = GetTerminatedArrayLength(data, constLength, maxLength); + + writer.Write(data, dataLength); + + var stringTerminatorData = encoding.GetBytes(new[] { TypeNode.StringTerminator }); + writer.Write(stringTerminatorData, stringTerminatorData.Length); + + var fillLength = maxLength < constLength ? maxLength : constLength; + var padLength = fillLength - (dataLength + stringTerminatorData.Length); + if (padLength > 0) + writer.Write(GetFieldPaddingValue(), padLength); + + break; + } default: throw new NotSupportedException(TypeNotSupportedMessage); @@ -286,16 +309,19 @@ public async Task SerializeAsync(AsyncBinaryWriter writer, object value, Seriali // see if we're dealing with something that needs to be padded if (serializedType != SerializedType.ByteArray && - serializedType != SerializedType.TerminatedString) + serializedType != SerializedType.TerminatedString && + serializedType != SerializedType.TerminatedSizedString) { return; } var data = new byte[constLength.TotalByteCount]; - if (serializedType == SerializedType.TerminatedString && data.Length > 0) + if ((serializedType == SerializedType.TerminatedString || + serializedType == SerializedType.TerminatedSizedString) + && data.Length > 0) { - var terminatorData = GetNullTermination(); + var terminatorData = GetTermination(); Array.Copy(terminatorData, data, terminatorData.Length); } @@ -376,11 +402,11 @@ public async Task SerializeAsync(AsyncBinaryWriter writer, object value, Seriali { var data = GetFieldEncoding().GetBytes(value.ToString()); - var dataLength = GetNullTerminatedArrayLength(data, constLength, maxLength); + var dataLength = GetTerminatedArrayLength(data, constLength, maxLength); await writer.WriteAsync(data, dataLength, cancellationToken).ConfigureAwait(false); - var stringTerminatorData = GetNullTermination(); + var stringTerminatorData = GetTermination(); await writer.WriteAsync(TypeNode.StringTerminator, stringTerminatorData.Length, cancellationToken) .ConfigureAwait(false); @@ -404,6 +430,25 @@ await writer.WriteAsync(TypeNode.StringTerminator, stringTerminatorData.Length, writer.Write(value.ToString()); break; } + case SerializedType.TerminatedSizedString: + { + var encoding = GetFieldEncoding(); + var data = encoding.GetBytes(value.ToString()); + + var dataLength = GetTerminatedArrayLength(data, constLength, maxLength); + + await writer.WriteAsync(data, dataLength, cancellationToken); + + var stringTerminatorData = encoding.GetBytes(new[] { TypeNode.StringTerminator }); + await writer.WriteAsync(stringTerminatorData, stringTerminatorData.Length, cancellationToken); + + var fillLength = maxLength < constLength ? maxLength : constLength; + var padLength = fillLength - (dataLength + stringTerminatorData.Length); + if (padLength > 0) + await writer.WriteAsync(GetFieldPaddingValue(), padLength, cancellationToken); + + break; + } default: @@ -467,7 +512,8 @@ public void Deserialize(BinaryReader reader, SerializedType serializedType, Fiel break; } case SerializedType.TerminatedString: - { + case SerializedType.TerminatedSizedString: + { var data = ReadTerminated(reader, effectiveLengthValue, TypeNode.StringTerminator); value = GetFieldEncoding().GetString(data, 0, data.Length); break; @@ -540,7 +586,8 @@ public async Task DeserializeAsync(AsyncBinaryReader reader, SerializedType seri break; } case SerializedType.TerminatedString: - { + case SerializedType.TerminatedSizedString: + { var data = await ReadTerminatedAsync(reader, (int) effectiveLengthValue.ByteCount, TypeNode.StringTerminator, cancellationToken) .ConfigureAwait(false); @@ -675,7 +722,7 @@ private FieldLength GetConstLength(FieldLength length) return constLength; } - private byte[] GetNullTermination() + private byte[] GetTermination() { return GetFieldEncoding().GetBytes(new[] { TypeNode.StringTerminator }); } @@ -684,8 +731,10 @@ private static FieldLength GetMaxLength(BoundedStream stream, SerializedType ser { FieldLength maxLength = null; - if (serializedType == SerializedType.ByteArray || serializedType == SerializedType.SizedString || - serializedType == SerializedType.TerminatedString) + if (serializedType == SerializedType.ByteArray || + serializedType == SerializedType.SizedString || + serializedType == SerializedType.TerminatedString || + serializedType == SerializedType.TerminatedSizedString) { // try to get bounded length maxLength = stream.AvailableForWriting; @@ -711,20 +760,22 @@ private static FieldLength GetArrayLength(byte[] data, FieldLength constLength, return length; } - private FieldLength GetNullTerminatedArrayLength(byte[] data, FieldLength constLength, FieldLength maxLength) + private FieldLength GetTerminatedArrayLength(byte[] data, FieldLength constLength, FieldLength maxLength) { FieldLength length = data.Length; - var nullTermination = GetNullTermination(); - var nullTerminationLength = new FieldLength(nullTermination.Length); + var termination = GetTermination(); + var terminationLength = new FieldLength(termination.Length); - if (constLength != null) + if (constLength != null && + constLength < length + terminationLength) { - length = constLength - nullTerminationLength; + length = constLength - terminationLength; } - if (maxLength != null && length > maxLength) + if (maxLength != null && + maxLength < length + terminationLength) { - length = maxLength - nullTerminationLength; + length = maxLength - terminationLength; } return length; @@ -776,7 +827,8 @@ private FieldLength GetEffectiveLengthValue(BinaryReader reader, SerializedType { if (serializedType == SerializedType.ByteArray || serializedType == SerializedType.SizedString || - serializedType == SerializedType.TerminatedString) + serializedType == SerializedType.TerminatedString || + serializedType == SerializedType.TerminatedSizedString) { // try to get bounded length var baseStream = (BoundedStream) reader.BaseStream; diff --git a/BinarySerializer/SerializedType.cs b/BinarySerializer/SerializedType.cs index 168f31c6..c9623440 100644 --- a/BinarySerializer/SerializedType.cs +++ b/BinarySerializer/SerializedType.cs @@ -90,6 +90,12 @@ public enum SerializedType /// /// An encoded string prefixed with a LEB128-encoded length. This is equivalent to how BinaryWriter encodes a string. /// - LengthPrefixedString + LengthPrefixedString, + + /// + /// An encoded string with a terminator, which is null (zero) by default or can be specified by setting StringTerminator. + /// Also has a maximum length storage available + /// + TerminatedSizedString, } } \ No newline at end of file