~ruther/NosSmooth

c3d5b45b65d58084e5511b1089ce9a2d9557ccab — František Boháček 3 years ago 32a1844
feat: use ReadOnlySpan for deserialization instead of strings
37 files changed, 248 insertions(+), 224 deletions(-)

M Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BoolInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/EnumInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/FallbackInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/StringInlineConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs
M Core/NosSmooth.Packets/Converters/BaseStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/BoolStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/ByteStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/CharStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/IntStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/LongStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/ShortStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/StringTypeConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/UIntStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/ULongStringConverter.cs
M Core/NosSmooth.Packets/Converters/Basic/UShortStringConverter.cs
M Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs
M Core/NosSmooth.Packets/Converters/IStringConverter.cs
M Core/NosSmooth.Packets/Converters/IStringSerializer.cs
M Core/NosSmooth.Packets/Converters/Packets/UpgradeRareSubPacketConverter.cs
A Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Special/NullableTypeConverter.cs
A Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs
M Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs
M Core/NosSmooth.Packets/NosSmooth.Packets.csproj
M Core/NosSmooth.Packets/PacketSerializer.cs
M Core/NosSmooth.Packets/PacketStringEnumerator.cs
M Core/NosSmooth.Packets/PacketToken.cs
M Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs
M Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs
M Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs +1 -1
@@ 94,7 94,7 @@ public class ConverterDeserializationGenerator
        _textWriter.WriteLine($@"while ({_stringEnumeratorVariable}.IsOnLastToken() == false)");
        _textWriter.WriteLine("{");
        _textWriter.Indent++;
        _textWriter.WriteLine($"{_stringEnumeratorVariable}.GetNextToken();");
        _textWriter.WriteLine($"{_stringEnumeratorVariable}.GetNextToken(out _);");
        _textWriter.Indent--;
        _textWriter.WriteLine("}");
    }

M Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs +10 -0
@@ 38,6 38,16 @@ public static class ParameterInfoExtensions
    }

    /// <summary>
    /// Gets the name of the token variable.
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>
    /// <returns>The name of the token variable.</returns>
    public static string GetTokenVariableName(this ParameterInfo parameterInfo)
    {
        return $"{parameterInfo.Name}Token";
    }

    /// <summary>
    /// Gets the name of the error variable.
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs +1 -1
@@ 91,4 91,4 @@ public static Result<{type}?> ParseBasic{type}(IStringConverter typeConverter, P
");
        }
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BoolInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BoolInlineConverterGenerator.cs +1 -1
@@ 70,4 70,4 @@ public static Result<bool?> ParseBool(PacketStringEnumerator stringEnumerator)
}}
");
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/EnumInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/EnumInlineConverterGenerator.cs +1 -1
@@ 105,4 105,4 @@ public static Result<{type}?> ParseEnum{type.ToString().Replace('.', '_')}(IStri
            );
        }
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/FallbackInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/FallbackInlineConverterGenerator.cs +1 -1
@@ 58,4 58,4 @@ public class FallbackInlineConverterGenerator : IInlineConverterGenerator
    {
        // ignore
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/StringInlineConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/StringInlineConverterGenerator.cs +1 -1
@@ 53,4 53,4 @@ public static Result<string?> ParseString(PacketStringEnumerator stringEnumerato
"
        );
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs +3 -3
@@ 95,7 95,7 @@ public override Result Serialize({_packetInfo.Name}? obj, PacketStringBuilder bu
}}

/// <inheritdoc />
public override Result<{_packetInfo.Name}?> Deserialize(PacketStringEnumerator stringEnumerator)
public override Result<{_packetInfo.Name}?> Deserialize(ref PacketStringEnumerator stringEnumerator)
{{
    var typeConverter = this;
"


@@ 176,7 176,7 @@ public override Result<{_packetInfo.Name}?> Deserialize(PacketStringEnumerator s
                    textWriter.WriteLine("IResultError? skipError;");
                    skipped = true;
                }
                textWriter.WriteLine($@"skipResult = stringEnumerator.GetNextToken();");
                textWriter.WriteLine($@"skipResult = stringEnumerator.GetNextToken(out _);");
                textWriter.WriteLine
                    ("skipError = CheckDeserializationResult(result, \"None\", stringEnumerator, false);");
                textWriter.WriteMultiline


@@ 246,4 246,4 @@ public override Result<{_packetInfo.Name}?> Deserialize(PacketStringEnumerator s

        return null;
    }
}
\ No newline at end of file
}

M Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs +5 -0
@@ 130,6 130,11 @@ public class SourceGenerator : ISourceGenerator
                    $"{packetRecord.GetPrefix()}.{packetRecord.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs",
                    stringWriter.GetStringBuilder().ToString()
                );
                File.WriteAllText
                (
                    $"/tmp/{packetRecord.GetPrefix()}.{packetRecord.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs",
                    stringWriter.GetStringBuilder().ToString()
                );
            }
        }


M Core/NosSmooth.Packets/Converters/BaseStringConverter.cs => Core/NosSmooth.Packets/Converters/BaseStringConverter.cs +4 -4
@@ 19,12 19,12 @@ public abstract class BaseStringConverter<TParseType> : IStringConverter<TParseT
    public abstract Result Serialize(TParseType? obj, PacketStringBuilder builder);

    /// <inheritdoc />
    public abstract Result<TParseType?> Deserialize(PacketStringEnumerator stringEnumerator);
    public abstract Result<TParseType?> Deserialize(ref PacketStringEnumerator stringEnumerator);

    /// <inheritdoc/>
    Result<object?> IStringConverter.Deserialize(PacketStringEnumerator stringEnumerator)
    Result<object?> IStringConverter.Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var result = Deserialize(stringEnumerator);
        var result = Deserialize(ref stringEnumerator);
        if (!result.IsSuccess)
        {
            return Result<object?>.FromError(result);


@@ 43,4 43,4 @@ public abstract class BaseStringConverter<TParseType> : IStringConverter<TParseT

        return Serialize(parseType, builder);
    }
}
\ No newline at end of file
}

M Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs +5 -5
@@ 23,20 23,20 @@ public abstract class BasicTypeConverter<TBasicType> : BaseStringConverter<TBasi
    }

    /// <inheritdoc />
    public override Result<TBasicType?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<TBasicType?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var nextTokenResult = stringEnumerator.GetNextToken();
        var nextTokenResult = stringEnumerator.GetNextToken(out var packetToken);
        if (!nextTokenResult.IsSuccess)
        {
            return Result<TBasicType?>.FromError(nextTokenResult);
        }

        if (nextTokenResult.Entity.Token == "-")
        if (packetToken.Token[0] == '-' && packetToken.Token.Length == 1)
        {
            return Result<TBasicType?>.FromSuccess(default);
        }

        return Deserialize(nextTokenResult.Entity.Token);
        return Deserialize(packetToken.Token);
    }

    /// <summary>


@@ 44,5 44,5 @@ public abstract class BasicTypeConverter<TBasicType> : BaseStringConverter<TBasi
    /// </summary>
    /// <param name="value">The value to deserialize.</param>
    /// <returns>The deserialized value or an error.</returns>
    protected abstract Result<TBasicType?> Deserialize(string value);
    protected abstract Result<TBasicType?> Deserialize(ReadOnlySpan<char> value);
}
\ No newline at end of file

M Core/NosSmooth.Packets/Converters/Basic/BoolStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/BoolStringConverter.cs +3 -8
@@ 22,19 22,14 @@ public class BoolStringConverter : BaseStringConverter<bool>
    }

    /// <inheritdoc />
    public override Result<bool> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<bool> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var nextTokenResult = stringEnumerator.GetNextToken();
        var nextTokenResult = stringEnumerator.GetNextToken(out var packetToken);
        if (!nextTokenResult.IsSuccess)
        {
            return Result<bool>.FromError(nextTokenResult);
        }

        if (nextTokenResult.Entity.Token == "-")
        {
            return Result<bool>.FromSuccess(default);
        }

        return nextTokenResult.Entity.Token == "1" ? true : false;
        return packetToken.Token[0] == '1' ? true : false;
    }
}
\ No newline at end of file

M Core/NosSmooth.Packets/Converters/Basic/ByteStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ByteStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class ByteStringConverter : BasicTypeConverter<byte>
{
    /// <inheritdoc />
    protected override Result<byte> Deserialize(string value)
    protected override Result<byte> Deserialize(ReadOnlySpan<char> value)
    {
        if (!byte.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an byte.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as an byte.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/CharStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/CharStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class CharStringConverter : BasicTypeConverter<char>
{
    /// <inheritdoc />
    protected override Result<char> Deserialize(string value)
    protected override Result<char> Deserialize(ReadOnlySpan<char> value)
    {
        if (value.Length != 1)
        {
            return new CouldNotConvertError(this, value, "The token is not one character long.");
            return new CouldNotConvertError(this, value.ToString(), "The token is not one character long.");
        }

        return value[0];

M Core/NosSmooth.Packets/Converters/Basic/IntStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/IntStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class IntStringConverter : BasicTypeConverter<int>
{
    /// <inheritdoc />
    protected override Result<int> Deserialize(string value)
    protected override Result<int> Deserialize(ReadOnlySpan<char> value)
    {
        if (!int.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as int.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as int.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/LongStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/LongStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class LongStringConverter : BasicTypeConverter<long>
{
    /// <inheritdoc />
    protected override Result<long> Deserialize(string value)
    protected override Result<long> Deserialize(ReadOnlySpan<char> value)
    {
        if (!long.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as a long.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as a long.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/ShortStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ShortStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class ShortStringConverter : BasicTypeConverter<short>
{
    /// <inheritdoc />
    protected override Result<short> Deserialize(string value)
    protected override Result<short> Deserialize(ReadOnlySpan<char> value)
    {
        if (!short.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as short.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as short.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/StringTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/StringTypeConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;


@@ 14,8 15,8 @@ namespace NosSmooth.Packets.Converters.Basic;
public class StringTypeConverter : BasicTypeConverter<string>
{
    /// <inheritdoc />
    protected override Result<string?> Deserialize(string value)
    protected override Result<string?> Deserialize(ReadOnlySpan<char> value)
    {
        return value;
        return value.ToString();
    }
}
\ No newline at end of file

M Core/NosSmooth.Packets/Converters/Basic/UIntStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/UIntStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class UIntStringConverter : BasicTypeConverter<uint>
{
    /// <inheritdoc />
    protected override Result<uint> Deserialize(string value)
    protected override Result<uint> Deserialize(ReadOnlySpan<char> value)
    {
        if (!uint.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as uint");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as uint");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/ULongStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ULongStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class ULongStringConverter : BasicTypeConverter<ulong>
{
    /// <inheritdoc />
    protected override Result<ulong> Deserialize(string value)
    protected override Result<ulong> Deserialize(ReadOnlySpan<char> value)
    {
        if (!ulong.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an ulong.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as an ulong.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Basic/UShortStringConverter.cs => Core/NosSmooth.Packets/Converters/Basic/UShortStringConverter.cs +3 -2
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;



@@ 15,11 16,11 @@ namespace NosSmooth.Packets.Converters.Basic;
public class UShortStringConverter : BasicTypeConverter<ushort>
{
    /// <inheritdoc />
    protected override Result<ushort> Deserialize(string value)
    protected override Result<ushort> Deserialize(ReadOnlySpan<char> value)
    {
        if (!ushort.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an ushort.");
            return new CouldNotConvertError(this, value.ToString(), "Could not parse as an ushort.");
        }

        return parsed;

M Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs => Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs +4 -4
@@ 29,19 29,19 @@ public class NameStringConverter : BaseStringConverter<NameString>
    }

    /// <inheritdoc />
    public override Result<NameString?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<NameString?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        if (!tokenResult.IsSuccess)
        {
            return Result<NameString?>.FromError(tokenResult);
        }

        if (tokenResult.Entity.Token == "-")
        if (packetToken.Token[0] == '-' && packetToken.Token.Length == 1)
        {
            return Result<NameString?>.FromSuccess(null);
        }

        return NameString.FromPacket(tokenResult.Entity.Token);
        return NameString.FromPacket(packetToken.Token.ToString());
    }
}
\ No newline at end of file

M Core/NosSmooth.Packets/Converters/IStringConverter.cs => Core/NosSmooth.Packets/Converters/IStringConverter.cs +2 -2
@@ 18,7 18,7 @@ public interface IStringConverter
    /// </summary>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public Result<object?> Deserialize(PacketStringEnumerator stringEnumerator);
    public Result<object?> Deserialize(ref PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.


@@ 43,7 43,7 @@ public interface IStringConverter<TParseType> : IStringConverter
    /// </summary>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public new Result<TParseType?> Deserialize(PacketStringEnumerator stringEnumerator);
    public new Result<TParseType?> Deserialize(ref PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.

M Core/NosSmooth.Packets/Converters/IStringSerializer.cs => Core/NosSmooth.Packets/Converters/IStringSerializer.cs +2 -2
@@ 20,7 20,7 @@ public interface IStringSerializer
    /// <param name="parseType">The type of the object to serialize.</param>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public Result<object?> Deserialize(Type parseType, PacketStringEnumerator stringEnumerator);
    public Result<object?> Deserialize(Type parseType, ref PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.


@@ 37,7 37,7 @@ public interface IStringSerializer
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <typeparam name="TParseType">The type of the object to serialize.</typeparam>
    /// <returns>The parsed object or an error.</returns>
    public Result<TParseType?> Deserialize<TParseType>(PacketStringEnumerator stringEnumerator);
    public Result<TParseType?> Deserialize<TParseType>(ref PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.

M Core/NosSmooth.Packets/Converters/Packets/UpgradeRareSubPacketConverter.cs => Core/NosSmooth.Packets/Converters/Packets/UpgradeRareSubPacketConverter.cs +8 -8
@@ 28,31 28,31 @@ public class UpgradeRareSubPacketConverter : BaseStringConverter<UpgradeRareSubP
    }

    /// <inheritdoc />
    public override Result<UpgradeRareSubPacket?> Deserialize(PacketStringEnumerator stringEnumerator)
    public override Result<UpgradeRareSubPacket?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        if (!tokenResult.IsSuccess)
        {
            return Result<UpgradeRareSubPacket?>.FromError(tokenResult);
        }
        var token = packetToken.Token;

        var token = tokenResult.Entity.Token;
        if (token.Length > 3)
        {
            return new CouldNotConvertError(this, token, "The string is not two/three characters long.");
            return new CouldNotConvertError(this, token.ToString(), "The string is not two/three characters long.");
        }

        var upgradeString = token.Substring(0, token.Length - 1);
        var rareString = token[token.Length - 1].ToString();
        var upgradeString = token.Slice(0, token.Length - 1);
        var rareString = token.Slice(token.Length - 1);

        if (!byte.TryParse(upgradeString, out var upgrade))
        {
            return new CouldNotConvertError(this, upgradeString, "Could not parse as byte");
            return new CouldNotConvertError(this, upgradeString.ToString(), "Could not parse as byte");
        }

        if (!sbyte.TryParse(rareString, out var rare))
        {
            return new CouldNotConvertError(this, rareString, "Could not parse as byte");
            return new CouldNotConvertError(this, rareString.ToString(), "Could not parse as byte");
        }

        return new UpgradeRareSubPacket(upgrade, rare);

A Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs => Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs +0 -0
A Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs => Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs +0 -0
A Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs => Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs +0 -0
A Core/NosSmooth.Packets/Converters/Special/NullableTypeConverter.cs => Core/NosSmooth.Packets/Converters/Special/NullableTypeConverter.cs +0 -0
A Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs => Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs +0 -0
M Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs => Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs +1 -0
@@ 5,6 5,7 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Buffers;
using NosSmooth.Packets.Converters;
using Remora.Results;


M Core/NosSmooth.Packets/NosSmooth.Packets.csproj => Core/NosSmooth.Packets/NosSmooth.Packets.csproj +1 -1
@@ 2,9 2,9 @@

    <PropertyGroup>
        <LangVersion>10</LangVersion>
        <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
        <AssemblyName>NosSmooth.Packets</AssemblyName>
        <RootNamespace>NosSmooth.Packets</RootNamespace>
        <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
    </PropertyGroup>

    <ItemGroup>

M Core/NosSmooth.Packets/PacketSerializer.cs => Core/NosSmooth.Packets/PacketSerializer.cs +3 -3
@@ 59,20 59,20 @@ public class PacketSerializer : IPacketSerializer
    public Result<IPacket> Deserialize(string packetString, PacketSource preferredSource)
    {
        var packetStringEnumerator = new PacketStringEnumerator(packetString);
        var headerTokenResult = packetStringEnumerator.GetNextToken();
        var headerTokenResult = packetStringEnumerator.GetNextToken(out var packetToken);
        if (!headerTokenResult.IsSuccess)
        {
            return Result<IPacket>.FromError(headerTokenResult);
        }

        var packetInfoResult = _packetTypesRepository.FindPacketInfo(headerTokenResult.Entity.Token, preferredSource);
        var packetInfoResult = _packetTypesRepository.FindPacketInfo(packetToken.Token.ToString(), preferredSource);
        if (!packetInfoResult.IsSuccess)
        {
            return Result<IPacket>.FromError(packetInfoResult);
        }

        var packetInfo = packetInfoResult.Entity;
        var deserializedResult = packetInfo.PacketConverter.Deserialize(packetStringEnumerator);
        var deserializedResult = packetInfo.PacketConverter.Deserialize(ref packetStringEnumerator);
        if (!deserializedResult.IsSuccess)
        {
            return Result<IPacket>.FromError(deserializedResult);

M Core/NosSmooth.Packets/PacketStringEnumerator.cs => Core/NosSmooth.Packets/PacketStringEnumerator.cs +47 -91
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Text;
using NosSmooth.Packets.Errors;


@@ 14,15 15,18 @@ namespace NosSmooth.Packets;
/// <summary>
/// Enumerator for packet strings.
/// </summary>
public struct PacketStringEnumerator
public ref struct PacketStringEnumerator
{
    private readonly EnumeratorData _data;
    private readonly ReadOnlySpan<char> _data;
    private readonly Dictionary<char, ushort> _numberOfSeparators;
    private EnumeratorLevel _currentLevel;
    private (char Separator, uint? MaxTokens)? _preparedLevel;
    private PacketToken? _currentToken;
    private bool _currentTokenRead;
    private PacketToken _currentToken;
    private bool _readToLast;

    private int _cursor;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketStringEnumerator"/> struct.
    /// </summary>


@@ 31,30 35,14 @@ public struct PacketStringEnumerator
    public PacketStringEnumerator(string data, char separator = ' ')
    {
        _currentLevel = new EnumeratorLevel(null, separator);
        _data = new EnumeratorData(data);
        _data = new ReadOnlySpan<char>(data.ToCharArray());
        _cursor = 0;
        _numberOfSeparators = new Dictionary<char, ushort>();
        _numberOfSeparators.Add(separator, 1);
        _currentToken = null;
        _preparedLevel = null;
        _readToLast = false;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketStringEnumerator"/> struct.
    /// </summary>
    /// <param name="data">The data of the enumerator.</param>
    /// <param name="level">The current enumerator level.</param>
    /// <param name="numberOfSeparators">The number of separators.</param>
    private PacketStringEnumerator(EnumeratorData data, EnumeratorLevel level, Dictionary<char, ushort> numberOfSeparators)
    {
        _currentLevel = level;
        _data = data;

        // TODO: use something less heavy than copying everything from the dictionary.
        _numberOfSeparators = new Dictionary<char, ushort>(numberOfSeparators);
        _currentToken = null;
        _currentToken = new PacketToken(default, default, default, default);
        _preparedLevel = null;
        _readToLast = false;
        _currentTokenRead = false;
    }

    /// <summary>


@@ 66,7 54,7 @@ public struct PacketStringEnumerator
    /// <param name="separator">The separator to look for.</param>
    public void SetAfterSeparatorOnce(char separator)
    {
        _currentToken = null;
        _currentTokenRead = false;
        _currentLevel.SeparatorOnce = separator;
    }



@@ 97,19 85,6 @@ public struct PacketStringEnumerator
    }

    /// <summary>
    /// Create next level with the separator given in the prepared level.
    /// </summary>
    /// <remarks>
    /// Level of the current enumerator will stay the same.
    /// Will return null, if there is not a level prepared.
    /// </remarks>
    /// <returns>An enumerator with the new level pushed.</returns>
    public PacketStringEnumerator? CreatePreparedLevel()
    {
        return _preparedLevel is not null ? CreateLevel(_preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens) : null;
    }

    /// <summary>
    /// Push next level with the separator given in the prepared level.
    /// </summary>
    /// <returns>Whether there is a prepared level present.</returns>


@@ 120,7 95,7 @@ public struct PacketStringEnumerator
            return false;
        }

        _currentToken = null;
        _currentTokenRead = false;
        _currentLevel = new EnumeratorLevel(_currentLevel, _preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens)
        {
            ReachedEnd = _currentLevel.ReachedEnd


@@ 136,25 111,6 @@ public struct PacketStringEnumerator
    }

    /// <summary>
    /// Create next level with the given separator and maximum number of tokens.
    /// </summary>
    /// <remarks>
    /// Level of the current enumerator will stay the same.
    /// The maximum number of tokens indicates how many tokens can be read ie. in lists,
    /// the enumerator won't allow reading more than that many tokens, error will be thrown if the user tries to read more.
    /// </remarks>
    /// <param name="separator">The separator of the new level.</param>
    /// <param name="maxTokens">The maximum number of tokens to read.</param>
    /// <returns>An enumerator with the new level pushed.</returns>
    public PacketStringEnumerator CreateLevel(char separator, uint? maxTokens = default)
    {
        _currentToken = null;
        var stringEnumerator = new PacketStringEnumerator(_data, _currentLevel, _numberOfSeparators);
        stringEnumerator.PushLevel(separator, maxTokens);
        return stringEnumerator;
    }

    /// <summary>
    /// Push new separator level to the stack.
    /// </summary>
    /// <remarks>


@@ 166,7 122,7 @@ public struct PacketStringEnumerator
    public void PushLevel(char separator, uint? maxTokens = default)
    {
        _preparedLevel = null;
        _currentToken = null;
        _currentTokenRead = false;
        _currentLevel = new EnumeratorLevel(_currentLevel, separator, maxTokens)
        {
            ReachedEnd = _currentLevel.ReachedEnd


@@ 197,36 153,50 @@ public struct PacketStringEnumerator
    }

    /// <summary>
    /// Skip the given amount of characters.
    /// </summary>
    /// <param name="count">The count of characters to skip.</param>
    public void Skip(int count)
    {
        _cursor += count;
    }

    /// <summary>
    /// Get the next token.
    /// </summary>
    /// <param name="packetToken">The resulting token.</param>
    /// <param name="seek">Whether to seek the cursor to the end of the token.</param>
    /// <returns>The found token.</returns>
    public Result<PacketToken> GetNextToken(bool seek = true)
    public Result GetNextToken(out PacketToken packetToken, bool seek = true)
    {
        // The token is cached if seek was false to speed things up.
        if (_currentToken != null)
        if (_currentTokenRead)
        {
            var cachedToken = _currentToken.Value;
            var cachedToken = _currentToken;
            if (seek)
            {
                UpdateCurrentAndParentLevels(cachedToken);
                _currentLevel.TokensRead++;
                _currentToken = null;
                _data.Cursor += cachedToken.Token.Length + 1;
                _currentTokenRead = false;
                _cursor += cachedToken.Token.Length + 1;
                _currentLevel.SeparatorOnce = null;
            }

            return cachedToken;
            packetToken = new PacketToken(default, default, default, default);
            packetToken = cachedToken;
            return Result.FromSuccess();
        }

        if (_data.ReachedEnd || (_currentLevel.ReachedEnd ?? false))
        if ((_cursor >= _data.Length) || (_currentLevel.ReachedEnd ?? false))
        {
            return new PacketEndReachedError(_data.Data, _currentLevel.ReachedEnd ?? false);
            packetToken = new PacketToken(default, default, default, default);
            return new PacketEndReachedError(_data.ToString(), _currentLevel.ReachedEnd ?? false);
        }

        var currentIndex = _data.Cursor;
        char currentCharacter = _data.Data[currentIndex];
        StringBuilder tokenString = new StringBuilder();
        var currentIndex = _cursor;
        var length = 0;
        var startIndex = currentIndex;
        char currentCharacter = _data[currentIndex];

        bool? isLast, encounteredUpperLevel;



@@ 234,27 204,27 @@ public struct PacketStringEnumerator
        // If should read to last, then read until isLast is null or true.
        while (!IsSeparator(currentCharacter, out isLast, out encounteredUpperLevel) || (_readToLast && !(isLast ?? true)))
        {
            tokenString.Append(currentCharacter);
            length++;
            currentIndex++;

            if (currentIndex == _data.Data.Length)
            if (currentIndex >= _data.Length)
            {
                isLast = true;
                encounteredUpperLevel = true;
                break;
            }

            currentCharacter = _data.Data[currentIndex];
            currentCharacter = _data[currentIndex];
        }

        _readToLast = false;
        currentIndex++;

        var token = new PacketToken(tokenString.ToString(), isLast, encounteredUpperLevel, _data.ReachedEnd);
        var token = new PacketToken(_data.Slice(startIndex, length), isLast, encounteredUpperLevel, _cursor >= _data.Length);
        if (seek)
        {
            UpdateCurrentAndParentLevels(token);
            _data.Cursor = currentIndex;
            _cursor = currentIndex;
            _currentLevel.TokensRead++;
        }
        else


@@ 262,7 232,8 @@ public struct PacketStringEnumerator
            _currentToken = token;
        }

        return token;
        packetToken = token;
        return Result.FromSuccess();
    }

    /// <summary>


@@ 312,7 283,7 @@ public struct PacketStringEnumerator
    /// <returns>Whether the last token was read. Null if cannot determine (ie. there are multiple levels with the same separator.)</returns>
    public bool? IsOnLastToken()
    {
        if (_data.ReachedEnd)
        if (_cursor >= _data.Length)
        {
            return true;
        }


@@ 376,21 347,6 @@ public struct PacketStringEnumerator
        return true;
    }

    private class EnumeratorData
    {
        public EnumeratorData(string data)
        {
            Data = data;
            Cursor = 0;
        }

        public string Data { get; }

        public int Cursor { get; set; }

        public bool ReachedEnd => Cursor >= Data.Length;
    }

    private class EnumeratorLevel
    {
        public EnumeratorLevel(EnumeratorLevel? parent, char separator, uint? maxTokens = default)

M Core/NosSmooth.Packets/PacketToken.cs => Core/NosSmooth.Packets/PacketToken.cs +50 -6
@@ 4,6 4,7 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics.CodeAnalysis;

namespace NosSmooth.Packets;


@@ 11,9 12,52 @@ namespace NosSmooth.Packets;
/// <summary>
/// The single token from a packet.
/// </summary>
/// <param name="Token">The token.</param>
/// <param name="IsLast">Whether the token is last in the current level. Null if it cannot be determined.</param>
/// <param name="EncounteredUpperLevel">Whether the current separator was from an upper stack level than the parent. That could mean some kind of an error if not etc. at the end of parsing a last entry of a list and last entry of a subpacket.</param>
/// <param name="PacketEndReached">Whether the packet's end was reached.</param>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Record struct creates the underlying properties.")]
public readonly record struct PacketToken(string Token, bool? IsLast, bool? EncounteredUpperLevel, bool PacketEndReached);
\ No newline at end of file
[SuppressMessage
(
    "StyleCop.CSharp.NamingRules",
    "SA1313:Parameter names should begin with lower-case letter",
    Justification = "Record struct creates the underlying properties."
)]
public readonly ref struct PacketToken
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketToken"/> struct.
    /// </summary>
    /// <param name="token">The token.</param>
    /// <param name="isLast">Whether this is the last token in the current level.</param>
    /// <param name="encounteredUpperLevel">Whether upper level separator was encountered.</param>
    /// <param name="packetEndReached">Whether the packet end was reached.</param>
    public PacketToken
    (
        ReadOnlySpan<char> token,
        bool? isLast,
        bool? encounteredUpperLevel,
        bool packetEndReached
    )
    {
        Token = token;
        IsLast = isLast;
        EncounteredUpperLevel = encounteredUpperLevel;
        PacketEndReached = packetEndReached;
    }

    /// <summary>
    /// The token.
    /// </summary>
    public ReadOnlySpan<char> Token { get; }

    /// <summary>
    /// Whether the token is last in the current level. Null if it cannot be determined.
    /// </summary>
    public bool? IsLast { get; }

    /// <summary>
    /// Whether the current separator was from an upper stack level than the parent. That could mean some kind of an error if not etc. at the end of parsing a last entry of a list and last entry of a subpacket.
    /// </summary>
    public bool? EncounteredUpperLevel { get; }

    /// <summary>
    /// Whether the packet's end was reached.
    /// </summary>
    public bool PacketEndReached { get; }
}
\ No newline at end of file

M Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs => Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs +0 -1
@@ 55,7 55,6 @@ public class PacketStringBuilderTests
        stringBuilder.Append("a");
        stringBuilder.Append("b");
        stringBuilder.Append("c");
        stringBuilder.ReplaceWithParentSeparator();
        stringBuilder.PopLevel();
        stringBuilder.Append("d");


M Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs => Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs +67 -62
@@ 23,64 23,65 @@ public class PacketStringEnumeratorTests
    public void EnumeratorListComplexStringGivesCorrectResult()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1 11.12.13|14.15.16|17.18.19");
        var headerTokenResult = stringEnumerator.GetNextToken();
        var headerTokenResult = stringEnumerator.GetNextToken(out var packetToken);
        Assert.True(headerTokenResult.IsSuccess);
        Assert.False(headerTokenResult.Entity.PacketEndReached);
        Assert.NotNull(headerTokenResult.Entity.IsLast);
        Assert.NotNull(headerTokenResult.Entity.EncounteredUpperLevel);
        Assert.False(headerTokenResult.Entity.IsLast);
        Assert.False(headerTokenResult.Entity.EncounteredUpperLevel);
        Assert.Matches("in", headerTokenResult.Entity.Token);

        var firstToken = stringEnumerator.GetNextToken();
        Assert.False(packetToken.PacketEndReached);
        Assert.NotNull(packetToken.IsLast);
        Assert.NotNull(packetToken.EncounteredUpperLevel);
        Assert.False(packetToken.IsLast);
        Assert.False(packetToken.EncounteredUpperLevel);
        Assert.Matches("in", packetToken.Token.ToString());

        var firstToken = stringEnumerator.GetNextToken(out packetToken);
        Assert.True(firstToken.IsSuccess);
        Assert.False(firstToken.Entity.PacketEndReached);
        Assert.NotNull(firstToken.Entity.IsLast);
        Assert.NotNull(firstToken.Entity.EncounteredUpperLevel);
        Assert.False(firstToken.Entity.IsLast);
        Assert.False(firstToken.Entity.EncounteredUpperLevel);
        Assert.Matches("1", firstToken.Entity.Token);
        Assert.False(packetToken.PacketEndReached);
        Assert.NotNull(packetToken.IsLast);
        Assert.NotNull(packetToken.EncounteredUpperLevel);
        Assert.False(packetToken.IsLast);
        Assert.False(packetToken.EncounteredUpperLevel);
        Assert.Matches("1", packetToken.Token.ToString());

        var listEnumerator = stringEnumerator.CreateLevel('|');
        listEnumerator.PrepareLevel('.');
        stringEnumerator.PushLevel('|');
        stringEnumerator.PrepareLevel('.');

        for (int i = 0; i < 3; i++)
        {
            var preparedLevel = listEnumerator.CreatePreparedLevel();
            Assert.NotNull(preparedLevel);
            stringEnumerator.PushPreparedLevel();

            for (int j = 0; j < 3; j++)
            {
                string currentNum = (j + (i * 3) + 1 + 10).ToString();
                var currentToken = preparedLevel!.Value.GetNextToken();
                var currentToken = stringEnumerator.GetNextToken(out packetToken);
                Assert.True(currentToken.IsSuccess);
                Assert.False(currentToken.Entity.PacketEndReached);
                Assert.NotNull(currentToken.Entity.IsLast);
                Assert.NotNull(currentToken.Entity.EncounteredUpperLevel);
                Assert.False(packetToken.PacketEndReached);
                Assert.NotNull(packetToken.IsLast);
                Assert.NotNull(packetToken.EncounteredUpperLevel);
                if (j == 2 && i == 2)
                {
                    Assert.True(currentToken.Entity.EncounteredUpperLevel);
                    Assert.True(packetToken.EncounteredUpperLevel);
                }
                else
                {
                    Assert.False(currentToken.Entity.EncounteredUpperLevel);
                    Assert.False(packetToken.EncounteredUpperLevel);
                }

                if (j != 2)
                {
                    Assert.False(currentToken.Entity.IsLast);
                    Assert.False(packetToken.IsLast);
                }
                else
                {
                    Assert.True(currentToken.Entity.IsLast);
                    Assert.True(packetToken.IsLast);
                }

                Assert.Matches(currentNum, currentToken.Entity.Token);
                Assert.Matches(currentNum, packetToken.Token.ToString());
            }

            Assert.True(preparedLevel!.Value.IsOnLastToken());
            Assert.True(stringEnumerator.IsOnLastToken());
            stringEnumerator.PopLevel();
        }

        stringEnumerator.PopLevel();
        Assert.True(stringEnumerator.IsOnLastToken());
    }



@@ 91,18 92,18 @@ public class PacketStringEnumeratorTests
    public void EnumeratorDoesNotAllowOvereachingPacketEnd()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1 2 3 4");
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // in
        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 1
        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 2
        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 3
        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 4

        tokenResult = stringEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
    }


@@ 114,20 115,20 @@ public class PacketStringEnumeratorTests
    public void EnumeratorDoesNotAllowOvereachingListComplexTypeEnd()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2.2|3.3|4.4|5");
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.');
        var itemEnumerator = listEnumerator.CreateLevel('|');
        stringEnumerator.PushLevel('.');
        stringEnumerator.PushLevel('|');

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // 1

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        Assert.True(tokenResult.IsSuccess); // 2
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(packetToken.IsLast);

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
        Assert.True(((PacketEndReachedError)tokenResult.Error!).LevelEnd);


@@ 140,33 141,37 @@ public class PacketStringEnumeratorTests
    public void EnumeratorDoesNotAllowOvereachingListLength()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2.2|3.3|4.4|5");
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.', 2);
        var itemEnumerator = listEnumerator.CreateLevel('|');
        stringEnumerator.PushLevel('.', 2);
        stringEnumerator.PushLevel('|');

        // first item
        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(packetToken.IsLast);

        stringEnumerator.PopLevel();

        // second item
        itemEnumerator = listEnumerator.CreateLevel('|');
        tokenResult = itemEnumerator.GetNextToken();
        stringEnumerator.PushLevel('|');
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        stringEnumerator.GetNextToken(out packetToken);
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(packetToken.IsLast);

        stringEnumerator.PopLevel();

        // cannot reach third item
        Assert.True(listEnumerator.IsOnLastToken());
        itemEnumerator = listEnumerator.CreateLevel('|');
        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(stringEnumerator.IsOnLastToken());
        stringEnumerator.PushLevel('|');
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
        Assert.True(((PacketEndReachedError)tokenResult.Error!).LevelEnd);


@@ 179,18 184,18 @@ public class PacketStringEnumeratorTests
    public void EnumeratorReturnsEncounteredUpperLevel()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2 1");
        var tokenResult = stringEnumerator.GetNextToken();
        var tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.');
        var itemEnumerator = listEnumerator.CreateLevel('|');
        stringEnumerator.PushLevel('.');
        stringEnumerator.PushLevel('|');

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out _);
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        tokenResult = stringEnumerator.GetNextToken(out var packetToken);
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(tokenResult.Entity.EncounteredUpperLevel);
        Assert.True(packetToken.IsLast);
        Assert.True(packetToken.EncounteredUpperLevel);
    }
}
\ No newline at end of file

Do not follow this link