File: Encoding\Int7BitEncodingUtilsTests.cs
Web Access
Project: src\src\Shared\test\Shared.Tests\Microsoft.AspNetCore.Shared.Tests.csproj (Microsoft.AspNetCore.Shared.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.AspNetCore.Shared.Tests;
 
public class Int7BitEncodingUtilsTests
{
    [Theory]
    [InlineData(0, 1)]
    [InlineData(1, 1)]
    [InlineData(0b0_1111111, 1)]
    [InlineData(0b1_0000000, 2)]
    [InlineData(0b1111111_1111111, 2)]
    [InlineData(0b1_0000000_0000000, 3)]
    [InlineData(0b1111111_1111111_1111111, 3)]
    [InlineData(0b1_0000000_0000000_0000000, 4)]
    [InlineData(0b1111111_1111111_1111111_1111111, 4)]
    [InlineData(0b1_0000000_0000000_0000000_0000000, 5)]
    [InlineData(uint.MaxValue, 5)]
    public void Measure7BitEncodedUIntLength_ReturnsExceptedLength(uint value, int expectedSize)
    {
        var actualSize = value.Measure7BitEncodedUIntLength();
        Assert.Equal(expectedSize, actualSize);
    }
 
    [Theory]
    [InlineData(0, new byte[] { 0x00 })]
    [InlineData(1, new byte[] { 0x01 })]
    [InlineData(127, new byte[] { 0x7F })]
    [InlineData(128, new byte[] { 0x80, 0x01 })]
    [InlineData(255, new byte[] { 0xFF, 0x01 })]
    [InlineData(256, new byte[] { 0x80, 0x02 })]
    [InlineData(16383, new byte[] { 0xFF, 0x7F })]
    [InlineData(16384, new byte[] { 0x80, 0x80, 0x01 })]
    [InlineData(2097151, new byte[] { 0xFF, 0xFF, 0x7F })]
    [InlineData(2097152, new byte[] { 0x80, 0x80, 0x80, 0x01 })]
    [InlineData(268435455, new byte[] { 0xFF, 0xFF, 0xFF, 0x7F })]
    [InlineData(268435456, new byte[] { 0x80, 0x80, 0x80, 0x80, 0x01 })]
    [InlineData(int.MaxValue, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x07 })]
    public void Read7BitEncodedInt_DecodesCorrectly(int expected, byte[] encoded)
    {
        ReadOnlySpan<byte> source = encoded;
 
        var bytesConsumed = source.Read7BitEncodedInt(out var value);
 
        Assert.Equal(expected, value);
        Assert.Equal(encoded.Length, bytesConsumed);
    }
 
    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(127)]
    [InlineData(128)]
    [InlineData(255)]
    [InlineData(16383)]
    [InlineData(16384)]
    [InlineData(2097151)]
    [InlineData(2097152)]
    [InlineData(268435455)]
    [InlineData(268435456)]
    [InlineData(int.MaxValue)]
    public void Read7BitEncodedInt_RoundTripsWithWrite(int value)
    {
        Span<byte> buffer = stackalloc byte[5];
 
        var bytesWritten = buffer.Write7BitEncodedInt(value);
 
        ReadOnlySpan<byte> source = buffer.Slice(0, bytesWritten);
        var bytesConsumed = source.Read7BitEncodedInt(out var decoded);
 
        Assert.Equal(value, decoded);
        Assert.Equal(bytesWritten, bytesConsumed);
    }
 
    [Fact]
    public void Read7BitEncodedInt_WithEmptySpan_ThrowsFormatException()
    {
        Assert.Throws<FormatException>(() =>
        {
            var source = ReadOnlySpan<byte>.Empty;
            return source.Read7BitEncodedInt(out _);
        });
    }
 
    [Fact]
    public void Read7BitEncodedInt_WithTruncatedData_ThrowsFormatException()
    {
        Assert.Throws<FormatException>(() =>
        {
            // This represents the start of a multi-byte encoded value but is incomplete
            // 0x80 has continuation bit set, meaning more bytes should follow
            ReadOnlySpan<byte> source = [0x80];
 
            return source.Read7BitEncodedInt(out _);
        });
    }
 
    [Fact]
    public void Read7BitEncodedInt_WithOverflow_ThrowsFormatException()
    {
        Assert.Throws<FormatException>(() =>
        {
            // 6 bytes with continuation bits set would overflow a 32-bit integer
            ReadOnlySpan<byte> source = [0x80, 0x80, 0x80, 0x80, 0x80, 0x01];
 
            return source.Read7BitEncodedInt(out _);
        });
    }
 
    [Fact]
    public void Read7BitEncodedInt_WithExtraDataAfterValue_ConsumesOnlyNeededBytes()
    {
        // Value 127 followed by extra bytes
        ReadOnlySpan<byte> source = [0x7F, 0xFF, 0xFF];
 
        var bytesConsumed = source.Read7BitEncodedInt(out var value);
 
        Assert.Equal(127, value);
        Assert.Equal(1, bytesConsumed);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("Hello")]
    [InlineData("Hello, World!")]
    [InlineData("UTF-8: \u00e9\u00e8\u00ea")]
    public void Read7BitEncodedString_DecodesCorrectly(string expected)
    {
        var stringBytes = System.Text.Encoding.UTF8.GetBytes(expected);
        var lengthBytes = new byte[5];
        Span<byte> lengthSpan = lengthBytes;
        var lengthSize = lengthSpan.Write7BitEncodedInt(stringBytes.Length);
 
        var encodedBytes = new byte[lengthSize + stringBytes.Length];
        Array.Copy(lengthBytes, 0, encodedBytes, 0, lengthSize);
        Array.Copy(stringBytes, 0, encodedBytes, lengthSize, stringBytes.Length);
 
        ReadOnlySpan<byte> source = encodedBytes;
 
        var bytesConsumed = source.Read7BitEncodedString(out var value);
 
        Assert.Equal(expected, value);
        Assert.Equal(encodedBytes.Length, bytesConsumed);
    }
 
    [Fact]
    public void Read7BitEncodedString_WithEmptyString_ReturnsEmptyAndConsumesLengthByte()
    {
        // Length of 0
        ReadOnlySpan<byte> source = new byte[] { 0x00 };
 
        var bytesConsumed = source.Read7BitEncodedString(out var value);
 
        Assert.Equal(string.Empty, value);
        Assert.Equal(1, bytesConsumed);
    }
 
    [Fact]
    public void Read7BitEncodedString_WithTruncatedStringData_ThrowsFormatException()
    {
        Assert.Throws<FormatException>(() =>
        {
            // Length says 10 bytes, but only 3 bytes of data follow
            ReadOnlySpan<byte> source = [0x0A, 0x41, 0x42, 0x43];
 
            return source.Read7BitEncodedString(out _);
        });
    }
 
    [Fact]
    public void Read7BitEncodedString_WithMultiByteLengthPrefixAndTruncatedData_ThrowsFormatException()
    {
        Assert.Throws<FormatException>(() =>
        {
            // Length prefix 0xC8 0x01 = 200 (multi-byte), but only 2 bytes of string data follow
            // Total: 4 bytes, but need 2 (prefix) + 200 (data) = 202 bytes
            ReadOnlySpan<byte> source = [0xC8, 0x01, 0x41, 0x42];
 
            return source.Read7BitEncodedString(out _);
        });
    }
 
    [Fact]
    public void Read7BitEncodedString_WithTruncatedLengthPrefix_ThrowsFormatException()
    {
        Assert.Throws<FormatException>(() =>
        {
            // Continuation bit set but no more data
            ReadOnlySpan<byte> source = [0x80];
 
            return source.Read7BitEncodedString(out _);
        });
    }
 
    [Fact]
    public void Read7BitEncodedString_WithExtraDataAfterString_ConsumesOnlyNeededBytes()
    {
        // "Hi" (length 2) followed by extra bytes
        ReadOnlySpan<byte> source = new byte[] { 0x02, 0x48, 0x69, 0xFF, 0xFF };
 
        var bytesConsumed = source.Read7BitEncodedString(out var value);
 
        Assert.Equal("Hi", value);
        Assert.Equal(3, bytesConsumed); // 1 byte length + 2 bytes string
    }
 
    [Fact]
    public void Read7BitEncodedString_WithMultiByteLengthPrefix_DecodesCorrectly()
    {
        // Create a string that requires a multi-byte length prefix (> 127 bytes)
        var longString = new string('A', 200);
        var stringBytes = System.Text.Encoding.UTF8.GetBytes(longString);
        var lengthBytes = new byte[5];
        Span<byte> lengthSpan = lengthBytes;
        var lengthSize = lengthSpan.Write7BitEncodedInt(stringBytes.Length);
 
        Assert.True(lengthSize > 1); // Verify we're testing multi-byte length
 
        var encodedBytes = new byte[lengthSize + stringBytes.Length];
        Array.Copy(lengthBytes, 0, encodedBytes, 0, lengthSize);
        Array.Copy(stringBytes, 0, encodedBytes, lengthSize, stringBytes.Length);
 
        ReadOnlySpan<byte> source = encodedBytes;
 
        var bytesConsumed = source.Read7BitEncodedString(out var value);
 
        Assert.Equal(longString, value);
        Assert.Equal(encodedBytes.Length, bytesConsumed);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("Hello")]
    [InlineData("Hello, World!")]
    [InlineData("UTF-8: \u00e9\u00e8\u00ea")]
    public void Write7BitEncodedString_EncodesCorrectly(string value)
    {
        var expectedByteCount = Int7BitEncodingUtils.Measure7BitEncodedStringLength(value);
        Span<byte> buffer = stackalloc byte[expectedByteCount];
 
        var bytesWritten = buffer.Write7BitEncodedString(value);
 
        Assert.Equal(expectedByteCount, bytesWritten);
 
        // Verify by reading back
        ReadOnlySpan<byte> source = buffer.Slice(0, bytesWritten);
        var bytesConsumed = source.Read7BitEncodedString(out var decoded);
 
        Assert.Equal(value, decoded);
        Assert.Equal(bytesWritten, bytesConsumed);
    }
 
    [Fact]
    public void Write7BitEncodedString_WithNullString_WritesZeroLength()
    {
        Span<byte> buffer = stackalloc byte[10];
 
        var bytesWritten = buffer.Write7BitEncodedString(null!);
 
        Assert.Equal(1, bytesWritten);
        Assert.Equal(0, buffer[0]);
    }
 
    [Fact]
    public void Write7BitEncodedString_WithEmptyString_WritesZeroLength()
    {
        Span<byte> buffer = stackalloc byte[10];
 
        var bytesWritten = buffer.Write7BitEncodedString(string.Empty);
 
        Assert.Equal(1, bytesWritten);
        Assert.Equal(0, buffer[0]);
    }
 
    [Fact]
    public void Write7BitEncodedString_WithLongString_UsesMultiByteLengthPrefix()
    {
        var longString = new string('A', 200);
        var expectedByteCount = Int7BitEncodingUtils.Measure7BitEncodedStringLength(longString);
        var buffer = new byte[expectedByteCount];
 
        var bytesWritten = buffer.AsSpan().Write7BitEncodedString(longString);
 
        Assert.Equal(expectedByteCount, bytesWritten);
 
        // Verify the length prefix is multi-byte (200 > 127)
        Assert.True((buffer[0] & 0x80) != 0); // Continuation bit set
 
        // Verify by reading back
        ReadOnlySpan<byte> source = buffer;
        var bytesConsumed = source.Read7BitEncodedString(out var decoded);
 
        Assert.Equal(longString, decoded);
        Assert.Equal(bytesWritten, bytesConsumed);
    }
 
    [Theory]
    [InlineData("", 1)]
    [InlineData("A", 2)]
    [InlineData("Hello", 6)]
    public void Measure7BitEncodedStringLength_ReturnsCorrectLength(string value, int expectedLength)
    {
        var actualLength = Int7BitEncodingUtils.Measure7BitEncodedStringLength(value);
 
        Assert.Equal(expectedLength, actualLength);
    }
 
    [Fact]
    public void Measure7BitEncodedStringLength_WithNullString_ReturnsOne()
    {
        var length = Int7BitEncodingUtils.Measure7BitEncodedStringLength(null!);
 
        Assert.Equal(1, length);
    }
 
    [Fact]
    public void Measure7BitEncodedStringLength_WithLongString_IncludesMultiByteLengthPrefix()
    {
        var longString = new string('A', 200);
 
        var length = Int7BitEncodingUtils.Measure7BitEncodedStringLength(longString);
 
        // 200 bytes for string + 2 bytes for length prefix (200 requires 2 bytes in 7-bit encoding)
        Assert.Equal(202, length);
    }
 
    [Fact]
    public void Measure7BitEncodedStringLength_WithUtf8String_CountsUtf8Bytes()
    {
        // Each of these characters is 2 bytes in UTF-8
        var utf8String = "\u00e9\u00e8\u00ea"; // 3 chars, 6 bytes
 
        var length = Int7BitEncodingUtils.Measure7BitEncodedStringLength(utf8String);
 
        // 6 bytes for string + 1 byte for length prefix
        Assert.Equal(7, length);
    }
}