diff --git a/src/DotNext.IO/Buffers/BufferWriter.cs b/src/DotNext.IO/Buffers/BufferWriter.cs index 6ae0b1409..522d516d3 100644 --- a/src/DotNext.IO/Buffers/BufferWriter.cs +++ b/src/DotNext.IO/Buffers/BufferWriter.cs @@ -60,9 +60,9 @@ static int Write7BitEncodedInteger(ref SpanWriter writer, int value) internal static int WriteLength(this IBufferWriter buffer, int length, LengthFormat lengthFormat) { - var writer = new SpanWriter(buffer.GetSpan(SevenBitEncodedInt.MaxSizeInBytes)); - buffer.Advance(writer.WriteLength(length, lengthFormat)); - return writer.WrittenCount; + var bytesWritten = WriteLength(buffer.GetSpan(SevenBitEncodedInt.MaxSizeInBytes), length, lengthFormat); + buffer.Advance(bytesWritten); + return bytesWritten; } internal static int WriteLength(Span buffer, int length, LengthFormat lengthFormat) @@ -71,21 +71,28 @@ internal static int WriteLength(Span buffer, int length, LengthFormat leng return writer.WriteLength(length, lengthFormat); } + private static int WriteLength(this ref BufferWriterSlim buffer, int length, LengthFormat lengthFormat) + { + var bytesWritten = WriteLength(buffer.GetSpan(SevenBitEncodedInt.MaxSizeInBytes), length, lengthFormat); + buffer.Advance(bytesWritten); + return bytesWritten; + } + /// /// Encodes string using the specified encoding. /// /// The buffer writer. - /// The sequence of characters. + /// The sequence of characters. /// The encoding context. /// String length encoding format; or to prevent encoding of string length. /// The number of written bytes. - public static long Encode(this IBufferWriter writer, ReadOnlySpan value, in EncodingContext context, LengthFormat? lengthFormat = null) + public static long Encode(this IBufferWriter writer, ReadOnlySpan chars, in EncodingContext context, LengthFormat? lengthFormat = null) { var result = lengthFormat.HasValue - ? writer.WriteLength(context.Encoding.GetByteCount(value), lengthFormat.GetValueOrDefault()) + ? writer.WriteLength(context.Encoding.GetByteCount(chars), lengthFormat.GetValueOrDefault()) : 0L; - context.GetEncoder().Convert(value, writer, true, out var bytesWritten, out _); + context.GetEncoder().Convert(chars, writer, true, out var bytesWritten, out _); result += bytesWritten; return result; @@ -95,19 +102,19 @@ public static long Encode(this IBufferWriter writer, ReadOnlySpan va /// Encodes string using the specified encoding. /// /// The buffer writer. - /// The sequence of characters. + /// The sequence of characters. /// The encoding context. /// String length encoding format; or to prevent encoding of string length. /// The number of written bytes. - public static int Encode(this ref SpanWriter writer, ReadOnlySpan value, in EncodingContext context, LengthFormat? lengthFormat = null) + public static int Encode(this ref SpanWriter writer, scoped ReadOnlySpan chars, in EncodingContext context, LengthFormat? lengthFormat = null) { var result = lengthFormat.HasValue - ? writer.WriteLength(context.Encoding.GetByteCount(value), lengthFormat.GetValueOrDefault()) + ? writer.WriteLength(context.Encoding.GetByteCount(chars), lengthFormat.GetValueOrDefault()) : 0; var bytesWritten = context.TryGetEncoder() is { } encoder - ? encoder.GetBytes(value, writer.RemainingSpan, flush: true) - : context.Encoding.GetBytes(value, writer.RemainingSpan); + ? encoder.GetBytes(chars, writer.RemainingSpan, flush: true) + : context.Encoding.GetBytes(chars, writer.RemainingSpan); result += bytesWritten; writer.Advance(bytesWritten); @@ -128,6 +135,66 @@ public static int Write(this ref SpanWriter writer, scoped ReadOnlySpan + /// Encodes string using the specified encoding. + /// + /// The buffer writer. + /// The sequence of characters. + /// The encoding context. + /// String length encoding format; or to prevent encoding of string length. + /// The number of written bytes. + public static int Encode(this ref BufferWriterSlim writer, scoped ReadOnlySpan chars, in EncodingContext context, + LengthFormat? lengthFormat = null) + { + Span buffer; + int byteCount, result; + if (lengthFormat.HasValue) + { + byteCount = context.Encoding.GetByteCount(chars); + result = writer.WriteLength(byteCount, lengthFormat.GetValueOrDefault()); + + buffer = writer.GetSpan(byteCount); + byteCount = context.TryGetEncoder() is { } encoder + ? encoder.GetBytes(chars, buffer, flush: true) + : context.Encoding.GetBytes(chars, buffer); + + result += byteCount; + writer.Advance(byteCount); + } + else + { + result = 0; + var encoder = context.GetEncoder(); + byteCount = context.Encoding.GetMaxByteCount(1); + for (int charsUsed, bytesWritten; !chars.IsEmpty; chars = chars.Slice(charsUsed), result += bytesWritten) + { + buffer = writer.GetSpan(byteCount); + var maxChars = buffer.Length / byteCount; + + encoder.Convert(chars, buffer, chars.Length <= maxChars, out charsUsed, out bytesWritten, out _); + writer.Advance(bytesWritten); + } + } + + return result; + } + + /// + /// Writes a sequence of bytes prefixed with the length. + /// + /// The buffer writer. + /// A sequence of bytes to be written. + /// A format of the buffer length to be written. + /// A number of bytes written. + public static int Write(this ref BufferWriterSlim writer, scoped ReadOnlySpan value, LengthFormat lengthFormat) + { + var result = writer.WriteLength(value.Length, lengthFormat); + writer.Write(value); + result += value.Length; + + return result; + } private static bool TryFormat(IBufferWriter writer, T value, Span buffer, in EncodingContext context, LengthFormat? lengthFormat, ReadOnlySpan format, IFormatProvider? provider, out long bytesWritten) where T : notnull, ISpanFormattable diff --git a/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs b/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs index dc0740767..29f78cba7 100644 --- a/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs +++ b/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs @@ -1,10 +1,13 @@ using System.Numerics; using System.Text; -using DotNext.Buffers.Binary; using static System.Globalization.CultureInfo; namespace DotNext.Buffers; +using Binary; +using IO; +using static DotNext.Text.EncodingExtensions; + public sealed class BufferWriterSlimTests : Test { [Fact] @@ -360,4 +363,92 @@ public static void ReadWriteBigInteger() Equal(expected, new BigInteger(writer.WrittenSpan)); } + + private static void EncodeDecodeZeroAndMaxValue() + where T : struct, IBinaryInteger, IUnsignedNumber + { + Span buffer = stackalloc byte[SevenBitEncodedInteger.MaxSizeInBytes]; + var writer = new BufferWriterSlim(buffer); + var reader = new SpanReader(buffer); + + Equal(1, writer.Write7BitEncodedInteger(T.Zero)); + Equal(T.Zero, reader.Read7BitEncodedInteger()); + + writer.Clear(reuseBuffer: true); + reader.Reset(); + + Equal(SevenBitEncodedInteger.MaxSizeInBytes, writer.Write7BitEncodedInteger(T.AllBitsSet)); + Equal(T.AllBitsSet, reader.Read7BitEncodedInteger()); + } + + [Fact] + public static void EncodeDecodeUInt32() => EncodeDecodeZeroAndMaxValue(); + + [Fact] + public static void EncodeDecodeUInt64() => EncodeDecodeZeroAndMaxValue(); + + [InlineData(LengthFormat.BigEndian)] + [InlineData(LengthFormat.LittleEndian)] + [InlineData(LengthFormat.Compressed)] + [Theory] + public static void WriteLengthPrefixedBytes(LengthFormat format) + { + ReadOnlySpan expected = [1, 2, 3]; + + var writer = new BufferWriterSlim(); + True(writer.Write(expected, format) > 0); + + using var buffer = writer.DetachOrCopyBuffer(); + var reader = IAsyncBinaryReader.Create(buffer.Memory); + using var actual = reader.ReadBlock(format); + Equal(expected, actual.Span); + } + + [Theory] + [InlineData("UTF-8", null)] + [InlineData("UTF-8", LengthFormat.LittleEndian)] + [InlineData("UTF-8", LengthFormat.BigEndian)] + [InlineData("UTF-8", LengthFormat.Compressed)] + [InlineData("UTF-16LE", null)] + [InlineData("UTF-16LE", LengthFormat.LittleEndian)] + [InlineData("UTF-16LE", LengthFormat.BigEndian)] + [InlineData("UTF-16LE", LengthFormat.Compressed)] + [InlineData("UTF-16BE", null)] + [InlineData("UTF-16BE", LengthFormat.LittleEndian)] + [InlineData("UTF-16BE", LengthFormat.BigEndian)] + [InlineData("UTF-16BE", LengthFormat.Compressed)] + [InlineData("UTF-32LE", null)] + [InlineData("UTF-32LE", LengthFormat.LittleEndian)] + [InlineData("UTF-32LE", LengthFormat.BigEndian)] + [InlineData("UTF-32LE", LengthFormat.Compressed)] + [InlineData("UTF-32BE", null)] + [InlineData("UTF-32BE", LengthFormat.LittleEndian)] + [InlineData("UTF-32BE", LengthFormat.BigEndian)] + [InlineData("UTF-32BE", LengthFormat.Compressed)] + public static void EncodeDecodeString(string encodingName, LengthFormat? format) + { + var encoding = Encoding.GetEncoding(encodingName); + const string expected = "Hello, world!&*(@&*(fghjwgfwffgw Привет, мир!"; + var writer = new BufferWriterSlim(); + + True(writer.Encode(expected, encoding, format) > 0); + + using var buffer = writer.DetachOrCopyBuffer(); + MemoryOwner actual; + if (format.HasValue) + { + var reader = IAsyncBinaryReader.Create(buffer.Memory); + actual = reader.Decode(encoding, format.GetValueOrDefault()); + Equal(expected, actual.Span); + } + else + { + actual = encoding.GetChars(buffer.Span); + } + + using (actual) + { + Equal(expected, actual.Span); + } + } } \ No newline at end of file diff --git a/src/DotNext/Buffers/ByteBuffer.cs b/src/DotNext/Buffers/ByteBuffer.cs index cf3c9845a..35745c34a 100644 --- a/src/DotNext/Buffers/ByteBuffer.cs +++ b/src/DotNext/Buffers/ByteBuffer.cs @@ -342,6 +342,25 @@ public static int Write7BitEncodedInteger(this ref SpanWriter writer, T return count; } + + /// + /// Writes 32-bit integer in a compressed format. + /// + /// The buffer writer. + /// The integer to be written. + /// A number of bytes written to the buffer. + public static int Write7BitEncodedInteger(this ref BufferWriterSlim writer, T value) + where T : struct, IBinaryInteger, IUnsignedNumber + { + var count = 0; + foreach (var b in new SevenBitEncodedInteger(value)) + { + writer.Add() = b; + count += 1; + } + + return count; + } /// /// Decodes an integer encoded as 7-bit octets.