From 844c16bb8ab4d2d4fd578fb0b01a53f904a3de20 Mon Sep 17 00:00:00 2001 From: glomdom <103685817+glomdom@users.noreply.github.com> Date: Thu, 28 May 2026 09:35:30 +0300 Subject: [PATCH 1/2] feat: create `SerializeWithAttribute` stub --- .../Embedded/SerializeWithAttribute.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 BinaryWizard/Embedded/SerializeWithAttribute.cs diff --git a/BinaryWizard/Embedded/SerializeWithAttribute.cs b/BinaryWizard/Embedded/SerializeWithAttribute.cs new file mode 100644 index 0000000..f884ed7 --- /dev/null +++ b/BinaryWizard/Embedded/SerializeWithAttribute.cs @@ -0,0 +1,31 @@ +/* + * Copyright 2026 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; + +namespace BinaryWizard; + +[AttributeUsage(AttributeTargets.Field)] +public sealed class SerializeWithAttribute : Attribute { + /// + /// The type of the function to use when serializing the attached field. + /// + public Type Function { get; set; } + + public SerializeWithAttribute(Type function) { + Function = function; + } +} \ No newline at end of file From 6496af051212dbc3213da2c5ca80120650fe501b Mon Sep 17 00:00:00 2001 From: glomdom <103685817+glomdom@users.noreply.github.com> Date: Thu, 28 May 2026 22:08:17 +0300 Subject: [PATCH 2/2] feat: serialize `string` by default with null terminated reader (closes: #3) --- BinaryWizard.Tests/BinaryWriterExtensions.cs | 19 ++++++++++++ BinaryWizard.Tests/Samples/Entity.cs | 2 +- BinaryWizard.Tests/SerializerTests.cs | 5 +-- BinaryWizard/Analysis/SymbolExtensions.cs | 8 +++-- BinaryWizard/Emission/CodeBuilder.cs | 31 +++++++++++++++++-- BinaryWizard/Models/TypeModel.cs | 1 + .../Segmenting/DynamicStringSegment.cs | 28 +++++++++++++++++ BinaryWizard/Segmenting/SegmentManager.cs | 16 +++++++--- 8 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 BinaryWizard.Tests/BinaryWriterExtensions.cs create mode 100644 BinaryWizard/Segmenting/DynamicStringSegment.cs diff --git a/BinaryWizard.Tests/BinaryWriterExtensions.cs b/BinaryWizard.Tests/BinaryWriterExtensions.cs new file mode 100644 index 0000000..f37bc32 --- /dev/null +++ b/BinaryWizard.Tests/BinaryWriterExtensions.cs @@ -0,0 +1,19 @@ +using System.Text; + +namespace BinaryWizard.Tests; + +public static class BinaryWriterExtensions { + public static void WriteNullTerminatedString(this BinaryWriter writer, string text, Encoding? encoding = null) { + encoding ??= Encoding.UTF8; + + if (string.IsNullOrEmpty(text)) { + writer.Write("\0"u8); + + return; + } + + var textBytes = encoding.GetBytes(text); + writer.Write(textBytes); + writer.Write(encoding is UnicodeEncoding ? "\0\0"u8 : "\0"u8); + } +} \ No newline at end of file diff --git a/BinaryWizard.Tests/Samples/Entity.cs b/BinaryWizard.Tests/Samples/Entity.cs index ad2375b..e4c0766 100644 --- a/BinaryWizard.Tests/Samples/Entity.cs +++ b/BinaryWizard.Tests/Samples/Entity.cs @@ -19,6 +19,6 @@ namespace BinaryWizard.Tests.Samples; [BinarySerializable] public partial struct Entity { public int Id; - // public string Name; + public string Name; public Vector3 Position; } \ No newline at end of file diff --git a/BinaryWizard.Tests/SerializerTests.cs b/BinaryWizard.Tests/SerializerTests.cs index b6c95ba..abacd53 100644 --- a/BinaryWizard.Tests/SerializerTests.cs +++ b/BinaryWizard.Tests/SerializerTests.cs @@ -74,7 +74,7 @@ public void Entity_CorrectlySerialized() { using var stream = new MemoryStream(); using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) { writer.Write(69); // Id - // writer.Write("glomdom"); // Name + writer.WriteNullTerminatedString("glomdom"); // Name // Position: Vector3 writer.Write(1); // X @@ -88,7 +88,8 @@ public void Entity_CorrectlySerialized() { var actual = Entity.FromBinary(reader); var expected = new Entity { - Id = 69, /* Name = "glomdom", */ + Id = 69, + Name = "glomdom", Position = new Vector3 { X = 1, Y = 2, Z = 3, }, diff --git a/BinaryWizard/Analysis/SymbolExtensions.cs b/BinaryWizard/Analysis/SymbolExtensions.cs index b8c6006..83741ea 100644 --- a/BinaryWizard/Analysis/SymbolExtensions.cs +++ b/BinaryWizard/Analysis/SymbolExtensions.cs @@ -44,7 +44,7 @@ internal static string GetBinaryPrimitiveReader(this ITypeSymbol sym, Endianness internal static int GetByteSize(this ITypeSymbol primitive) { return primitive.SpecialType switch { SpecialType.System_Boolean => 1, - SpecialType.System_Char => 1, + SpecialType.System_Char => 2, SpecialType.System_SByte => 1, SpecialType.System_Byte => 1, SpecialType.System_Int16 => 2, @@ -53,10 +53,12 @@ internal static int GetByteSize(this ITypeSymbol primitive) { SpecialType.System_UInt32 => 4, SpecialType.System_Int64 => 8, SpecialType.System_UInt64 => 8, + SpecialType.System_Single => 4, + SpecialType.System_Double => 8, SpecialType.System_Decimal => 16, - SpecialType.System_Double => 32, + SpecialType.System_String => -1, - _ => throw new InvalidOperationException("Unexpected case encountered."), + _ => throw new InvalidOperationException($"Unexpected case encountered for SpecialType: {primitive.SpecialType}"), }; } diff --git a/BinaryWizard/Emission/CodeBuilder.cs b/BinaryWizard/Emission/CodeBuilder.cs index b30263a..2ff0f87 100644 --- a/BinaryWizard/Emission/CodeBuilder.cs +++ b/BinaryWizard/Emission/CodeBuilder.cs @@ -51,7 +51,22 @@ internal static void Generate(SourceProductionContext spc, ClassSerializationMet switch (seg) { case FixedSegment fixedSeg: ProcessFixedSegment(++segmentCount, fixedSeg, bodyBuilder, bodyIndent, meta); break; case DynamicSegment dynSeg: ProcessDynamicSegment(dynSeg, bodyBuilder, bodyIndent, meta); break; - case NestedObjectSegment nestedSeg: bodyBuilder.AppendLine($"{bodyIndent}result.{nestedSeg.FieldName} = {nestedSeg.TypeName}.FromBinary(reader);"); break; + case NestedObjectSegment nestedSeg: + bodyBuilder.AppendLine($"{bodyIndent}result.{nestedSeg.FieldName} = {nestedSeg.TypeName}.FromBinary(reader);"); break; + case DynamicStringSegment stringSeg: + bodyBuilder.AppendLine($$""" + {{bodyIndent}}{ + {{bodyIndent}} var stringBytes = new System.Collections.Generic.List(); + {{bodyIndent}} while (true) { + {{bodyIndent}} byte b = reader.ReadByte(); + {{bodyIndent}} if (b == 0) break; + {{bodyIndent}} stringBytes.Add(b); + {{bodyIndent}} } + {{bodyIndent}} result.{{stringSeg.Field.Name}} = System.Text.Encoding.UTF8.GetString(stringBytes.ToArray()); + {{bodyIndent}}} + """); + + break; } } @@ -117,7 +132,19 @@ private static void ProcessFixedSegment(int segmentIndex, FixedSegment seg, Stri continue; } - sb.AppendLine($"{indent}result.{field.Name} = {field.TypeModel.Type.GetBinaryPrimitiveReader(meta.Endianness)}({bufName}.Slice({localOffset}, {field.ByteSize}));"); + if (field.TypeModel.IsString) { + sb.AppendLine($$""" + {{indent}}{ + {{indent}} var textSpan = {{bufName}}.Slice({{localOffset}}, {{field.ByteSize}}); + {{indent}} var nullIdx = textSpan.IndexOf((byte)0); + {{indent}} result.{{field.Name}} = System.Text.Encoding.UTF8.GetString(nullIdx >= 0 ? textSpan.Slice(0, nullIdx) : textSpan); + {{indent}}} + """); + } else { + sb.AppendLine( + $"{indent}result.{field.Name} = {field.TypeModel.Type.GetBinaryPrimitiveReader(meta.Endianness)}({bufName}.Slice({localOffset}, {field.ByteSize}));" + ); + } if (field.HasMagic) { var magicStr = field.Magic; diff --git a/BinaryWizard/Models/TypeModel.cs b/BinaryWizard/Models/TypeModel.cs index cc4db7b..015f23f 100644 --- a/BinaryWizard/Models/TypeModel.cs +++ b/BinaryWizard/Models/TypeModel.cs @@ -26,6 +26,7 @@ internal sealed record TypeModel { internal int? InnerTypeByteSize { get; set; } internal bool IsFixedArray => FixedArraySize is not null && InnerType is not null; internal bool IsDynamicArray => FixedArraySize is null && InnerType is not null; + internal bool IsString => Type.SpecialType == SpecialType.System_String; internal TypeModel(ITypeSymbol type, int? fixedArraySize = null) { Type = type; diff --git a/BinaryWizard/Segmenting/DynamicStringSegment.cs b/BinaryWizard/Segmenting/DynamicStringSegment.cs new file mode 100644 index 0000000..a1a2edf --- /dev/null +++ b/BinaryWizard/Segmenting/DynamicStringSegment.cs @@ -0,0 +1,28 @@ +/* + * Copyright 2026 glomdom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using BinaryWizard.Models; + +namespace BinaryWizard.Segmenting; + +internal record DynamicStringSegment : Segment { + internal FieldDef Field { get; set; } + + internal DynamicStringSegment(FieldDef field) { + Field = field; + } +} \ No newline at end of file diff --git a/BinaryWizard/Segmenting/SegmentManager.cs b/BinaryWizard/Segmenting/SegmentManager.cs index f209608..7507d48 100644 --- a/BinaryWizard/Segmenting/SegmentManager.cs +++ b/BinaryWizard/Segmenting/SegmentManager.cs @@ -29,13 +29,21 @@ internal sealed class SegmentManager { internal void AddField(FieldDef field, string? lengthRef = null) { if (field.IsDynamic) { - if (string.IsNullOrEmpty(lengthRef)) throw new ArgumentNullException(nameof(lengthRef), "Length reference was not provided when field is dynamic"); - CommitFixed(); - _segments.Add(new DynamicSegment([field], lengthRef!)); + if (field.TypeModel.IsString) { + _segments.Add(new DynamicStringSegment(field)); + + Debug.WriteLine($"Added dynamic string segment for {field.Name}"); + } else { + if (string.IsNullOrEmpty(lengthRef)) { + throw new ArgumentNullException(nameof(lengthRef), $"Length reference was not provided for dynamic array field '{field.Name}'"); + } + + _segments.Add(new DynamicSegment([field], lengthRef!)); - Debug.WriteLine($"Added dynamic segment for {field.Name} (dep. {lengthRef})"); + Debug.WriteLine($"Added dynamic segment for {field.Name} (dep. {lengthRef})"); + } } else { _currentFields.Add(field);