From 20f90a0926d994a5544343446d191e6c10d3c180 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:34:07 +0100 Subject: [PATCH] vendor in newer version of VersionedSerialization this is done now to reduce the migration burden in the future when this is made into a nuget package (hopefully) --- .../Analyzer/InvalidVersionAnalyzer.cs | 8 +- .../Models/ObjectSerializationInfo.cs | 1 + .../Models/PropertyType.cs | 109 ++- .../Models/VersionCondition.cs | 10 +- .../ObjectSerializationGenerator.cs | 700 ++++++++++-------- .../StructVersion.cs | 23 +- .../Utils/CodeGenerator.cs | 91 +-- .../Utils/Constants.cs | 6 +- .../VersionedSerialization.Generator.csproj | 9 +- .../CustomSerializationAttribute.cs | 6 - .../Attributes/NativeIntegerAttribute.cs | 1 + .../Attributes/VersionConditionAttribute.cs | 4 + VersionedSerialization/INonSeekableReader.cs | 3 + VersionedSerialization/IReadable.cs | 6 +- VersionedSerialization/IReader.cs | 21 +- VersionedSerialization/ISeekableReader.cs | 7 + VersionedSerialization/Impl/EndianReader.cs | 182 +++++ VersionedSerialization/Impl/SpanReader.cs | 69 ++ VersionedSerialization/ReadableExtensions.cs | 26 + VersionedSerialization/Reader.cs | 99 +++ VersionedSerialization/ReaderConfig.cs | 3 + VersionedSerialization/ReaderExtensions.cs | 113 +-- VersionedSerialization/Reader`1.cs | 107 +++ .../SeekableReaderExtensions.cs | 63 ++ VersionedSerialization/SpanReader.cs | 151 ---- VersionedSerialization/StructVersion.cs | 90 ++- .../VersionedSerialization.csproj | 28 +- 27 files changed, 1218 insertions(+), 718 deletions(-) delete mode 100644 VersionedSerialization/Attributes/CustomSerializationAttribute.cs create mode 100644 VersionedSerialization/INonSeekableReader.cs create mode 100644 VersionedSerialization/ISeekableReader.cs create mode 100644 VersionedSerialization/Impl/EndianReader.cs create mode 100644 VersionedSerialization/Impl/SpanReader.cs create mode 100644 VersionedSerialization/ReadableExtensions.cs create mode 100644 VersionedSerialization/Reader.cs create mode 100644 VersionedSerialization/ReaderConfig.cs create mode 100644 VersionedSerialization/Reader`1.cs create mode 100644 VersionedSerialization/SeekableReaderExtensions.cs delete mode 100644 VersionedSerialization/SpanReader.cs diff --git a/VersionedSerialization.Generator/Analyzer/InvalidVersionAnalyzer.cs b/VersionedSerialization.Generator/Analyzer/InvalidVersionAnalyzer.cs index 5bb7d8c..d19bd97 100644 --- a/VersionedSerialization.Generator/Analyzer/InvalidVersionAnalyzer.cs +++ b/VersionedSerialization.Generator/Analyzer/InvalidVersionAnalyzer.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Immutable; +using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; @@ -7,7 +6,9 @@ using VersionedSerialization.Generator.Utils; namespace VersionedSerialization.Generator.Analyzer; +#pragma warning disable RS1038 [DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 public class InvalidVersionAnalyzer : DiagnosticAnalyzer { private const string Identifier = "VS0001"; @@ -49,7 +50,8 @@ public class InvalidVersionAnalyzer : DiagnosticAnalyzer foreach (var argument in attribute.NamedArguments) { var name = argument.Key; - if (name is Constants.LessThan or Constants.GreaterThan or Constants.EqualTo) + if (name is Constants.LessThan or Constants.GreaterThan or Constants.EqualTo + or Constants.LessThanOrEqual or Constants.GreaterThanOrEqual) { var value = (string)argument.Value.Value!; diff --git a/VersionedSerialization.Generator/Models/ObjectSerializationInfo.cs b/VersionedSerialization.Generator/Models/ObjectSerializationInfo.cs index c2f91e0..cd8e7fb 100644 --- a/VersionedSerialization.Generator/Models/ObjectSerializationInfo.cs +++ b/VersionedSerialization.Generator/Models/ObjectSerializationInfo.cs @@ -7,6 +7,7 @@ public sealed record ObjectSerializationInfo( string Namespace, string Name, bool HasBaseType, + bool IsPublic, SyntaxKind DefinitionType, bool CanGenerateSizeMethod, ImmutableEquatableArray Properties diff --git a/VersionedSerialization.Generator/Models/PropertyType.cs b/VersionedSerialization.Generator/Models/PropertyType.cs index d2a7f48..787f6ea 100644 --- a/VersionedSerialization.Generator/Models/PropertyType.cs +++ b/VersionedSerialization.Generator/Models/PropertyType.cs @@ -16,73 +16,62 @@ public enum PropertyType Int32, Int64, String, - Custom, NativeInteger, UNativeInteger, + Bytes } public static class PropertyTypeExtensions { - public static string GetTypeName(this PropertyType type) - => type switch - { - PropertyType.Unsupported => nameof(PropertyType.Unsupported), - PropertyType.None => nameof(PropertyType.None), - PropertyType.UInt8 => nameof(Byte), - PropertyType.Int8 => nameof(SByte), - PropertyType.Boolean => nameof(PropertyType.Boolean), - PropertyType.UInt16 => nameof(PropertyType.UInt16), - PropertyType.UInt32 => nameof(PropertyType.UInt32), - PropertyType.UInt64 => nameof(PropertyType.UInt64), - PropertyType.Int16 => nameof(PropertyType.Int16), - PropertyType.Int32 => nameof(PropertyType.Int32), - PropertyType.Int64 => nameof(PropertyType.Int64), - PropertyType.String => nameof(String), - PropertyType.Custom => "", - PropertyType.NativeInteger => "NInt", - PropertyType.UNativeInteger => "NUInt", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; + extension(PropertyType type) + { + public string GetTypeName() + => type switch + { + PropertyType.Unsupported => nameof(PropertyType.Unsupported), + PropertyType.None => nameof(PropertyType.None), + PropertyType.UInt8 => nameof(Byte), + PropertyType.Int8 => nameof(SByte), + PropertyType.Boolean => nameof(PropertyType.Boolean), + PropertyType.UInt16 => nameof(PropertyType.UInt16), + PropertyType.UInt32 => nameof(PropertyType.UInt32), + PropertyType.UInt64 => nameof(PropertyType.UInt64), + PropertyType.Int16 => nameof(PropertyType.Int16), + PropertyType.Int32 => nameof(PropertyType.Int32), + PropertyType.Int64 => nameof(PropertyType.Int64), + PropertyType.String => nameof(String), + PropertyType.NativeInteger => "NativeInt", + PropertyType.UNativeInteger => "NativeUInt", + PropertyType.Bytes => "", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; - public static bool IsSeperateMethod(this PropertyType type) - => type switch - { - PropertyType.Boolean => true, - PropertyType.String => true, - PropertyType.Custom => true, - PropertyType.NativeInteger => true, - PropertyType.UNativeInteger => true, - _ => false - }; + public bool IsSeperateMethod() + => type switch + { + PropertyType.Boolean => true, + PropertyType.String => true, + PropertyType.NativeInteger => true, + PropertyType.UNativeInteger => true, + PropertyType.Bytes => true, + _ => false + }; - public static int GetTypeSize(this PropertyType type) - => type switch - { - PropertyType.Unsupported => -1, - PropertyType.None => 0, - PropertyType.UInt8 => 1, - PropertyType.Int8 => 1, - PropertyType.Boolean => 1, - PropertyType.UInt16 => 2, - PropertyType.UInt32 => 4, - PropertyType.UInt64 => 8, - PropertyType.Int16 => 2, - PropertyType.Int32 => 4, - PropertyType.Int64 => 8, - PropertyType.String => -1, - PropertyType.Custom => -1, - PropertyType.NativeInteger => -1, - PropertyType.UNativeInteger => -1, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; + public bool IsUnsignedType() + => type switch + { + PropertyType.UInt8 + or PropertyType.UInt16 + or PropertyType.UInt32 + or PropertyType.UInt64 => true, + _ => false + }; - public static bool IsUnsignedType(this PropertyType type) - => type switch - { - PropertyType.UInt8 - or PropertyType.UInt16 - or PropertyType.UInt32 - or PropertyType.UInt64 => true, - _ => false - }; + public bool NeedsAssignmentToMember() + => type switch + { + PropertyType.Bytes => false, + _ => true + }; + } } \ No newline at end of file diff --git a/VersionedSerialization.Generator/Models/VersionCondition.cs b/VersionedSerialization.Generator/Models/VersionCondition.cs index a7983df..7db95e9 100644 --- a/VersionedSerialization.Generator/Models/VersionCondition.cs +++ b/VersionedSerialization.Generator/Models/VersionCondition.cs @@ -1,3 +1,11 @@ namespace VersionedSerialization.Generator.Models; -public sealed record VersionCondition(StructVersion? LessThan, StructVersion? GreaterThan, StructVersion? EqualTo, string? IncludingTag, string? ExcludingTag); \ No newline at end of file +public sealed record VersionCondition( + StructVersion? LessThan, + StructVersion? GreaterThan, + StructVersion? EqualTo, + StructVersion? LessThanOrEqual, + StructVersion? GreaterThanOrEqual, + string? IncludingTag, + string? ExcludingTag +); \ No newline at end of file diff --git a/VersionedSerialization.Generator/ObjectSerializationGenerator.cs b/VersionedSerialization.Generator/ObjectSerializationGenerator.cs index d6d4010..29547de 100644 --- a/VersionedSerialization.Generator/ObjectSerializationGenerator.cs +++ b/VersionedSerialization.Generator/ObjectSerializationGenerator.cs @@ -8,114 +8,102 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using VersionedSerialization.Generator.Models; using VersionedSerialization.Generator.Utils; -namespace VersionedSerialization.Generator +namespace VersionedSerialization.Generator; + +#pragma warning disable RS1038 +[Generator] +#pragma warning restore RS1038 +public sealed class ObjectSerializationGenerator : IIncrementalGenerator { - [Generator] - public sealed class ObjectSerializationGenerator : IIncrementalGenerator + public void Initialize(IncrementalGeneratorInitializationContext context) { - public void Initialize(IncrementalGeneratorInitializationContext context) - { - //Debugger.Launch(); + //Debugger.Launch(); - var valueProvider = context.SyntaxProvider - .ForAttributeWithMetadataName(Constants.VersionedStructAttribute, - static (node, _) => node is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax, - static (context, _) => (ContextClass: (TypeDeclarationSyntax)context.TargetNode, context.SemanticModel)) - .Combine(context.CompilationProvider) - .Select(static (tuple, cancellationToken) => ParseSerializationInfo(tuple.Left.ContextClass, tuple.Left.SemanticModel, tuple.Right, cancellationToken)) - .WithTrackingName(nameof(ObjectSerializationGenerator)); + var valueProvider = context.SyntaxProvider + .ForAttributeWithMetadataName(Constants.VersionedStructAttribute, + static (node, _) => node is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax, + static (context, _) => (ContextClass: (TypeDeclarationSyntax)context.TargetNode, context.SemanticModel)) + .Combine(context.CompilationProvider) + .Select(static (tuple, cancellationToken) => ParseSerializationInfo(tuple.Left.ContextClass, tuple.Left.SemanticModel, tuple.Right, cancellationToken)) + .WithTrackingName(nameof(ObjectSerializationGenerator)); - context.RegisterSourceOutput(valueProvider, EmitCode); + context.RegisterSourceOutput(valueProvider, EmitCode); + } + + private static void EmitCode(SourceProductionContext sourceProductionContext, ObjectSerializationInfo info) + { + var generator = new CodeGenerator(); + generator.AppendLine("#nullable restore"); + generator.AppendLine("using VersionedSerialization;"); + generator.AppendLine("using System.Runtime.CompilerServices;"); + generator.AppendLine(); + + generator.AppendLine($"namespace {info.Namespace};"); + + var versions = new HashSet(); + foreach (var condition in info.Properties.SelectMany(static x => x.VersionConditions)) + { + if (condition.LessThan.HasValue) + versions.Add(condition.LessThan.Value); + + if (condition.GreaterThan.HasValue) + versions.Add(condition.GreaterThan.Value); + + if (condition.EqualTo.HasValue) + versions.Add(condition.EqualTo.Value); + + if (condition.LessThanOrEqual.HasValue) + versions.Add(condition.LessThanOrEqual.Value); + + if (condition.GreaterThanOrEqual.HasValue) + versions.Add(condition.GreaterThanOrEqual.Value); } - private static void EmitCode(SourceProductionContext sourceProductionContext, ObjectSerializationInfo info) + if (versions.Count > 0) { - var generator = new CodeGenerator(); - generator.AppendLine("#nullable restore"); - generator.AppendLine("using VersionedSerialization;"); - generator.AppendLine(); + generator.EnterScope("file static class Versions"); - generator.AppendLine($"namespace {info.Namespace};"); - - var versions = new HashSet(); - foreach (var condition in info.Properties.SelectMany(static x => x.VersionConditions)) + foreach (var version in versions) { - if (condition.LessThan.HasValue) - versions.Add(condition.LessThan.Value); - - if (condition.GreaterThan.HasValue) - versions.Add(condition.GreaterThan.Value); - - if (condition.EqualTo.HasValue) - versions.Add(condition.EqualTo.Value); - } - - if (versions.Count > 0) - { - generator.EnterScope("file static class Versions"); - - foreach (var version in versions) - { - generator.AppendLine($"public static readonly StructVersion {GetVersionIdentifier(version)} = \"{version}\";"); - } - - generator.LeaveScope(); - } - - var definitionType = info.DefinitionType switch - { - SyntaxKind.ClassDeclaration => "class", - SyntaxKind.StructDeclaration => "struct", - SyntaxKind.RecordDeclaration => "record", - SyntaxKind.RecordStructDeclaration => "record struct", - _ => throw new IndexOutOfRangeException() - }; - - generator.EnterScope($"public partial {definitionType} {info.Name} : IReadable"); - GenerateReadMethod(generator, info); - generator.AppendLine(); - GenerateSizeMethod(generator, info); - generator.LeaveScope(); - - sourceProductionContext.AddSource($"{info.Namespace}.{info.Name}.g.cs", generator.ToString()); - } - - private static void GenerateSizeMethod(CodeGenerator generator, ObjectSerializationInfo info) - { - generator.EnterScope("public static int Size(in StructVersion version = default, bool is32Bit = false)"); - - if (!info.CanGenerateSizeMethod) - { - generator.AppendLine("throw new InvalidOperationException(\"No size can be calculated for this struct.\");"); - } - else - { - generator.AppendLine("var size = 0;"); - if (info.HasBaseType) - generator.AppendLine("size += base.Size(in version, is32Bit);"); - - foreach (var property in info.Properties) - { - if (property.VersionConditions.Length > 0) - GenerateVersionCondition(property.VersionConditions, generator); - - generator.EnterScope(); - generator.AppendLine($"size += {property.SizeExpression};"); - generator.LeaveScope(); - } - - generator.AppendLine("return size;"); + generator.AppendLine($"public static readonly StructVersion {GetVersionIdentifier(version)} = \"{version}\";"); } generator.LeaveScope(); } - private static void GenerateReadMethod(CodeGenerator generator, ObjectSerializationInfo info) + var definitionType = info.DefinitionType switch { - generator.EnterScope("public void Read(ref TReader reader, in StructVersion version = default) where TReader : IReader, allows ref struct"); + SyntaxKind.ClassDeclaration => "class", + SyntaxKind.StructDeclaration => "struct", + SyntaxKind.RecordDeclaration => "record", + SyntaxKind.RecordStructDeclaration => "record struct", + _ => throw new IndexOutOfRangeException() + }; + var visibility = info.IsPublic ? "public" : "internal"; + + generator.EnterScope($"{visibility} partial {definitionType} {info.Name} : IReadable"); + GenerateReadMethod(generator, info); + generator.AppendLine(); + GenerateSizeMethod(generator, info); + generator.LeaveScope(); + + sourceProductionContext.AddSource($"{info.Namespace}.{info.Name}.g.cs", generator.ToString()); + } + + private static void GenerateSizeMethod(CodeGenerator generator, ObjectSerializationInfo info) + { + generator.EnterScope("static int IReadable.Size(in StructVersion version, in ReaderConfig config)"); + + if (!info.CanGenerateSizeMethod) + { + generator.AppendLine("throw new InvalidOperationException(\"No size can be calculated for this struct.\");"); + } + else + { + generator.AppendLine("var size = 0;"); if (info.HasBaseType) - generator.AppendLine("base.Read(ref reader, in version);"); + generator.AppendLine("size += base.StructSize(in version, in config);"); foreach (var property in info.Properties) { @@ -123,245 +111,305 @@ namespace VersionedSerialization.Generator GenerateVersionCondition(property.VersionConditions, generator); generator.EnterScope(); - generator.AppendLine($"this.{property.Name} = {property.ReadMethod}"); + generator.AppendLine($"size += {property.SizeExpression};"); generator.LeaveScope(); } + generator.AppendLine("return size;"); + } + + generator.LeaveScope(); + } + + private static void GenerateReadMethod(CodeGenerator generator, ObjectSerializationInfo info) + { + generator.EnterScope("public void Read(ref Reader reader, in StructVersion version = default) where TReader : IReader, allows ref struct"); + + if (info.HasBaseType) + generator.AppendLine("base.Read(ref reader, in version);"); + + foreach (var property in info.Properties) + { + if (property.VersionConditions.Length > 0) + GenerateVersionCondition(property.VersionConditions, generator); + + generator.EnterScope(); + generator.AppendLine(property.Type.NeedsAssignmentToMember() + ? $"this.{property.Name} = {property.ReadMethod}" + : property.ReadMethod); + generator.LeaveScope(); } - private static string GetVersionIdentifier(StructVersion version) - => $"V{version.Major}_{version.Minor}{(version.Tag == null ? "" : $"_{version.Tag}")}"; - - private static void GenerateVersionCondition(ImmutableEquatableArray conditions, - CodeGenerator generator) - { - generator.AppendLine("if ("); - generator.IncreaseIndentation(); - - for (var i = 0; i < conditions.Length; i++) - { - generator.AppendLine("(true"); - - var condition = conditions[i]; - if (condition.LessThan.HasValue) - generator.AppendLine($"&& Versions.{GetVersionIdentifier(condition.LessThan.Value)} >= version"); - - if (condition.GreaterThan.HasValue) - generator.AppendLine($"&& version >= Versions.{GetVersionIdentifier(condition.GreaterThan.Value)}"); - - if (condition.EqualTo.HasValue) - generator.AppendLine($"&& version == Versions.{GetVersionIdentifier(condition.EqualTo.Value)}"); - - if (condition.IncludingTag != null) - generator.AppendLine(condition.IncludingTag == "" - ? "&& version.Tag == null" - : $"&& version.Tag == \"{condition.IncludingTag}\""); - - if (condition.ExcludingTag != null) - generator.AppendLine(condition.IncludingTag == "" - ? "&& version.Tag != null" - : $"&& version.Tag != \"{condition.IncludingTag}\""); - - generator.AppendLine(")"); - - if (i != conditions.Length - 1) - generator.AppendLine("||"); - } - - generator.DecreaseIndentation(); - generator.AppendLine(")"); - } - - private static ObjectSerializationInfo ParseSerializationInfo(TypeDeclarationSyntax contextClass, - SemanticModel model, Compilation compilation, - CancellationToken cancellationToken) - { - var classSymbol = model.GetDeclaredSymbol(contextClass, cancellationToken) ?? throw new InvalidOperationException(); - - //var versionedStructAttribute = compilation.GetTypeByMetadataName(Constants.VersionedStructAttribute); - var versionConditionAttribute = compilation.GetTypeByMetadataName(Constants.VersionConditionAttribute); - var customSerializationAttribute = compilation.GetTypeByMetadataName(Constants.CustomSerializationAttribute); - var nativeIntegerAttribute = compilation.GetTypeByMetadataName(Constants.NativeIntegerAttribute); - - var canGenerateSizeMethod = true; - - var properties = new List(); - foreach (var member in classSymbol.GetMembers()) - { - if (member.IsStatic - || member is IFieldSymbol { AssociatedSymbol: not null } - || member is IPropertySymbol { SetMethod: null }) - continue; - - var versionConditions = new List(); - - ITypeSymbol type; - switch (member) - { - case IFieldSymbol field: - type = field.Type; - break; - case IPropertySymbol property: - type = property.Type; - break; - default: - continue; - } - - var typeInfo = ParseType(type); - string? readMethod = null; - string? sizeExpression = null; - - foreach (var attribute in member.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, versionConditionAttribute)) - { - StructVersion? lessThan = null, - moreThan = null, - equalTo = null; - - string? includingTag = null, - excludingTag = null; - - foreach (var argument in attribute.NamedArguments) - { - switch (argument.Key) - { - case Constants.LessThan: - lessThan = (StructVersion)(string)argument.Value.Value!; - break; - case Constants.GreaterThan: - moreThan = (StructVersion)(string)argument.Value.Value!; - break; - case Constants.EqualTo: - equalTo = (StructVersion)(string)argument.Value.Value!; - break; - case Constants.IncludingTag: - includingTag = (string)argument.Value.Value!; - break; - case Constants.ExcludingTag: - excludingTag = (string)argument.Value.Value!; - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - versionConditions.Add(new VersionCondition(lessThan, moreThan, equalTo, includingTag, excludingTag)); - } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, customSerializationAttribute)) - { - typeInfo = (PropertyType.Custom, "", typeInfo.IsArray); - readMethod = (string)attribute.ConstructorArguments[0].Value!; - sizeExpression = (string)attribute.ConstructorArguments[1].Value!; - } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, nativeIntegerAttribute)) - { - typeInfo = (typeInfo.Type.IsUnsignedType() - ? PropertyType.UNativeInteger - : PropertyType.NativeInteger, - typeInfo.ComplexTypeName == "" - ? typeInfo.Type.GetTypeName() - : typeInfo.ComplexTypeName, - typeInfo.IsArray); - } - } - - canGenerateSizeMethod &= typeInfo.Type != PropertyType.String; - - if (readMethod == null) - { - if (typeInfo.Type == PropertyType.None) - { - readMethod = $"reader.ReadVersionedObject<{typeInfo.ComplexTypeName}>(in version);"; - } - else - { - readMethod = typeInfo.Type.IsSeperateMethod() - ? $"reader.Read{typeInfo.Type.GetTypeName()}();" - : $"reader.ReadPrimitive<{typeInfo.Type.GetTypeName()}>();"; - - if (typeInfo.ComplexTypeName != "") - readMethod = $"({typeInfo.ComplexTypeName}){readMethod}"; - } - } - - sizeExpression ??= typeInfo.Type switch - { - PropertyType.None => $"{typeInfo.ComplexTypeName}.Size(in version, is32Bit)", - PropertyType.NativeInteger or PropertyType.UNativeInteger => - "is32Bit ? sizeof(uint) : sizeof(ulong)", - _ => $"sizeof({typeInfo.Type.GetTypeName()})" - }; - - properties.Add(new PropertySerializationInfo( - member.Name, - readMethod, - sizeExpression, - typeInfo.Type, - versionConditions.ToImmutableEquatableArray() - )); - } - - var hasBaseType = false; - if (classSymbol.BaseType != null) - { - var objectSymbol = compilation.GetSpecialType(SpecialType.System_Object); - var valueTypeSymbol = compilation.GetSpecialType(SpecialType.System_ValueType); - - if (!SymbolEqualityComparer.Default.Equals(objectSymbol, classSymbol.BaseType) - && !SymbolEqualityComparer.Default.Equals(valueTypeSymbol, classSymbol.BaseType)) - hasBaseType = true; - } - - return new ObjectSerializationInfo( - classSymbol.ContainingNamespace.ToDisplayString(), - classSymbol.Name, - hasBaseType, - contextClass.Kind(), - canGenerateSizeMethod, - properties.ToImmutableEquatableArray() - ); - } - - private static (PropertyType Type, string ComplexTypeName, bool IsArray) ParseType(ITypeSymbol typeSymbol) - { - switch (typeSymbol) - { - case IArrayTypeSymbol arrayTypeSymbol: - { - var elementType = ParseType(arrayTypeSymbol.ElementType); - return (elementType.Type, elementType.ComplexTypeName, true); - } - case INamedTypeSymbol { EnumUnderlyingType: not null } namedTypeSymbol: - var res = ParseType(namedTypeSymbol.EnumUnderlyingType); - return (res.Type, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), false); - } - - if (typeSymbol.SpecialType != SpecialType.None) - { - var type = typeSymbol.SpecialType switch - { - SpecialType.System_Boolean => PropertyType.Boolean, - SpecialType.System_Byte => PropertyType.UInt8, - SpecialType.System_UInt16 => PropertyType.UInt16, - SpecialType.System_UInt32 => PropertyType.UInt32, - SpecialType.System_UInt64 => PropertyType.UInt64, - SpecialType.System_SByte => PropertyType.Int8, - SpecialType.System_Int16 => PropertyType.Int16, - SpecialType.System_Int32 => PropertyType.Int32, - SpecialType.System_Int64 => PropertyType.Int64, - SpecialType.System_String => PropertyType.String, - _ => PropertyType.Unsupported - }; - - return (type, "", false); - } - - var complexType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - return (PropertyType.None, complexType, false); - } + generator.LeaveScope(); } -} + + private static string GetVersionIdentifier(StructVersion version) + => $"V{version.Major}_{version.Minor}{(version.Tag == null ? "" : $"_{version.Tag}")}"; + + private static void GenerateVersionCondition(ImmutableEquatableArray conditions, + CodeGenerator generator) + { + generator.AppendLine("if ("); + generator.IncreaseIndentation(); + + for (var i = 0; i < conditions.Length; i++) + { + generator.AppendLine("(true"); + + var condition = conditions[i]; + if (condition.LessThan.HasValue) + generator.AppendLine($"&& version < Versions.{GetVersionIdentifier(condition.LessThan.Value)}"); + + if (condition.GreaterThan.HasValue) + generator.AppendLine($"&& version > Versions.{GetVersionIdentifier(condition.GreaterThan.Value)}"); + + if (condition.EqualTo.HasValue) + generator.AppendLine($"&& version == Versions.{GetVersionIdentifier(condition.EqualTo.Value)}"); + + if (condition.LessThanOrEqual.HasValue) + generator.AppendLine($"&& version <= Versions.{GetVersionIdentifier(condition.LessThanOrEqual.Value)}"); + + if (condition.GreaterThanOrEqual.HasValue) + generator.AppendLine($"&& version >= Versions.{GetVersionIdentifier(condition.GreaterThanOrEqual.Value)}"); + + if (condition.IncludingTag != null) + generator.AppendLine(condition.IncludingTag == "" + ? "&& version.Tag == null" + : $"&& version.Tag == \"{condition.IncludingTag}\""); + + if (condition.ExcludingTag != null) + generator.AppendLine(condition.ExcludingTag == "" + ? "&& version.Tag != null" + : $"&& version.Tag != \"{condition.ExcludingTag}\""); + + generator.AppendLine(")"); + + if (i != conditions.Length - 1) + generator.AppendLine("||"); + } + + generator.DecreaseIndentation(); + generator.AppendLine(")"); + } + + private static ObjectSerializationInfo ParseSerializationInfo(TypeDeclarationSyntax contextClass, + SemanticModel model, Compilation compilation, + CancellationToken cancellationToken) + { + var classSymbol = model.GetDeclaredSymbol(contextClass, cancellationToken) ?? throw new InvalidOperationException(); + var classIsPublic = classSymbol.DeclaredAccessibility == Accessibility.Public; + + //var versionedStructAttribute = compilation.GetTypeByMetadataName(Constants.VersionedStructAttribute); + var versionConditionAttribute = compilation.GetTypeByMetadataName(Constants.VersionConditionAttribute); + var nativeIntegerAttribute = compilation.GetTypeByMetadataName(Constants.NativeIntegerAttribute); + var inlineArrayAttribute = compilation.GetTypeByMetadataName(Constants.InlineArrayAttribute); + + var canGenerateSizeMethod = true; + + var properties = new List(); + foreach (var member in classSymbol.GetMembers()) + { + if (member.IsStatic + || member is IFieldSymbol { AssociatedSymbol: not null } + || member is IPropertySymbol { SetMethod: null }) + continue; + + var versionConditions = new List(); + + ITypeSymbol type; + switch (member) + { + case IFieldSymbol field: + type = field.Type; + break; + case IPropertySymbol property: + type = property.Type; + break; + default: + continue; + } + + var typeInfo = ParseType(type, inlineArrayAttribute); + string? readMethod = null; + string? sizeExpression = null; + + foreach (var attribute in member.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, versionConditionAttribute)) + { + StructVersion? lessThan = null, + moreThan = null, + equalTo = null, + lessThanOrEqual = null, + moreThanOrEqual = null; + + string? includingTag = null, + excludingTag = null; + + foreach (var argument in attribute.NamedArguments) + { + var stringArgument = (string)argument.Value.Value!; + + switch (argument.Key) + { + case Constants.LessThan: + lessThan = stringArgument; + break; + case Constants.GreaterThan: + moreThan = stringArgument; + break; + case Constants.EqualTo: + equalTo = stringArgument; + break; + case Constants.LessThanOrEqual: + lessThanOrEqual = stringArgument; + break; + case Constants.GreaterThanOrEqual: + moreThanOrEqual = stringArgument; + break; + case Constants.IncludingTag: + includingTag = stringArgument; + break; + case Constants.ExcludingTag: + excludingTag = stringArgument; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + var condition = new VersionCondition( + lessThan, + moreThan, + equalTo, + lessThanOrEqual, + moreThanOrEqual, + includingTag, + excludingTag); + + versionConditions.Add(condition); + } + else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, nativeIntegerAttribute)) + { + var nativeIntegerType = typeInfo.Type.IsUnsignedType() + ? PropertyType.UNativeInteger + : PropertyType.NativeInteger; + + var complexTypeName = typeInfo.ComplexTypeName == "" + ? typeInfo.Type.GetTypeName() + : typeInfo.ComplexTypeName; + + typeInfo = (nativeIntegerType, complexTypeName, typeInfo.IsArray); + } + } + + canGenerateSizeMethod &= typeInfo.Type != PropertyType.String; + + if (readMethod == null) + { + if (typeInfo.Type == PropertyType.None) + { + readMethod = $"reader.ReadVersionedObject<{typeInfo.ComplexTypeName}>(in version);"; + } + else if (typeInfo.Type == PropertyType.Bytes) + { + readMethod = $"reader.Read({member.Name});"; + } + else + { + readMethod = typeInfo.Type.IsSeperateMethod() + ? $"reader.Read{typeInfo.Type.GetTypeName()}();" + : $"reader.ReadPrimitive<{typeInfo.Type.GetTypeName()}>();"; + + if (typeInfo.ComplexTypeName != "") + readMethod = $"({typeInfo.ComplexTypeName}){readMethod}"; + } + } + + sizeExpression ??= typeInfo.Type switch + { + PropertyType.None => $"{typeInfo.ComplexTypeName}.StructSize(in version, in config)", + PropertyType.NativeInteger or PropertyType.UNativeInteger => + "config.Is32Bit ? sizeof(uint) : sizeof(ulong)", + PropertyType.Bytes => $"Unsafe.SizeOf<{typeInfo.ComplexTypeName}>()", + _ => $"sizeof({typeInfo.Type.GetTypeName()})" + }; + + properties.Add(new PropertySerializationInfo( + member.Name, + readMethod, + sizeExpression, + typeInfo.Type, + versionConditions.ToImmutableEquatableArray() + )); + } + + var hasBaseType = false; + if (classSymbol.BaseType != null) + { + var objectSymbol = compilation.GetSpecialType(SpecialType.System_Object); + var valueTypeSymbol = compilation.GetSpecialType(SpecialType.System_ValueType); + + if (!SymbolEqualityComparer.Default.Equals(objectSymbol, classSymbol.BaseType) + && !SymbolEqualityComparer.Default.Equals(valueTypeSymbol, classSymbol.BaseType)) + hasBaseType = true; + } + + return new ObjectSerializationInfo( + classSymbol.ContainingNamespace.ToDisplayString(), + classSymbol.Name, + hasBaseType, + classIsPublic, + contextClass.Kind(), + canGenerateSizeMethod, + properties.ToImmutableEquatableArray() + ); + } + + private static (PropertyType Type, string ComplexTypeName, bool IsArray) ParseType(ITypeSymbol typeSymbol, + INamedTypeSymbol? inlineArraySymbol) + { + switch (typeSymbol) + { + case IArrayTypeSymbol arrayTypeSymbol: + { + var elementType = ParseType(arrayTypeSymbol.ElementType, inlineArraySymbol); + return (elementType.Type, elementType.ComplexTypeName, true); + } + case INamedTypeSymbol { EnumUnderlyingType: not null } namedTypeSymbol: + var res = ParseType(namedTypeSymbol.EnumUnderlyingType, inlineArraySymbol); + return (res.Type, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), false); + } + + if (typeSymbol.SpecialType != SpecialType.None) + { + var type = typeSymbol.SpecialType switch + { + SpecialType.System_Boolean => PropertyType.Boolean, + SpecialType.System_Byte => PropertyType.UInt8, + SpecialType.System_UInt16 => PropertyType.UInt16, + SpecialType.System_UInt32 => PropertyType.UInt32, + SpecialType.System_UInt64 => PropertyType.UInt64, + SpecialType.System_SByte => PropertyType.Int8, + SpecialType.System_Int16 => PropertyType.Int16, + SpecialType.System_Int32 => PropertyType.Int32, + SpecialType.System_Int64 => PropertyType.Int64, + SpecialType.System_String => PropertyType.String, + _ => PropertyType.Unsupported + }; + + return (type, "", false); + } + + var complexType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + foreach (var attribute in typeSymbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, inlineArraySymbol)) + return (PropertyType.Bytes, complexType, false); + } + + return (PropertyType.None, complexType, false); + } +} \ No newline at end of file diff --git a/VersionedSerialization.Generator/StructVersion.cs b/VersionedSerialization.Generator/StructVersion.cs index c69018b..4da6e3a 100644 --- a/VersionedSerialization.Generator/StructVersion.cs +++ b/VersionedSerialization.Generator/StructVersion.cs @@ -1,21 +1,11 @@ using System; -namespace VersionedSerialization; +namespace VersionedSerialization.Generator; -public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = null) +public readonly record struct StructVersion(int Major = 0, int Minor = 0, string? Tag = null) { - public readonly int Major = major; - public readonly int Minor = minor; - public readonly string? Tag = tag; - #region Equality operators - public static bool operator ==(StructVersion left, StructVersion right) - => left.Major == right.Major && left.Minor == right.Minor; - - public static bool operator !=(StructVersion left, StructVersion right) - => !(left == right); - public static bool operator >(StructVersion left, StructVersion right) => left.Major > right.Major || (left.Major == right.Major && left.Minor > right.Minor); @@ -28,15 +18,6 @@ public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = public static bool operator <=(StructVersion left, StructVersion right) => left.Major < right.Major || (left.Major == right.Major && left.Minor <= right.Minor); - public override bool Equals(object? obj) - => obj is StructVersion other && Equals(other); - - public bool Equals(StructVersion other) - => Major == other.Major && Minor == other.Minor; - - public override int GetHashCode() - => HashCode.Combine(Major, Minor, Tag); - #endregion public override string ToString() => $"{Major}.{Minor}{(Tag != null ? $"-{Tag}" : "")}"; diff --git a/VersionedSerialization.Generator/Utils/CodeGenerator.cs b/VersionedSerialization.Generator/Utils/CodeGenerator.cs index 5c9ac9b..9d3dc3f 100644 --- a/VersionedSerialization.Generator/Utils/CodeGenerator.cs +++ b/VersionedSerialization.Generator/Utils/CodeGenerator.cs @@ -1,54 +1,55 @@ using System.Text; -namespace VersionedSerialization.Generator.Utils +namespace VersionedSerialization.Generator.Utils; + +public class CodeGenerator { - public class CodeGenerator + private const string Indent = " "; + + private readonly StringBuilder _sb = new(); + private string _currentIndent = ""; + + public void EnterScope(string? header = null) { - private const string Indent = " "; - - private readonly StringBuilder _sb = new(); - private string _currentIndent = ""; - - public void EnterScope(string? header = null) + if (header != null) { - if (header != null) - { - AppendLine(header); - } - - AppendLine("{"); - IncreaseIndentation(); + AppendLine(header); } - public void LeaveScope(string suffix = "") - { - DecreaseIndentation(); - AppendLine($"}}{suffix}"); - } - - public void IncreaseIndentation() - { - _currentIndent += Indent; - } - - public void DecreaseIndentation() - { - _currentIndent = _currentIndent.Substring(0, _currentIndent.Length - Indent.Length); - } - - public void AppendLine() - { - _sb.AppendLine(); - } - - public void AppendLine(string text) - { - _sb.AppendLine(_currentIndent + text); - } - - public override string ToString() - { - return _sb.ToString(); - } + AppendLine("{"); + IncreaseIndentation(); } + + public void LeaveScope(string suffix = "") + { + DecreaseIndentation(); + + _sb.Append(_currentIndent); + _sb.Append('}'); + _sb.AppendLine(suffix); + } + + public void IncreaseIndentation() + { + _currentIndent += Indent; + } + + public void DecreaseIndentation() + { + _currentIndent = _currentIndent[..^Indent.Length]; + } + + public void AppendLine() + { + _sb.AppendLine(); + } + + public void AppendLine(string text) + { + _sb.Append(_currentIndent); + _sb.AppendLine(text); + } + + public override string ToString() + => _sb.ToString(); } \ No newline at end of file diff --git a/VersionedSerialization.Generator/Utils/Constants.cs b/VersionedSerialization.Generator/Utils/Constants.cs index 065fd50..8c92fd2 100644 --- a/VersionedSerialization.Generator/Utils/Constants.cs +++ b/VersionedSerialization.Generator/Utils/Constants.cs @@ -6,12 +6,16 @@ public static class Constants public const string VersionedStructAttribute = $"{AttributeNamespace}.{nameof(VersionedStructAttribute)}"; public const string VersionConditionAttribute = $"{AttributeNamespace}.{nameof(VersionConditionAttribute)}"; - public const string CustomSerializationAttribute = $"{AttributeNamespace}.{nameof(CustomSerializationAttribute)}"; public const string NativeIntegerAttribute = $"{AttributeNamespace}.{nameof(NativeIntegerAttribute)}"; + public const string InlineArrayAttribute = "System.Runtime.CompilerServices.InlineArrayAttribute"; + public const string LessThan = nameof(LessThan); public const string GreaterThan = nameof(GreaterThan); public const string EqualTo = nameof(EqualTo); + public const string LessThanOrEqual = nameof(LessThanOrEqual); + public const string GreaterThanOrEqual = nameof(GreaterThanOrEqual); + public const string IncludingTag = nameof(IncludingTag); public const string ExcludingTag = nameof(ExcludingTag); } \ No newline at end of file diff --git a/VersionedSerialization.Generator/VersionedSerialization.Generator.csproj b/VersionedSerialization.Generator/VersionedSerialization.Generator.csproj index 8f3e124..1aee75d 100644 --- a/VersionedSerialization.Generator/VersionedSerialization.Generator.csproj +++ b/VersionedSerialization.Generator/VersionedSerialization.Generator.csproj @@ -2,15 +2,16 @@ netstandard2.0 - 4.10 - 4.10.0 + latest + 4.14 + 4.14.0 enable true - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/VersionedSerialization/Attributes/CustomSerializationAttribute.cs b/VersionedSerialization/Attributes/CustomSerializationAttribute.cs deleted file mode 100644 index 8e44430..0000000 --- a/VersionedSerialization/Attributes/CustomSerializationAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace VersionedSerialization.Attributes; - -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -#pragma warning disable CS9113 // Parameter is unread. -public class CustomSerializationAttribute(string methodName, string sizeExpression) : Attribute; -#pragma warning restore CS9113 // Parameter is unread. diff --git a/VersionedSerialization/Attributes/NativeIntegerAttribute.cs b/VersionedSerialization/Attributes/NativeIntegerAttribute.cs index ae90d98..9e3c67b 100644 --- a/VersionedSerialization/Attributes/NativeIntegerAttribute.cs +++ b/VersionedSerialization/Attributes/NativeIntegerAttribute.cs @@ -1,3 +1,4 @@ namespace VersionedSerialization.Attributes; +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class NativeIntegerAttribute : Attribute; \ No newline at end of file diff --git a/VersionedSerialization/Attributes/VersionConditionAttribute.cs b/VersionedSerialization/Attributes/VersionConditionAttribute.cs index d105800..4ff61a4 100644 --- a/VersionedSerialization/Attributes/VersionConditionAttribute.cs +++ b/VersionedSerialization/Attributes/VersionConditionAttribute.cs @@ -6,6 +6,10 @@ public class VersionConditionAttribute : Attribute public string LessThan { get; set; } = ""; public string GreaterThan { get; set; } = ""; public string EqualTo { get; set; } = ""; + + public string LessThanOrEqual { get; set; } = ""; + public string GreaterThanOrEqual { get; set; } = ""; + public string IncludingTag { get; set; } = ""; public string ExcludingTag { get; set; } = ""; } \ No newline at end of file diff --git a/VersionedSerialization/INonSeekableReader.cs b/VersionedSerialization/INonSeekableReader.cs new file mode 100644 index 0000000..055cc84 --- /dev/null +++ b/VersionedSerialization/INonSeekableReader.cs @@ -0,0 +1,3 @@ +namespace VersionedSerialization; + +public interface INonSeekableReader : IReader; \ No newline at end of file diff --git a/VersionedSerialization/IReadable.cs b/VersionedSerialization/IReadable.cs index eb04b41..760cbee 100644 --- a/VersionedSerialization/IReadable.cs +++ b/VersionedSerialization/IReadable.cs @@ -2,6 +2,8 @@ public interface IReadable { - public void Read(ref TReader reader, in StructVersion version = default) where TReader : IReader, allows ref struct; - public static abstract int Size(in StructVersion version = default, bool is32Bit = false); + public void Read(ref Reader reader, in StructVersion version = default) + where TReader : IReader, allows ref struct; + + public static abstract int Size(in StructVersion version = default, in ReaderConfig config = default); } \ No newline at end of file diff --git a/VersionedSerialization/IReader.cs b/VersionedSerialization/IReader.cs index 764eac6..3ff5617 100644 --- a/VersionedSerialization/IReader.cs +++ b/VersionedSerialization/IReader.cs @@ -1,23 +1,12 @@ -using System.Collections.Immutable; +using System.Text; namespace VersionedSerialization; public interface IReader { - bool Is32Bit { get; } + string ReadString(int length = -1, Encoding? encoding = null); + ReadOnlySpan ReadBytes(long length); - bool ReadBoolean(); - long ReadNInt(); - ulong ReadNUInt(); - string ReadString(); - ReadOnlySpan ReadBytes(int length); - - T ReadPrimitive() where T : unmanaged; - ImmutableArray ReadPrimitiveArray(long count) where T : unmanaged; - - T ReadVersionedObject(in StructVersion version = default) where T : IReadable, new(); - ImmutableArray ReadVersionedObjectArray(long count, in StructVersion version = default) where T : IReadable, new(); - - void Align(int alignment = 0); - void Skip(int count); + void Read(scoped Span dest) where T : unmanaged; + void ReadPrimitive(scoped Span dest) where T : unmanaged; } \ No newline at end of file diff --git a/VersionedSerialization/ISeekableReader.cs b/VersionedSerialization/ISeekableReader.cs new file mode 100644 index 0000000..fe241b6 --- /dev/null +++ b/VersionedSerialization/ISeekableReader.cs @@ -0,0 +1,7 @@ +namespace VersionedSerialization; + +public interface ISeekableReader : IReader +{ + int Offset { get; set; } + int Length { get; } +} \ No newline at end of file diff --git a/VersionedSerialization/Impl/EndianReader.cs b/VersionedSerialization/Impl/EndianReader.cs new file mode 100644 index 0000000..1f6f8b8 --- /dev/null +++ b/VersionedSerialization/Impl/EndianReader.cs @@ -0,0 +1,182 @@ +using System.Buffers.Binary; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace VersionedSerialization.Impl; + +file static class EndianReader +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Span Cast(Span src) + { + Debug.Assert(Unsafe.SizeOf() == Unsafe.SizeOf()); + return Unsafe.BitCast, Span>(src); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + + private static ReadOnlySpan Cast(ReadOnlySpan src) + { + Debug.Assert(Unsafe.SizeOf() == Unsafe.SizeOf()); + return Unsafe.BitCast, ReadOnlySpan>(src); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReverseEndianness(ReadOnlySpan input, Span output) + where T : unmanaged + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + throw new InvalidOperationException(); + + if (Unsafe.SizeOf() == sizeof(short)) + { + var src = Cast(input); + var dst = Cast(output); + BinaryPrimitives.ReverseEndianness(src, dst); + } + else if (Unsafe.SizeOf() == sizeof(int)) + { + var src = Cast(input); + var dst = Cast(output); + BinaryPrimitives.ReverseEndianness(src, dst); + } + else if (Unsafe.SizeOf() == sizeof(long)) + { + var src = Cast(input); + var dst = Cast(output); + BinaryPrimitives.ReverseEndianness(src, dst); + } + else if (Unsafe.SizeOf() == Unsafe.SizeOf()) + { + var src = Cast(input); + var dst = Cast(output); + BinaryPrimitives.ReverseEndianness(src, dst); + } + + Debug.Assert(false, "Failed to reverse endianness for type"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ReadLittleEndian(ref TReader reader, scoped Span dest) + where TReader : IReader, allows ref struct + where TType : unmanaged + { + if (BitConverter.IsLittleEndian || Unsafe.SizeOf() == sizeof(byte)) + { + reader.ReadPrimitive(dest); + } + else + { + var data = reader.ReadBytes(Unsafe.SizeOf() * dest.Length); + var src = MemoryMarshal.Cast(data); + ReverseEndianness(src, dest); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ReadBigEndian(ref TReader reader, scoped Span dest) + where TReader : IReader, allows ref struct + where TType : unmanaged + { + if (!BitConverter.IsLittleEndian || Unsafe.SizeOf() == sizeof(byte)) + { + reader.ReadPrimitive(dest); + } + else + { + var data = reader.ReadBytes(Unsafe.SizeOf() * dest.Length); + var src = MemoryMarshal.Cast(data); + ReverseEndianness(src, dest); + } + } +} + +public ref struct LittleEndianReader(TReader impl) : IReader + where TReader : IReader, allows ref struct +{ + private TReader _impl = impl; + + public string ReadString(int length = -1, Encoding? encoding = null) + => _impl.ReadString(length, encoding); + + public ReadOnlySpan ReadBytes(long length) + => _impl.ReadBytes(length); + + public void Read(scoped Span dest) where T : unmanaged + => _impl.Read(dest); + + public void ReadPrimitive(scoped Span dest) where T : unmanaged + => EndianReader.ReadLittleEndian(ref _impl, dest); +} + +public ref struct LittleEndianSeekableReader(TReader impl) : ISeekableReader + where TReader : ISeekableReader, allows ref struct +{ + private TReader _impl = impl; + + public int Offset + { + get => _impl.Offset; + set => _impl.Offset = value; + } + + public int Length => _impl.Length; + + public string ReadString(int length = -1, Encoding? encoding = null) + => _impl.ReadString(length, encoding); + + public ReadOnlySpan ReadBytes(long length) + => _impl.ReadBytes(length); + + public void Read(scoped Span dest) where T : unmanaged + => _impl.Read(dest); + + public void ReadPrimitive(scoped Span dest) where T : unmanaged + => EndianReader.ReadLittleEndian(ref _impl, dest); +} + +public ref struct BigEndianReader(TReader impl) : IReader + where TReader : IReader, allows ref struct +{ + private TReader _impl = impl; + + public string ReadString(int length = -1, Encoding? encoding = null) + => _impl.ReadString(length, encoding); + + public ReadOnlySpan ReadBytes(long length) + => _impl.ReadBytes(length); + + public void Read(scoped Span dest) where T : unmanaged + => _impl.Read(dest); + + public void ReadPrimitive(scoped Span dest) where T : unmanaged + => EndianReader.ReadBigEndian(ref _impl, dest); +} + +public ref struct BigEndianSeekableReader(TReader impl) : ISeekableReader + where TReader : ISeekableReader, allows ref struct +{ + private TReader _impl = impl; + + public int Offset + { + get => _impl.Offset; + set => _impl.Offset = value; + } + + public int Length => _impl.Length; + + public string ReadString(int length = -1, Encoding? encoding = null) + => _impl.ReadString(length, encoding); + + public ReadOnlySpan ReadBytes(long length) + => _impl.ReadBytes(length); + + public void Read(scoped Span dest) where T : unmanaged + => _impl.Read(dest); + + public void ReadPrimitive(scoped Span dest) where T : unmanaged + => EndianReader.ReadBigEndian(ref _impl, dest); +} \ No newline at end of file diff --git a/VersionedSerialization/Impl/SpanReader.cs b/VersionedSerialization/Impl/SpanReader.cs new file mode 100644 index 0000000..544d9e8 --- /dev/null +++ b/VersionedSerialization/Impl/SpanReader.cs @@ -0,0 +1,69 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace VersionedSerialization.Impl; + +// ReSharper disable ReplaceSliceWithRangeIndexer | The range indexer gets compiled into .Slice(x, y) and not .Slice(x) which worsens performance +public ref struct SpanReader(ReadOnlySpan data, int offset = 0) + : ISeekableReader +{ + public int Offset { get; set; } = offset; + + public readonly byte Peek => _data[Offset]; + public readonly int Length => _data.Length; + + private readonly ReadOnlySpan _data = data; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Read(scoped Span dest) where T : unmanaged + { + var offset = Offset; + var data = MemoryMarshal.Cast(dest); + _data.Slice(offset, data.Length).CopyTo(data); + Offset = offset + data.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadPrimitive(scoped Span dest) where T : unmanaged + => Read(dest); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan ReadBytes(long length) + { + var intLength = checked((int)length); + + var offset = Offset; + var val = _data.Slice(offset, intLength); + Offset = offset + intLength; + return val; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadString(int length = -1, Encoding? encoding = null) + { + encoding ??= Encoding.UTF8; + if (encoding is not (UTF8Encoding or ASCIIEncoding)) + ThrowUnsupportedEncodingException(); + + var offset = Offset; + if (length == -1) + { + length = _data.Slice(offset).IndexOf(byte.MinValue); + if (length == -1) + throw new InvalidDataException("Failed to find string in span."); + } + + var val = _data.Slice(offset, length); + var str = encoding.GetString(val); + + Offset = offset + length + 1; // Skip null terminator + + return str; + } + + [DoesNotReturn] + private static void ThrowUnsupportedEncodingException() => + throw new InvalidOperationException("Unsupported encoding: Only ASCII/UTF8 is currently supported."); +} \ No newline at end of file diff --git a/VersionedSerialization/ReadableExtensions.cs b/VersionedSerialization/ReadableExtensions.cs new file mode 100644 index 0000000..073ccd9 --- /dev/null +++ b/VersionedSerialization/ReadableExtensions.cs @@ -0,0 +1,26 @@ +namespace VersionedSerialization; + +public static class ReadableExtensions +{ + extension(TReadable) + where TReadable : IReadable, new() + { + public static int StructSize(in StructVersion version = default, in ReaderConfig config = default) + => TReadable.Size(version, config); + + public static TReadable FromBytes(ReadOnlySpan data, bool littleEndian = true, ReaderConfig config = default, + in StructVersion version = default) + { + if (littleEndian) + { + var reader = Reader.LittleEndian(data, config: config); + return reader.ReadVersionedObject(version); + } + else + { + var reader = Reader.BigEndian(data, config: config); + return reader.ReadVersionedObject(version); + } + } + } +} \ No newline at end of file diff --git a/VersionedSerialization/Reader.cs b/VersionedSerialization/Reader.cs new file mode 100644 index 0000000..363655d --- /dev/null +++ b/VersionedSerialization/Reader.cs @@ -0,0 +1,99 @@ +using System.Collections.Immutable; +using VersionedSerialization.Impl; + +namespace VersionedSerialization; + +public static class Reader +{ + public static Reader From(T reader, ReaderConfig config = default) where T : IReader, allows ref struct + => new(reader, config); + + public static Reader> LittleEndian(ReadOnlySpan data, int offset = 0, + ReaderConfig config = default) + => new(new SpanReader(data, offset).AsLittleEndian(), config); + + public static Reader> BigEndian(ReadOnlySpan data, int offset = 0, + ReaderConfig config = default) + => new(new SpanReader(data, offset).AsBigEndian(), config); + + public static T Read(ReadOnlySpan data, int offset = 0, bool littleEndian = true, + ReaderConfig config = default) + where T : unmanaged + { + if (littleEndian) + { + var reader = LittleEndian(data, offset, config); + return reader.Read(); + } + else + { + var reader = BigEndian(data, offset, config); + return reader.Read(); + } + } + + public static T ReadPrimitive(ReadOnlySpan data, int offset = 0, bool littleEndian = true, + ReaderConfig config = default) + where T : unmanaged + { + if (littleEndian) + { + var reader = LittleEndian(data, offset, config); + return reader.ReadPrimitive(); + } + else + { + var reader = BigEndian(data, offset, config); + return reader.ReadPrimitive(); + } + } + + public static ImmutableArray ReadPrimitiveArray(ReadOnlySpan data, long count, int offset = 0, bool littleEndian = true, + ReaderConfig config = default) + where T : unmanaged + { + if (littleEndian) + { + var reader = LittleEndian(data, offset, config); + return reader.ReadPrimitiveArray(count); + } + else + { + var reader = BigEndian(data, offset, config); + return reader.ReadPrimitiveArray(count); + } + } + + public static T ReadVersionedObject(ReadOnlySpan data, StructVersion version = default, int offset = 0, bool littleEndian = true, + ReaderConfig config = default) + where T : IReadable, new() + { + if (littleEndian) + { + var reader = LittleEndian(data, offset, config); + return reader.ReadVersionedObject(version); + } + else + { + var reader = BigEndian(data, offset, config); + return reader.ReadVersionedObject(version); + } + } + + public static ImmutableArray ReadVersionedObjectArray(ReadOnlySpan data, long count, + StructVersion version = default, int offset = 0, bool littleEndian = true, + ReaderConfig config = default) + where T : IReadable, new() + { + if (littleEndian) + { + var reader = LittleEndian(data, offset, config); + return reader.ReadVersionedObjectArray(count, version); + } + else + { + var reader = BigEndian(data, offset, config); + return reader.ReadVersionedObjectArray(count, version); + } + } +} \ No newline at end of file diff --git a/VersionedSerialization/ReaderConfig.cs b/VersionedSerialization/ReaderConfig.cs new file mode 100644 index 0000000..f8965aa --- /dev/null +++ b/VersionedSerialization/ReaderConfig.cs @@ -0,0 +1,3 @@ +namespace VersionedSerialization; + +public readonly record struct ReaderConfig(bool Is32Bit); \ No newline at end of file diff --git a/VersionedSerialization/ReaderExtensions.cs b/VersionedSerialization/ReaderExtensions.cs index 58b00a3..bddfb15 100644 --- a/VersionedSerialization/ReaderExtensions.cs +++ b/VersionedSerialization/ReaderExtensions.cs @@ -1,62 +1,77 @@ using System.Runtime.CompilerServices; +using VersionedSerialization.Impl; namespace VersionedSerialization; public static class ReaderExtensions { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint ReadCompressedUInt(this ref T reader) where T : struct, IReader, allows ref struct + extension(ref Reader reader) + where TReader : struct, IReader, allows ref struct { - var first = reader.ReadPrimitive(); - - if ((first & 0b10000000) == 0b00000000) - return first; - - if ((first & 0b11000000) == 0b10000000) - return (uint)(((first & ~0b10000000) << 8) | reader.ReadPrimitive()); - - if ((first & 0b11100000) == 0b11000000) - return (uint)(((first & ~0b11000000) << 24) | (reader.ReadPrimitive() << 16) | (reader.ReadPrimitive() << 8) | reader.ReadPrimitive()); - - return first switch + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadCompressedUInt() { - 0b11110000 => reader.ReadPrimitive(), - 0b11111110 => uint.MaxValue - 1, - 0b11111111 => uint.MaxValue, - _ => throw new InvalidDataException("Invalid compressed uint") - }; + var first = reader.ReadPrimitive(); + + if ((first & 0b10000000) == 0b00000000) + return first; + + if ((first & 0b11000000) == 0b10000000) + return (uint)(((first & ~0b10000000) << 8) | reader.ReadPrimitive()); + + if ((first & 0b11100000) == 0b11000000) + return (uint)(((first & ~0b11000000) << 24) | (reader.ReadPrimitive() << 16) | (reader.ReadPrimitive() << 8) | reader.ReadPrimitive()); + + return first switch + { + 0b11110000 => reader.ReadPrimitive(), + 0b11111110 => uint.MaxValue - 1, + 0b11111111 => uint.MaxValue, + _ => throw new InvalidDataException("Invalid compressed uint") + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadCompressedInt() + { + var value = reader.ReadCompressedUInt(); + if (value == uint.MaxValue) + return int.MinValue; + + var isNegative = (value & 0b1) == 1; + value >>= 1; + + return (int)(isNegative ? -(value + 1) : value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadSLEB128() + { + var value = 0uL; + var shift = 0; + byte current; + + do + { + current = reader.ReadPrimitive(); + value |= (current & 0x7FuL) << shift; + shift += 7; + } while ((current & 0x80) != 0); + + if (64 >= shift && (current & 0x40) != 0) + value |= ulong.MaxValue << shift; + + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ReadBoolean() + => reader.ReadPrimitive() != 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ReadCompressedInt(this ref T reader) where T : struct, IReader, allows ref struct + extension(TReader reader) where TReader : INonSeekableReader, allows ref struct { - var value = reader.ReadCompressedUInt(); - if (value == uint.MaxValue) - return int.MinValue; - - var isNegative = (value & 0b1) == 1; - value >>= 1; - - return (int)(isNegative ? -(value + 1) : value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong ReadSLEB128(this ref T reader) where T : struct, IReader, allows ref struct - { - var value = 0uL; - var shift = 0; - byte current; - - do - { - current = reader.ReadPrimitive(); - value |= (current & 0x7FuL) << shift; - shift += 7; - } while ((current & 0x80) != 0); - - if (64 >= shift && (current & 0x40) != 0) - value |= ulong.MaxValue << shift; - - return value; + public LittleEndianReader AsLittleEndian() => new(reader); + public BigEndianReader AsBigEndian() => new(reader); } } \ No newline at end of file diff --git a/VersionedSerialization/Reader`1.cs b/VersionedSerialization/Reader`1.cs new file mode 100644 index 0000000..5987290 --- /dev/null +++ b/VersionedSerialization/Reader`1.cs @@ -0,0 +1,107 @@ +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace VersionedSerialization; + +public ref struct Reader(TReader impl, ReaderConfig config = default) + where TReader : IReader, allows ref struct +{ + private TReader _impl = impl; + public ReaderConfig Config { get; } = config; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadString(int length = -1, Encoding? encoding = null) + => _impl.ReadString(length, encoding); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan ReadBytes(long length) + => _impl.ReadBytes(length); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Read() where T : unmanaged + { + Unsafe.SkipInit(out T value); + Read(ref value); + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Read(scoped ref T value) where T : unmanaged + => Read(new Span(ref value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Read(scoped Span dest) where T : unmanaged + => _impl.Read(dest); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T ReadPrimitive() where T : unmanaged + { + Unsafe.SkipInit(out T value); + ReadPrimitive(ref value); + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadPrimitive(scoped ref T value) where T : unmanaged + => ReadPrimitive(new Span(ref value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadPrimitive(scoped Span dest) where T : unmanaged + => _impl.ReadPrimitive(dest); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray ReadPrimitiveArray(long count) where T : unmanaged + { + var data = GC.AllocateUninitializedArray((int)count); + ReadPrimitive(data); + return ImmutableCollectionsMarshal.AsImmutableArray(data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T ReadVersionedObject(in StructVersion version = default) where T : IReadable, new() + { + var obj = new T(); + ReadVersionedObject(ref obj, version); + return obj; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadVersionedObject(scoped ref T dest, in StructVersion version = default) + where T : IReadable + { + dest.Read(ref this, in version); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadVersionedObject(scoped Span dest, in StructVersion version = default) + where T : IReadable + { + for (int i = 0; i < dest.Length; i++) + { + ReadVersionedObject(ref dest[i], version); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray ReadVersionedObjectArray(long count, in StructVersion version = default) + where T : IReadable, new() + { + var array = GC.AllocateUninitializedArray((int)count); + ReadVersionedObject(array, in version); + return ImmutableCollectionsMarshal.AsImmutableArray(array); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadNativeUInt() + => Config.Is32Bit + ? ReadPrimitive() + : ReadPrimitive(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long ReadNativeInt() + => Config.Is32Bit + ? ReadPrimitive() + : ReadPrimitive(); +} diff --git a/VersionedSerialization/SeekableReaderExtensions.cs b/VersionedSerialization/SeekableReaderExtensions.cs new file mode 100644 index 0000000..3800767 --- /dev/null +++ b/VersionedSerialization/SeekableReaderExtensions.cs @@ -0,0 +1,63 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using VersionedSerialization.Impl; + +namespace VersionedSerialization; + +file static class ReaderAccessors + where TReader : IReader, allows ref struct +{ + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_impl")] + public static extern ref TReader GetImpl(ref Reader obj); +} + +public static class SeekableReaderExtensions +{ + extension(ref Reader reader) + where TReader : ISeekableReader, allows ref struct + { + public int Offset + { + get => ReaderAccessors.GetImpl(ref reader).Offset; + set => ReaderAccessors.GetImpl(ref reader).Offset = value; + } + + public int Length => ReaderAccessors.GetImpl(ref reader).Length; + + public void Align(int alignment = 0) + { + if (alignment == 0) + { + alignment = reader.Config.Is32Bit + ? sizeof(int) + : sizeof(long); + } + + if (BitOperations.IsPow2(alignment)) + { + reader.Offset = (reader.Offset + (alignment - 1)) & ~(alignment - 1); + } + else + { + var offset = reader.Offset; + + var rem = offset % alignment; + if (rem != 0) + { + reader.Offset += alignment - rem; + } + } + } + + public void Skip(int count) + { + reader.Offset += count; + } + } + + extension(TReader reader) where TReader : ISeekableReader, allows ref struct + { + public LittleEndianSeekableReader AsLittleEndian() => new(reader); + public BigEndianSeekableReader AsBigEndian() => new(reader); + } +} \ No newline at end of file diff --git a/VersionedSerialization/SpanReader.cs b/VersionedSerialization/SpanReader.cs deleted file mode 100644 index 6402cd4..0000000 --- a/VersionedSerialization/SpanReader.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Buffers.Binary; -using System.Collections.Immutable; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; - -namespace VersionedSerialization; - -// ReSharper disable ReplaceSliceWithRangeIndexer | The range indexer gets compiled into .Slice(x, y) and not .Slice(x) which worsens performance -public ref struct SpanReader(ReadOnlySpan data, int offset = 0, bool littleEndian = true, bool is32Bit = false) : IReader -{ - public int Offset = offset; - public readonly byte Peek => _data[Offset]; - public readonly bool IsLittleEndian => _littleEndian; - public readonly bool Is32Bit => _is32Bit; - public readonly int Length => _data.Length; - public readonly int PointerSize => Is32Bit ? sizeof(uint) : sizeof(ulong); - - private readonly ReadOnlySpan _data = data; - private readonly bool _littleEndian = littleEndian; - private readonly bool _is32Bit = is32Bit; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private T ReadInternal() where T : unmanaged - { - var value = MemoryMarshal.Read(_data.Slice(Offset)); - Offset += Unsafe.SizeOf(); - return value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TTo Cast(in TFrom from) => Unsafe.As(ref Unsafe.AsRef(in from)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan ReadBytes(int length) - { - var val = _data.Slice(Offset, length); - Offset += length; - return val; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T ReadPrimitive() where T : unmanaged - { - if (typeof(T) == typeof(byte)) - return Cast(_data[Offset++]); - - var value = ReadInternal(); - if (!_littleEndian) - { - if (value is ulong val) - { - var converted = BinaryPrimitives.ReverseEndianness(val); - value = Cast(converted); - } - else if (typeof(T) == typeof(long)) - { - var converted = BinaryPrimitives.ReverseEndianness(Cast(value)); - value = Cast(converted); - } - else if (typeof(T) == typeof(uint)) - { - var converted = BinaryPrimitives.ReverseEndianness(Cast(value)); - value = Cast(converted); - } - else if (typeof(T) == typeof(int)) - { - var converted = BinaryPrimitives.ReverseEndianness(Cast(value)); - value = Cast(converted); - } - else if (typeof(T) == typeof(ushort)) - { - var converted = BinaryPrimitives.ReverseEndianness(Cast(value)); - value = Cast(converted); - } - else if (typeof(T) == typeof(short)) - { - var converted = BinaryPrimitives.ReverseEndianness(Cast(value)); - value = Cast(converted); - } - } - - return value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ImmutableArray ReadPrimitiveArray(long count) where T : unmanaged - { - var array = ImmutableArray.CreateBuilder(checked((int)count)); - for (long i = 0; i < count; i++) - array.Add(ReadPrimitive()); - - return array.MoveToImmutable(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T ReadVersionedObject(in StructVersion version = default) where T : IReadable, new() - { - var obj = new T(); - obj.Read(ref this, in version); - return obj; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ImmutableArray ReadVersionedObjectArray(long count, in StructVersion version = default) where T : IReadable, new() - { - var array = ImmutableArray.CreateBuilder(checked((int)count)); - for (long i = 0; i < count; i++) - array.Add(ReadVersionedObject(in version)); - - return array.MoveToImmutable(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadString() - { - var length = _data.Slice(Offset).IndexOf(byte.MinValue); - - if (length == -1) - throw new InvalidDataException("Failed to find string in span."); - - var val = _data.Slice(Offset, length); - Offset += length + 1; // Skip null terminator - - return Encoding.UTF8.GetString(val); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ReadBoolean() => ReadPrimitive() != 0; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ulong ReadNUInt() => _is32Bit ? ReadPrimitive() : ReadPrimitive(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long ReadNInt() => _is32Bit ? ReadPrimitive() : ReadPrimitive(); - - public void Align(int alignment = 0) - { - if (alignment == 0) - alignment = Is32Bit ? 4 : 8; - - var rem = Offset % alignment; - if (rem != 0) - Offset += alignment - rem; - } - - public void Skip(int count) - { - Offset += count; - } -} \ No newline at end of file diff --git a/VersionedSerialization/StructVersion.cs b/VersionedSerialization/StructVersion.cs index c7a9f17..149a66c 100644 --- a/VersionedSerialization/StructVersion.cs +++ b/VersionedSerialization/StructVersion.cs @@ -1,6 +1,10 @@ -namespace VersionedSerialization; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; -public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = null) : IEquatable +namespace VersionedSerialization; + +public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = null) + : IEquatable, IParsable { public readonly int Major = major; public readonly int Minor = minor; @@ -8,6 +12,63 @@ public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = public double AsDouble => Major + Minor / 10.0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasTag(string tag) + => Tag != null && Tag.Contains(tag, StringComparison.Ordinal); + + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out StructVersion result) + { + if (s == null) + { + result = default; + return false; + } + + var versionParts = s.Split('.'); + if (versionParts.Length > 2) + { + result = default; + return false; + } + + if (versionParts.Length == 1) + { + if (!int.TryParse(versionParts[0], out var version)) + { + result = default; + return false; + } + + result = new StructVersion(version); + return true; + } + + var tagParts = versionParts[1].Split("-"); + if (tagParts.Length > 2) + { + result = default; + return false; + } + + var major = int.Parse(versionParts[0]); + var minor = int.Parse(tagParts[0]); + var tag = tagParts.ElementAtOrDefault(1); + + result = new StructVersion(major, minor, tag); + return true; + } + + public static StructVersion Parse(string s, IFormatProvider? provider = null) => + TryParse(s, provider, out var version) + ? version + : throw new InvalidOperationException($"Failed to parse {s} as a StructVersion."); + + public static implicit operator StructVersion(string value) + => Parse(value); + + public static implicit operator StructVersion(double value) + => new((int)value, (int)((value - (int)value) * 10.0)); + #region Equality operators public static bool operator ==(StructVersion left, StructVersion right) @@ -40,29 +101,4 @@ public readonly struct StructVersion(int major = 0, int minor = 0, string? tag = #endregion public override string ToString() => $"{Major}.{Minor}{(Tag != null ? $"-{Tag}" : "")}"; - - public static implicit operator StructVersion(string value) - { - var versionParts = value.Split('.'); - if (versionParts.Length > 2) - throw new InvalidOperationException("Invalid version string."); - - if (versionParts.Length == 1) - { - if (!int.TryParse(versionParts[0], out var version)) - throw new InvalidOperationException("Invalid single-number version string."); - - return new StructVersion(version); - } - - var tagParts = versionParts[1].Split("-"); - if (tagParts.Length > 2) - throw new InvalidOperationException("Invalid version string."); - - var major = int.Parse(versionParts[0]); - var minor = int.Parse(tagParts[0]); - var tag = tagParts.Length == 1 ? null : tagParts[1]; - - return new StructVersion(major, minor, tag); - } } \ No newline at end of file diff --git a/VersionedSerialization/VersionedSerialization.csproj b/VersionedSerialization/VersionedSerialization.csproj index 7322890..1380809 100644 --- a/VersionedSerialization/VersionedSerialization.csproj +++ b/VersionedSerialization/VersionedSerialization.csproj @@ -1,16 +1,32 @@  + net10.0 enable enable + preview + True + LukeFZ + Library and source generator to allow fast reading of structs with version-specific layouts. + + https://github.com/LukeFZ/VersionedSerialization + https://github.com/LukeFZ/VersionedSerialization + + MIT + 0.0.12 + True - - True - + + + - - True - + + + false + Content + PreserveNewest + +