diff --git a/BencodeNET.Tests/BencodeNET.Tests.csproj b/BencodeNET.Tests/BencodeNET.Tests.csproj index 00fda03ff2..a97dbffe45 100644 --- a/BencodeNET.Tests/BencodeNET.Tests.csproj +++ b/BencodeNET.Tests/BencodeNET.Tests.csproj @@ -1,7 +1,8 @@ - + - net452 + net472;netcoreapp2.0;netcoreapp2.2;netcoreapp3.0 + 7.3 @@ -14,15 +15,14 @@ - - - - - - + + + + + + - diff --git a/BencodeNET.Tests/Extensions.cs b/BencodeNET.Tests/Extensions.cs index 54a071d099..fdb1410517 100644 --- a/BencodeNET.Tests/Extensions.cs +++ b/BencodeNET.Tests/Extensions.cs @@ -1,5 +1,12 @@ -using System.IO; +using System; +using System.IO; +using System.IO.Pipelines; using System.Text; +using System.Threading.Tasks; +using BencodeNET.IO; +using BencodeNET.Objects; +using BencodeNET.Parsing; +using NSubstitute.Core; namespace BencodeNET.Tests { @@ -18,5 +25,49 @@ internal static string AsString(this Stream stream, Encoding encoding) var sr = new StreamReader(stream, encoding); return sr.ReadToEnd(); } + + internal static void SkipBytes(this BencodeReader reader, int length) + { + reader.Read(new byte[length]); + } + + internal static Task SkipBytesAsync(this PipeBencodeReader reader, int length) + { + return reader.ReadAsync(new byte[length]).AsTask(); + } + + internal static ConfiguredCall AndSkipsAhead(this ConfiguredCall call, int length) + { + return call.AndDoes(x => x.Arg().SkipBytes(length)); + } + + internal static ConfiguredCall AndSkipsAheadAsync(this ConfiguredCall call, int length) + { + return call.AndDoes(async x => await x.Arg().SkipBytesAsync(length)); + } + + internal static async ValueTask ParseStringAsync(this IBObjectParser parser, string bencodedString) + { + var bytes = Encoding.UTF8.GetBytes(bencodedString).AsMemory(); + var (reader, writer) = new Pipe(); + await writer.WriteAsync(bytes); + writer.Complete(); + return await parser.ParseAsync(reader); + } + + internal static async ValueTask ParseStringAsync(this IBObjectParser parser, string bencodedString) where T : IBObject + { + var bytes = Encoding.UTF8.GetBytes(bencodedString).AsMemory(); + var (reader, writer) = new Pipe(); + await writer.WriteAsync(bytes); + writer.Complete(); + return await parser.ParseAsync(reader); + } + + internal static void Deconstruct(this Pipe pipe, out PipeReader reader, out PipeWriter writer) + { + reader = pipe.Reader; + writer = pipe.Writer; + } } } diff --git a/BencodeNET.Tests/IO/BencodeReaderTests.cs b/BencodeNET.Tests/IO/BencodeReaderTests.cs new file mode 100644 index 0000000000..639a6c6b82 --- /dev/null +++ b/BencodeNET.Tests/IO/BencodeReaderTests.cs @@ -0,0 +1,355 @@ +using System.IO; +using System.Text; +using BencodeNET.IO; +using FluentAssertions; +using NSubstitute; +using Xunit; + +namespace BencodeNET.Tests.IO +{ + public class BencodeReaderTests + { + [Fact] + public void ReadBytes() + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))) + using (var bs = new BencodeReader(ms)) + { + var bytes = new byte[12]; + var read = bs.Read(bytes); + + read.Should().Be(12); + Assert.Equal("Hello World!", Encoding.UTF8.GetString(bytes)); + } + } + + [Fact] + public void ReadZeroBytes() + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))) + using (var bs = new BencodeReader(ms)) + { + var bytes = new byte[0]; + var read = bs.Read(bytes); + + read.Should().Be(0); + Assert.Equal("", Encoding.UTF8.GetString(bytes)); + } + } + + [Fact] + public void ReadMoreBytesThanInStream() + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))) + using (var bs = new BencodeReader(ms)) + { + var bytes = new byte[20]; + var read = bs.Read(bytes); + + read.Should().Be(12); + Assert.Equal("Hello World!", Encoding.UTF8.GetString(bytes, 0, read)); + } + } + + [Theory] + [AutoMockedData] + public void ReadBytesWhenNotAllBytesAreReadOnFirstRead(Stream stream) + { + var ms = new MemoryStream(Encoding.UTF8.GetBytes("abcdef")); + var bs = new BencodeReader(stream); + + stream.Read(null, 0, 0).ReturnsForAnyArgs( + x => ms.Read(x.Arg(), x.ArgAt(1), 2), + x => ms.Read(x.Arg(), x.ArgAt(1), 2), + x => ms.Read(x.Arg(), x.ArgAt(1), 2) + ); + + var bytes = new byte[6]; + var read = bs.Read(bytes); + + read.Should().Be(6); + Assert.Equal("abcdef", Encoding.UTF8.GetString(bytes, 0, read)); + } + + [Fact] + public void ReadCharChangesStreamPosition() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.Position.Should().Be(0); + bs.ReadChar(); + bs.Position.Should().Be(1); + bs.ReadChar(); + bs.Position.Should().Be(2); + } + } + + [Fact] + public void ReadBytesChangesStreamPosition() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + Assert.Equal(0, bs.Position); + + var bytes = new byte[str.Length]; + var read = bs.Read(bytes); + + read.Should().Be(12); + bs.Position.Should().Be(12); + Assert.Equal(str, Encoding.UTF8.GetString(bytes)); + } + } + + [Fact] + public void PeekCharDoesNotChangeStreamPosition() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.Position.Should().Be(0); + bs.PeekChar(); + bs.Position.Should().Be(0); + bs.PeekChar(); + bs.Position.Should().Be(0); + } + } + + [Fact] + public void ReadCharAfterPeekCharChangesStreamPosition() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.Position.Should().Be(0); + bs.PeekChar(); + bs.Position.Should().Be(0); + bs.ReadChar(); + bs.Position.Should().Be(1); + } + } + + [Fact] + public void ReadChar() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.ReadChar().Should().Be('H'); + bs.ReadChar().Should().Be('e'); + bs.ReadChar().Should().Be('l'); + bs.ReadChar().Should().Be('l'); + bs.ReadChar().Should().Be('o'); + bs.ReadChar().Should().Be(' '); + bs.ReadChar().Should().Be('W'); + bs.ReadChar().Should().Be('o'); + bs.ReadChar().Should().Be('r'); + bs.ReadChar().Should().Be('l'); + bs.ReadChar().Should().Be('d'); + bs.ReadChar().Should().Be('!'); + bs.ReadChar().Should().Be(default); + } + } + + [Fact] + public void PreviousChar() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.PreviousChar.Should().Be(default); + + Assert.Equal('H', bs.ReadChar()); + Assert.Equal('H', bs.PreviousChar); + Assert.Equal('e', bs.ReadChar()); + Assert.Equal('e', bs.PreviousChar); + + bs.Read(new byte[20]); + + Assert.Equal('!', bs.PreviousChar); + } + } + + [Fact] + public void PreviousCharAtStartOfStream() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.PreviousChar.Should().Be(default); + } + } + + [Fact] + public void PreviousCharUnaffectedByPeekChar() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.SkipBytes(1); + Assert.Equal('H', bs.PreviousChar); + Assert.Equal('e', bs.PeekChar()); + Assert.Equal('H', bs.PreviousChar); + } + } + + [Fact] + public void PeekChar() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + Assert.Equal('H', bs.PeekChar()); + } + } + + [Fact] + public void PeekDoesNotAdvanceStreamPosition() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.Position.Should().Be(0); + bs.PeekChar().Should().Be('H'); + bs.Position.Should().Be(0); + bs.PeekChar().Should().Be('H'); + bs.Position.Should().Be(0); + } + } + + [Fact] + public void PeekAndReadEqual() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.PeekChar().Should().Be('H'); + bs.ReadChar().Should().Be('H'); + } + } + + [Fact] + public void PeekAreChangedAfterReadChar() + { + var str = "abcdefghijkl"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + Assert.Equal('a', bs.PeekChar()); + Assert.Equal('a', bs.ReadChar()); + + Assert.Equal('b', bs.PeekChar()); + Assert.Equal('b', bs.ReadChar()); + + Assert.Equal('c', bs.PeekChar()); + Assert.Equal('c', bs.ReadChar()); + + Assert.Equal('d', bs.PeekChar()); + Assert.Equal('d', bs.ReadChar()); + + Assert.Equal('e', bs.PeekChar()); + Assert.Equal('e', bs.ReadChar()); + } + } + + [Fact] + public void PeekAreChangedAfterReadSingleByte() + { + var str = "abcdefghijkl"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + var buffer = new byte[1]; + int read; + + Assert.Equal('a', bs.PeekChar()); + + read = bs.Read(buffer); + Assert.Equal(1, read); + Assert.Equal('a', (char)buffer[0]); + Assert.Equal('b', bs.PeekChar()); + + read = bs.Read(buffer); + Assert.Equal(1, read); + Assert.Equal('b', (char)buffer[0]); + Assert.Equal('c', bs.PeekChar()); + } + } + + [Fact] + public void PeekAreChangedAfterReadMultipleBytes() + { + var str = "abcdefghijkl"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + var buffer = new byte[2]; + + Assert.Equal('a', bs.PeekChar()); + + var read = bs.Read(buffer); + read.Should().Be(2); + Assert.Equal('a', (char)buffer[0]); + Assert.Equal('b', (char)buffer[1]); + Assert.Equal('c', bs.PeekChar()); + + read = bs.Read(buffer); + read.Should().Be(2); + Assert.Equal('c', (char)buffer[0]); + Assert.Equal('d', (char)buffer[1]); + Assert.Equal('e', bs.PeekChar()); + } + } + + [Fact] + public void PeekAtEndOfStreamThenReadSingleByte() + { + var str = "abcdefghijkl"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.SkipBytes(12); + bs.PeekChar().Should().Be(default); + bs.ReadChar().Should().Be(default); + } + } + + [Fact] + public void PeekAtEndOfStreamThenReadBytes() + { + var str = "abcdefghijkl"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.SkipBytes(12); + bs.PeekChar().Should().Be(default); + bs.Read(new byte[4]).Should().Be(0); + } + } + + [Fact] + public void EndOfStream() + { + var str = "Hello World!"; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) + using (var bs = new BencodeReader(ms)) + { + bs.SkipBytes(12); + bs.EndOfStream.Should().BeTrue(); + bs.ReadChar().Should().Be(default); + } + } + } +} diff --git a/BencodeNET.Tests/IO/BencodeStreamTests.cs b/BencodeNET.Tests/IO/BencodeStreamTests.cs deleted file mode 100644 index 36bb53aa1a..0000000000 --- a/BencodeNET.Tests/IO/BencodeStreamTests.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System.IO; -using System.Text; -using BencodeNET.IO; -using Xunit; - -// ReSharper disable ConsiderUsingConfigureAwait - -namespace BencodeNET.Tests.IO -{ - public class BencodeStreamTests - { - [Fact] - public void ReadBytes() - { - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))) - using (var bs = new BencodeStream(ms)) - { - var bytes = bs.Read(12); - Assert.Equal(12, bytes.Length); - Assert.Equal("Hello World!", Encoding.UTF8.GetString(bytes)); - } - } - - [Fact] - public void ReadZeroBytes() - { - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))) - using (var bs = new BencodeStream(ms)) - { - var bytes = bs.Read(0); - Assert.Equal(0, bytes.Length); - Assert.Equal("", Encoding.UTF8.GetString(bytes)); - } - } - - [Fact] - public void ReadMoreBytesThanInStream() - { - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))) - using (var bs = new BencodeStream(ms)) - { - var bytes = bs.Read(20); - Assert.Equal(12, bytes.Length); - Assert.Equal("Hello World!", Encoding.UTF8.GetString(bytes)); - } - } - - [Fact] - public void ReadBytesChangesStreamPosition() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal(0, bs.Position); - - var bytes = bs.Read(str.Length); - Assert.Equal(12, bytes.Length); - Assert.Equal(str, Encoding.UTF8.GetString(bytes)); - - Assert.Equal(12, bs.Position); - } - } - - [Fact] - public void Read() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal('H', bs.Read()); - Assert.Equal('e', bs.Read()); - Assert.Equal('l', bs.Read()); - Assert.Equal('l', bs.Read()); - Assert.Equal('o', bs.Read()); - Assert.Equal(' ', bs.Read()); - Assert.Equal('W', bs.Read()); - Assert.Equal('o', bs.Read()); - Assert.Equal('r', bs.Read()); - Assert.Equal('l', bs.Read()); - Assert.Equal('d', bs.Read()); - Assert.Equal('!', bs.Read()); - Assert.Equal(-1, bs.Read()); - } - } - - [Fact] - public void ReadChangeStreamPosition() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal('H', bs.Read()); - Assert.Equal('e', bs.Read()); - bs.Position -= 1; - Assert.Equal('e', bs.Read()); - } - } - - [Fact] - public void ReadPrevious() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal(-1, bs.ReadPrevious()); - Assert.Equal('H', bs.Read()); - Assert.Equal('H', bs.ReadPrevious()); - Assert.Equal('e', bs.Read()); - Assert.Equal('e', bs.ReadPrevious()); - - bs.Position = 20; - - Assert.Equal(-1, bs.ReadPrevious()); - } - } - - [Fact] - public void ReadPreviousAtStartOfStream() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal(-1, bs.ReadPrevious()); - } - } - - [Fact] - public void ReadPreviousUnaffectedByPeek() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - bs.Read(1); - Assert.Equal('H', bs.ReadPrevious()); - Assert.Equal('e', bs.Peek()); - Assert.Equal('H', bs.ReadPrevious()); - } - } - - [Fact] - public void ReadPreviousChar() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal(default(char), bs.ReadPreviousChar()); - bs.Read(1); - Assert.Equal('H', bs.ReadPreviousChar()); - } - } - - [Fact] - public void PeekUnnaffectedByReadPrevious() - { - var str = "abcdefghijkl"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - bs.Read(0); - Assert.Equal('a', bs.Peek()); - bs.ReadPrevious(); - Assert.Equal('a', bs.Peek()); - - bs.Read(1); - Assert.Equal('b', bs.Peek()); - bs.ReadPrevious(); - Assert.Equal('b', bs.Peek()); - } - } - - [Fact] - public void ReadUnnaffectedByReadPrevious() - { - var str = "abcdefghijkl"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal('a', bs.Read()); - bs.ReadPrevious(); - Assert.Equal('b', bs.Read()); - } - } - - [Fact] - public void Peek() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal('H', bs.Peek()); - } - } - - [Fact] - public void PeekDoesNotAdvanceStreamPosition() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal(0, bs.Position); - Assert.Equal('H', bs.Peek()); - Assert.Equal(0, bs.Position); - Assert.Equal('H', bs.Peek()); - Assert.Equal(0, bs.Position); - } - } - - [Fact] - public void PeekAndReadEqual() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal('H', bs.Peek()); - Assert.Equal('H', bs.Read()); - } - } - - [Fact] - public void PeekAreChangedAfterRead() - { - var str = "abcdefghijkl"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal('a', bs.Peek()); - Assert.Equal('a', bs.Read()); - - Assert.Equal('b', bs.Peek()); - Assert.Equal('b', bs.Read()); - - Assert.Equal('c', bs.Peek()); - Assert.Equal('c', bs.Read()); - - Assert.Equal('d', bs.Peek()); - Assert.Equal('d', bs.Read()); - - Assert.Equal('e', bs.Peek()); - Assert.Equal('e', bs.Read()); - } - } - - [Fact] - public void PeekAreChangedAfterReadSingleByte() - { - var str = "abcdefghijkl"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - byte[] bytes; - - Assert.Equal('a', bs.Peek()); - - bytes = bs.Read(1); - Assert.Equal('a', (char)bytes[0]); - Assert.Equal('b', bs.Peek()); - - bytes = bs.Read(1); - Assert.Equal('b', (char)bytes[0]); - Assert.Equal('c', bs.Peek()); - } - } - - [Fact] - public void PeekAreChangedAfterReadMutipleBytes() - { - var str = "abcdefghijkl"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - byte[] bytes; - - Assert.Equal('a', bs.Peek()); - - bytes = bs.Read(2); - Assert.Equal('a', (char)bytes[0]); - Assert.Equal('b', (char)bytes[1]); - Assert.Equal('c', bs.Peek()); - - bytes = bs.Read(2); - Assert.Equal('c', (char)bytes[0]); - Assert.Equal('d', (char)bytes[1]); - Assert.Equal('e', bs.Peek()); - } - } - - [Fact] - public void PeekAtEndOfStreamThenReadSingleByte() - { - var str = "abcdefghijkl"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - bs.Read(12); - Assert.Equal(-1, bs.Peek()); - Assert.Equal(-1, bs.Read()); - } - } - - [Fact] - public void PeekAtEndOfStreamThenReadBytes() - { - var str = "abcdefghijkl"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - bs.Read(12); - Assert.Equal(-1, bs.Peek()); - Assert.Equal(0, bs.Read(4).Length); - } - } - - [Fact] - public void PeekAfterPositionChange() - { - var str = "abcdefghijkl"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - Assert.Equal(0, bs.Position); - Assert.Equal('a', bs.PeekChar()); - bs.Position = 1; - Assert.Equal(1, bs.Position); - Assert.Equal('b', bs.PeekChar()); - } - } - - [Fact] - public void EndOfStream() - { - var str = "Hello World!"; - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(str))) - using (var bs = new BencodeStream(ms)) - { - bs.Read(12); - Assert.True(bs.EndOfStream); - Assert.Equal(-1, bs.Read()); - } - } - } -} diff --git a/BencodeNET.Tests/IO/PipeBencodeReaderTests.cs b/BencodeNET.Tests/IO/PipeBencodeReaderTests.cs new file mode 100644 index 0000000000..845f0e22df --- /dev/null +++ b/BencodeNET.Tests/IO/PipeBencodeReaderTests.cs @@ -0,0 +1,401 @@ +using System; +using System.Buffers; +using System.IO.Pipelines; +using System.Text; +using System.Threading.Tasks; +using AutoFixture.AutoNSubstitute; +using BencodeNET.IO; +using FluentAssertions; +using NSubstitute; +using Xunit; + +namespace BencodeNET.Tests.IO +{ + public class PipeBencodeReaderTests + { + private Pipe Pipe { get; } = new Pipe(); + private PipeReader PipeReader => Pipe.Reader; + private PipeWriter PipeWriter => Pipe.Writer; + + private PipeBencodeReader PipeBencodeReader { get; } + + public PipeBencodeReaderTests() + { + PipeBencodeReader = new PipeBencodeReader(PipeReader); + } + + [Fact] + public async Task CanReadAsyncBeforeWrite() + { + var bytes = Encoding.UTF8.GetBytes("abc").AsMemory(); + + var (reader, writer) = new Pipe(); + var bencodeReader = new PipeBencodeReader(reader); + + var readTask = bencodeReader.ReadCharAsync(); + + await writer.WriteAsync(bytes.Slice(0, 2)); + + writer.Complete(); + + var c = await readTask; + + c.Should().Be('a'); + } + + [Fact] + public async Task CanReadLessThanRequested() + { + var bytes = Encoding.UTF8.GetBytes("abc").AsMemory(); + + var (reader, writer) = new Pipe(); + var bencodeReader = new PipeBencodeReader(reader); + + await writer.WriteAsync(bytes.Slice(0, 1)); + + var buffer = new byte[bytes.Length]; + var readTask = bencodeReader.ReadAsync(buffer); + + await writer.WriteAsync(bytes.Slice(1, 1)); + writer.Complete(); + + var bytesRead = await readTask; + + bytesRead.Should().Be(2); + buffer[0].Should().Be((byte)'a'); + buffer[1].Should().Be((byte)'b'); + buffer[2].Should().Be(default); + } + + [Theory] + [AutoMockedData] + public async Task CanReadIncompleteFirstTry([Substitute] PipeReader reader) + { + var bytes = Encoding.UTF8.GetBytes("abcdef").AsMemory(); + + reader.TryRead(out _).Returns(false); + reader.ReadAsync().Returns( + new ReadResult(new ReadOnlySequence(bytes.Slice(0, 5)), false, false), + new ReadResult(new ReadOnlySequence(bytes), false, true) + ); + + var bencodeReader = new PipeBencodeReader(reader); + + var buffer = new byte[bytes.Length]; + var bytesRead = await bencodeReader.ReadAsync(buffer); + + bytesRead.Should().Be(bytes.Length); + buffer.Should().Equal(bytes.ToArray()); + } + + [Theory] + [AutoMockedData] + public async Task CanReadLessThanReceivedAsync([Substitute] PipeReader reader) + { + var bytes = Encoding.UTF8.GetBytes("abcdef").AsMemory(); + + reader.TryRead(out _).Returns(false); + reader.ReadAsync().Returns(new ReadResult(new ReadOnlySequence(bytes), false, true)); + + var bencodeReader = new PipeBencodeReader(reader); + + var buffer = new byte[bytes.Length-3]; + var readTask = bencodeReader.ReadAsync(buffer); + + var bytesRead = await readTask; + + bytesRead.Should().Be(bytes.Length-3); + buffer[0].Should().Be((byte)'a'); + buffer[1].Should().Be((byte)'b'); + buffer[2].Should().Be((byte)'c'); + } + + [Fact] + public async Task CanPeekAsyncBeforeWrite() + { + var bytes = Encoding.UTF8.GetBytes("abc").AsMemory(); + + var pipe = new Pipe(); + var reader = pipe.Reader; + var bencodeReader = new PipeBencodeReader(reader); + + var peekTask = bencodeReader.PeekCharAsync(); + + await pipe.Writer.WriteAsync(bytes.Slice(0, 2)); + + var c = await peekTask; + + c.Should().Be('a'); + + (await bencodeReader.ReadCharAsync()).Should().Be('a'); + } + + #region BencodeReader tests + + [Fact] + public async Task ReadBytesAsync() + { + Write("Hello World!"); + + var output = new byte[12]; + var read = await PipeBencodeReader.ReadAsync(output); + + read.Should().Be(output.Length); + Assert.Equal("Hello World!", Encoding.UTF8.GetString(output)); + } + + [Fact] + public async Task ReadZeroBytesAsync() + { + Write("Hello World!"); + + var output = new byte[0]; + var read = await PipeBencodeReader.ReadAsync(output); + + read.Should().Be(output.Length); + Assert.Equal("", Encoding.UTF8.GetString(output)); + } + + [Fact] + public async Task ReadMoreBytesThanInStream() + { + Write("Hello World!"); + + var bytes = new byte[20]; + var read = await PipeBencodeReader.ReadAsync(bytes); + + read.Should().Be(12); + Assert.Equal("Hello World!", Encoding.UTF8.GetString(bytes, 0, (int) read)); + } + + [Fact] + public async Task ReadCharChangesStreamPosition() + { + Write("Hello World!"); + + PipeBencodeReader.Position.Should().Be(0); + await PipeBencodeReader.ReadCharAsync(); + PipeBencodeReader.Position.Should().Be(1); + await PipeBencodeReader.ReadCharAsync(); + PipeBencodeReader.Position.Should().Be(2); + } + + [Fact] + public async Task ReadBytesChangesStreamPosition() + { + var str = "Hello World!"; + Write(str); + + Assert.Equal(0, PipeBencodeReader.Position); + + var bytes = new byte[str.Length]; + var read = await PipeBencodeReader.ReadAsync(bytes); + + read.Should().Be(12); + PipeBencodeReader.Position.Should().Be(12); + Assert.Equal(str, Encoding.UTF8.GetString(bytes)); + } + + [Fact] + public async Task PeekCharDoesNotChangeStreamPosition() + { + Write("Hello World!"); + + PipeBencodeReader.Position.Should().Be(0); + await PipeBencodeReader.PeekCharAsync(); + PipeBencodeReader.Position.Should().Be(0); + await PipeBencodeReader.PeekCharAsync(); + PipeBencodeReader.Position.Should().Be(0); + } + + + [Fact] + public async Task ReadCharAfterPeekCharChangesStreamPosition() + { + Write("Hello World!"); + + PipeBencodeReader.Position.Should().Be(0); + await PipeBencodeReader.PeekCharAsync(); + PipeBencodeReader.Position.Should().Be(0); + await PipeBencodeReader.ReadCharAsync(); + PipeBencodeReader.Position.Should().Be(1); + } + + [Fact] + public async Task ReadChar() + { + Write("Hello World!"); + + Assert.Equal('H', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('e', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('l', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('l', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('o', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal(' ', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('W', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('o', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('r', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('l', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('d', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('!', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal(default, await PipeBencodeReader.ReadCharAsync()); + } + + [Fact] + public async Task PreviousChar() + { + Write("Hello World!"); + + PipeBencodeReader.PreviousChar.Should().Be(default); + + Assert.Equal('H', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('H', PipeBencodeReader.PreviousChar); + Assert.Equal('e', await PipeBencodeReader.ReadCharAsync()); + Assert.Equal('e', PipeBencodeReader.PreviousChar); + + await PipeBencodeReader.ReadAsync(new byte[20]); + + Assert.Equal('!', PipeBencodeReader.PreviousChar); + } + + [Fact] + public void PreviousCharAtStartOfStream() + { + Write("Hello World!"); + + PipeBencodeReader.PreviousChar.Should().Be(default); + } + + [Fact] + public async Task PreviousCharUnaffectedByPeekCharAsync() + { + Write("Hello World!"); + + await PipeBencodeReader.SkipBytesAsync(1); + Assert.Equal('H', PipeBencodeReader.PreviousChar); + Assert.Equal('e', await PipeBencodeReader.PeekCharAsync()); + Assert.Equal('H', PipeBencodeReader.PreviousChar); + } + + [Fact] + public async Task PeekCharAsync() + { + Write("Hello World!"); + + Assert.Equal('H', await PipeBencodeReader.PeekCharAsync()); + } + + [Fact] + public async Task PeekDoesNotAdvanceStreamPosition() + { + Write("Hello World!"); + + PipeBencodeReader.Position.Should().Be(0); + Assert.Equal('H', await PipeBencodeReader.PeekCharAsync()); + PipeBencodeReader.Position.Should().Be(0); + Assert.Equal('H', await PipeBencodeReader.PeekCharAsync()); + PipeBencodeReader.Position.Should().Be(0); + } + + [Fact] + public async Task PeekAndReadEqual() + { + Write("Hello World!"); + + Assert.Equal('H', await PipeBencodeReader.PeekCharAsync()); + Assert.Equal('H', await PipeBencodeReader.ReadCharAsync()); + } + + [Fact] + public async Task PeekAreChangedAfterReadChar() + { + Write("abcdefghijkl"); + + Assert.Equal('a', await PipeBencodeReader.PeekCharAsync()); + Assert.Equal('a', await PipeBencodeReader.ReadCharAsync()); + + Assert.Equal('b', await PipeBencodeReader.PeekCharAsync()); + Assert.Equal('b', await PipeBencodeReader.ReadCharAsync()); + + Assert.Equal('c', await PipeBencodeReader.PeekCharAsync()); + Assert.Equal('c', await PipeBencodeReader.ReadCharAsync()); + + Assert.Equal('d', await PipeBencodeReader.PeekCharAsync()); + Assert.Equal('d', await PipeBencodeReader.ReadCharAsync()); + + Assert.Equal('e', await PipeBencodeReader.PeekCharAsync()); + Assert.Equal('e', await PipeBencodeReader.ReadCharAsync()); + } + + [Fact] + public async Task PeekAreChangedAfterReadSingleByte() + { + Write("abcdefghijkl"); + + var buffer = new byte[1]; + long read; + + Assert.Equal('a', await PipeBencodeReader.PeekCharAsync()); + + read = await PipeBencodeReader.ReadAsync(buffer); + Assert.Equal(1, read); + Assert.Equal('a', (char)buffer[0]); + Assert.Equal('b', await PipeBencodeReader.PeekCharAsync()); + + read = await PipeBencodeReader.ReadAsync(buffer); + Assert.Equal(1, read); + Assert.Equal('b', (char)buffer[0]); + Assert.Equal('c', await PipeBencodeReader.PeekCharAsync()); + } + + [Fact] + public async Task PeekAreChangedAfterReadMultipleBytes() + { + Write("abcdefghijkl"); + + var buffer = new byte[2]; + + Assert.Equal('a', await PipeBencodeReader.PeekCharAsync()); + + var read = await PipeBencodeReader.ReadAsync(buffer); + read.Should().Be(2); + Assert.Equal('a', (char)buffer[0]); + Assert.Equal('b', (char)buffer[1]); + Assert.Equal('c', await PipeBencodeReader.PeekCharAsync()); + + read = await PipeBencodeReader.ReadAsync(buffer); + read.Should().Be(2); + Assert.Equal('c', (char)buffer[0]); + Assert.Equal('d', (char)buffer[1]); + Assert.Equal('e', await PipeBencodeReader.PeekCharAsync()); + } + + [Fact] + public async Task PeekAtEndOfStreamThenReadSingleByte() + { + Write("abcdefghijkl"); + + await PipeBencodeReader.SkipBytesAsync(12); + Assert.Equal(default, await PipeBencodeReader.PeekCharAsync()); + Assert.Equal(default, await PipeBencodeReader.ReadCharAsync()); + } + + [Fact] + public async Task PeekAtEndOfStreamThenReadBytes() + { + Write("abcdefghijkl"); + + await PipeBencodeReader.SkipBytesAsync(12); + Assert.Equal(default, await PipeBencodeReader.PeekCharAsync()); + Assert.Equal(0, await PipeBencodeReader.ReadAsync(new byte[4])); + } + + #endregion + + private void Write(string str) + { + PipeWriter.Write(Encoding.UTF8.GetBytes(str)); + PipeWriter.Complete(); + } + } +} diff --git a/BencodeNET.Tests/LengthNotSupportedStream.cs b/BencodeNET.Tests/LengthNotSupportedStream.cs new file mode 100644 index 0000000000..1f717a2e0f --- /dev/null +++ b/BencodeNET.Tests/LengthNotSupportedStream.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; +using System.Text; + +namespace BencodeNET.Tests +{ + internal class LengthNotSupportedStream : MemoryStream + { + public LengthNotSupportedStream(string str) + : this(str, Encoding.UTF8) + { + } + + public LengthNotSupportedStream(string str, Encoding encoding) + : base(encoding.GetBytes(str)) + { + } + + public override long Length => throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/BencodeNET.Tests/Objects/BDictionaryTests.cs b/BencodeNET.Tests/Objects/BDictionaryTests.cs index e9a537719e..c1055f2c9e 100644 --- a/BencodeNET.Tests/Objects/BDictionaryTests.cs +++ b/BencodeNET.Tests/Objects/BDictionaryTests.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; using System.Linq; +using System.Text; +using System.Threading.Tasks; using BencodeNET.Objects; using FluentAssertions; using Xunit; @@ -10,15 +14,14 @@ namespace BencodeNET.Tests.Objects public class BDictionaryTests { [Fact] - public void Add_NullValue_ThrowsArgumentNullException() + public void Add_NullStringValue_ResultsInEmptyBString() { - var dict = new BDictionary(); - Action action = () => dict.Add("key", null); - action.Should().Throw(); + var dict = new BDictionary {{"key", (string) null}}; + dict.Get("key").Value.ToArray().Should().BeEmpty(); } [Fact] - public void Add_NullIBobjectValue_ThrowsArgumentNullException() + public void Add_NullIBObjectValue_ThrowsArgumentNullException() { var dict = new BDictionary(); Action action = () => dict.Add("key", (IBObject)null); @@ -41,6 +44,8 @@ public void Indexer_Set_Null_ThrowsArgumentNullException() action.Should().Throw(); } + #region MergeWith + [Fact] public void MergeWith_StringReplacesExistingKey() { @@ -226,6 +231,10 @@ public void MergeWith_DictionaryWithNewKeyIsAdded() dict1.Get("main2").Should().HaveCount(1).And.ContainKeys("key2"); } + #endregion + + #region SequenceEqual + [Fact] public void SequenceEqual_WithKeysAddedInSameOrder_AreEqual() { @@ -298,6 +307,10 @@ public void SequenceEqual_WithDifferentValues_AreNotEqual() bdict1.SequenceEqual(bdict2).Should().BeFalse(); } + #endregion + + #region Encode + [Fact] public void CanEncode_Simple() { @@ -366,5 +379,63 @@ public void CanEncode_Complex() bencode.Should() .Be("d6:A Listl3:foo3:bari123ed9:more spam9:more eggsee6:foobard7:numbersli1ei2ei3eee4:spam3:egge"); } + + #endregion + + [Fact] + public void GetSizeInBytes() + { + var bdict = new BDictionary + { + // 6 + 5 + {"spam", "egg"}, + // 6 + 3 + 3 + 3 + 3 (+ 2) + { "list", new BList { 1, 2, 3} }, + // 5 + 5 + { "str", "abc" }, + // 5 + 4 + { "num", 42 } + }; // 2 + bdict.GetSizeInBytes().Should().Be(49); + } + + [Fact] + public async Task WriteToPipeWriter() + { + var dict = new BDictionary { { "key", "value" } }; + var (reader, writer) = new Pipe(); + + dict.EncodeTo(writer); + await writer.FlushAsync(); + reader.TryRead(out var readResult); + + var result = Encoding.UTF8.GetString(readResult.Buffer.First.Span.ToArray()); + result.Should().Be("d3:key5:valuee"); + } + + [Fact] + public async Task WriteToPipeWriterAsync() + { + var dict = new BDictionary { { "key", "value" } }; + var (reader, writer) = new Pipe(); + + await dict.EncodeToAsync(writer); + reader.TryRead(out var readResult); + + var result = Encoding.UTF8.GetString(readResult.Buffer.First.Span.ToArray()); + result.Should().Be("d3:key5:valuee"); + } + + [Fact] + public async Task WriteToStreamAsync() + { + var dict = new BDictionary { { "key", "value" } }; + + var stream = new MemoryStream(); + await dict.EncodeToAsync(stream); + + var result = Encoding.UTF8.GetString(stream.ToArray()); + result.Should().Be("d3:key5:valuee"); + } } } diff --git a/BencodeNET.Tests/Objects/BListTests.cs b/BencodeNET.Tests/Objects/BListTests.cs index 3b74ea73f6..aaa7b1f2e1 100644 --- a/BencodeNET.Tests/Objects/BListTests.cs +++ b/BencodeNET.Tests/Objects/BListTests.cs @@ -1,6 +1,9 @@ using System; +using System.IO; +using System.IO.Pipelines; using System.Linq; using System.Text; +using System.Threading.Tasks; using BencodeNET.Objects; using FluentAssertions; using Xunit; @@ -73,6 +76,8 @@ public void SequenceEqual_DifferentOrder_AreNotEqual() blist1.SequenceEqual(blist2).Should().BeFalse(); } + #region Encode + [Fact] public void CanEncode_Simple() { @@ -138,6 +143,10 @@ public void CanEncode_Complex() bencode.Should().Be("l4:spami666el3:foo3:bari123ed9:more spam9:more eggsee6:foobard7:numbersli1ei2ei3eeee"); } + #endregion + + #region AsType/AsStrings/AsNumbers + [Fact] public void AsType_ConvertsToListOfType() { @@ -193,5 +202,53 @@ public void AsNumbers_ContainingNonBNumberType_ThrowsInvalidCastException() Action action = () => blist.AsNumbers(); action.Should().Throw(); } + + #endregion + + [Fact] + public void GetSizeInBytes() + { + var blist = new BList{1, 2, "abc"}; + blist.GetSizeInBytes().Should().Be(13); + } + + [Fact] + public async Task WriteToPipeWriter() + { + var blist = new BList { 1, 2, "abc" }; + var (reader, writer) = new Pipe(); + + blist.EncodeTo(writer); + await writer.FlushAsync(); + reader.TryRead(out var readResult); + + var result = Encoding.UTF8.GetString(readResult.Buffer.First.Span.ToArray()); + result.Should().Be("li1ei2e3:abce"); + } + + [Fact] + public async Task WriteToPipeWriterAsync() + { + var blist = new BList { 1, 2, "abc" }; + var (reader, writer) = new Pipe(); + + await blist.EncodeToAsync(writer); + reader.TryRead(out var readResult); + + var result = Encoding.UTF8.GetString(readResult.Buffer.First.Span.ToArray()); + result.Should().Be("li1ei2e3:abce"); + } + + [Fact] + public async Task WriteToStreamAsync() + { + var blist = new BList { 1, 2, "abc" }; + + var stream = new MemoryStream(); + await blist.EncodeToAsync(stream); + + var result = Encoding.UTF8.GetString(stream.ToArray()); + result.Should().Be("li1ei2e3:abce"); + } } } diff --git a/BencodeNET.Tests/Objects/BNumberTests.cs b/BencodeNET.Tests/Objects/BNumberTests.cs index 14620272ed..501fcd8ae6 100644 --- a/BencodeNET.Tests/Objects/BNumberTests.cs +++ b/BencodeNET.Tests/Objects/BNumberTests.cs @@ -1,5 +1,8 @@ using System; using System.IO; +using System.IO.Pipelines; +using System.Text; +using System.Threading.Tasks; using BencodeNET.Objects; using FluentAssertions; using Xunit; @@ -73,6 +76,8 @@ public void GetHashCode_SameAsInt64HashCode(long number) hash1.Should().Be(hash2); } + #region Encode + [Theory] [InlineAutoMockedData(1)] [InlineAutoMockedData(10)] @@ -124,7 +129,7 @@ public void CanEncode_Int64MaxValue() } [Fact] - public void CanEnodeToStream() + public void CanEncodeToStream() { var bnumber = new BNumber(42); @@ -137,6 +142,19 @@ public void CanEnodeToStream() } } + [Fact] + public void CanEncodeAsBytes() + { + var bnumber = new BNumber(42); + var expected = Encoding.ASCII.GetBytes("i42e"); + + var bytes = bnumber.EncodeAsBytes(); + + bytes.Should().BeEquivalentTo(expected); + } + + #endregion + [Fact] public void ToString_SameAsLong() { @@ -159,6 +177,8 @@ public void ToString_WithFormat_SameAsLong() str1.Should().Be(str2); } + #region Casts + [Fact] public void CanCastFromInt() { @@ -313,5 +333,53 @@ public void CastingToBool_Null_ThrowsInvalidCastException() Action action = () => { var b = (bool) bnumber; }; action.Should().Throw(); } + + #endregion + + [Fact] + public void GetSizeInBytes() + { + var bnumber = new BNumber(42); + bnumber.GetSizeInBytes().Should().Be(4); + } + + [Fact] + public async Task WriteToPipeWriter() + { + var bnumber = new BNumber(1234); + var (reader, writer) = new Pipe(); + + bnumber.EncodeTo(writer); + await writer.FlushAsync(); + reader.TryRead(out var readResult); + + var result = Encoding.UTF8.GetString(readResult.Buffer.First.Span.ToArray()); + result.Should().Be("i1234e"); + } + + [Fact] + public async Task WriteToPipeWriterAsync() + { + var bnumber = new BNumber(1234); + var (reader, writer) = new Pipe(); + + await bnumber.EncodeToAsync(writer); + reader.TryRead(out var readResult); + + var result = Encoding.UTF8.GetString(readResult.Buffer.First.Span.ToArray()); + result.Should().Be("i1234e"); + } + + [Fact] + public async Task WriteToStreamAsync() + { + var bnumber = new BNumber(1234); + + var ms = new MemoryStream(); + await bnumber.EncodeToAsync(ms); + + var result = Encoding.UTF8.GetString(ms.ToArray()); + result.Should().Be("i1234e"); + } } } diff --git a/BencodeNET.Tests/Objects/BStringTests.cs b/BencodeNET.Tests/Objects/BStringTests.cs index 37a6bab13f..1713e7268b 100644 --- a/BencodeNET.Tests/Objects/BStringTests.cs +++ b/BencodeNET.Tests/Objects/BStringTests.cs @@ -1,6 +1,7 @@ -using System; -using System.IO; +using System.IO; +using System.IO.Pipelines; using System.Text; +using System.Threading.Tasks; using BencodeNET.Objects; using FluentAssertions; using Xunit; @@ -10,13 +11,21 @@ namespace BencodeNET.Tests.Objects public class BStringTests { [Fact] - public void ConstructorWithNullValue_ThrowsArgumentNullException() + public void ConstructorEmpty_ResultsInEmptyValue() { - Action action = () => new BString((string) null); + var bstring = new BString(); + bstring.Value.ToArray().Should().BeEmpty(); + } - action.Should().Throw(); + [Fact] + public void ConstructorWithNullValue_ResultsInEmptyValue() + { + var bstring = new BString((string)null); + bstring.Value.ToArray().Should().BeEmpty(); } + #region Equals + [Theory] [InlineAutoMockedData("hello world", "hello world")] [InlineAutoMockedData("a", "a")] @@ -139,6 +148,8 @@ public void GetHashCode_AreNotEqualWithDifferentValues(string other) bstring.GetHashCode().Should().NotBe(otherBString.GetHashCode()); } + #endregion + [Fact] public void Encoding_DefaultIsUTF8() { @@ -146,6 +157,8 @@ public void Encoding_DefaultIsUTF8() bstring.Encoding.Should().Be(Encoding.UTF8); } + #region Encode + [Theory] [InlineAutoMockedData("some string", 11)] [InlineAutoMockedData("spam", 4)] @@ -157,6 +170,14 @@ public void CanEncode(string str, int length) bencode.Should().Be($"{length}:{str}"); } + [Fact] + public void CanEncode_NullString() + { + var bstring = new BString(); + var bencode = bstring.EncodeAsString(); + bencode.Should().Be("0:"); + } + [Fact] public void CanEncode_EmptyString() { @@ -204,6 +225,33 @@ public void CanEncode_NumbersAndSpecialCharacters() bencode.Should().Be("13:123:?!#{}'|<>"); } + [Fact] + public void CanEncodeToStream() + { + var bstring = new BString("hello world"); + + using (var stream = new MemoryStream()) + { + bstring.EncodeTo(stream); + + stream.Length.Should().Be(14); + stream.AsString().Should().Be("11:hello world"); + } + } + + [Fact] + public void CanEncodeAsBytes() + { + var bstring = new BString("hello world"); + var expected = Encoding.ASCII.GetBytes("11:hello world"); + + var bytes = bstring.EncodeAsBytes(); + + bytes.Should().BeEquivalentTo(expected); + } + + #endregion + [Fact] public void ToString_WithoutEncoding_EncodesUsingUTF8() { @@ -222,17 +270,56 @@ public void ToString_ISO88591() } [Fact] - public void CanEncodeToStream() + public void GetSizeInBytes() { - var bstring = new BString("hello world"); + var bstring = new BString("abc", Encoding.UTF8); + bstring.GetSizeInBytes().Should().Be(5); + } - using (var stream = new MemoryStream()) - { - bstring.EncodeTo(stream); + [Fact] + public void GetSizeInBytes_UTF8() + { + var bstring = new BString("æøå äö èéê ñ", Encoding.UTF8); + bstring.GetSizeInBytes().Should().Be(24); + } - stream.Length.Should().Be(14); - stream.AsString().Should().Be("11:hello world"); - } + [Fact] + public async Task WriteToPipeWriter() + { + var bstring = new BString("æøå äö èéê ñ"); + var (reader, writer) = new Pipe(); + + bstring.EncodeTo(writer); + await writer.FlushAsync(); + reader.TryRead(out var readResult); + + var result = Encoding.UTF8.GetString(readResult.Buffer.First.Span.ToArray()); + result.Should().Be("21:æøå äö èéê ñ"); + } + + [Fact] + public async Task WriteToPipeWriterAsync() + { + var bstring = new BString("æøå äö èéê ñ"); + var (reader, writer) = new Pipe(); + + await bstring.EncodeToAsync(writer); + reader.TryRead(out var readResult); + + var result = Encoding.UTF8.GetString(readResult.Buffer.First.Span.ToArray()); + result.Should().Be("21:æøå äö èéê ñ"); + } + + [Fact] + public async Task WriteToStreamAsync() + { + var bstring = new BString("æøå äö èéê ñ"); + + var stream = new MemoryStream(); + await bstring.EncodeToAsync(stream); + + var result = Encoding.UTF8.GetString(stream.ToArray()); + result.Should().Be("21:æøå äö èéê ñ"); } } } diff --git a/BencodeNET.Tests/Parsing/BDictionaryParserTests.Async.cs b/BencodeNET.Tests/Parsing/BDictionaryParserTests.Async.cs new file mode 100644 index 0000000000..7b74ba9899 --- /dev/null +++ b/BencodeNET.Tests/Parsing/BDictionaryParserTests.Async.cs @@ -0,0 +1,138 @@ +using System; +using System.Threading.Tasks; +using BencodeNET.Exceptions; +using BencodeNET.IO; +using BencodeNET.Objects; +using BencodeNET.Parsing; +using FluentAssertions; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace BencodeNET.Tests.Parsing +{ + public partial class BDictionaryParserTests + { + [Theory] + [InlineAutoMockedData("d4:spam3:egge")] + public async Task CanParseSimpleAsync(string bencode, IBencodeParser bparser) + { + // Arrange + var key = new BString("key"); + var value = new BString("value"); + + bparser.ParseAsync(Arg.Any()) + .Returns(key); + + bparser.ParseAsync(Arg.Any()) + .Returns(value) + .AndSkipsAheadAsync(bencode.Length - 2); + + // Act + var parser = new BDictionaryParser(bparser); + var bdictionary = await parser.ParseStringAsync(bencode); + + // Assert + bdictionary.Count.Should().Be(1); + bdictionary.Should().ContainKey(key); + bdictionary[key].Should().BeSameAs(value); + } + + [Theory] + [InlineAutoMockedData("de")] + public async Task CanParseEmptyDictionaryAsync(string bencode, IBencodeParser bparser) + { + var parser = new BDictionaryParser(bparser); + var bdictionary = await parser.ParseStringAsync(bencode); + + bdictionary.Count.Should().Be(0); + } + + [Theory] + [InlineAutoMockedData("")] + [InlineAutoMockedData("d")] + public void BelowMinimumLength2_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser) + { + var parser = new BDictionaryParser(bparser); + Func action = async () => await parser.ParseStringAsync(bencode); + + action.Should().Throw>().WithMessage("*reached end of stream*"); + } + + [Theory] + [InlineAutoMockedData("ade")] + [InlineAutoMockedData(":de")] + [InlineAutoMockedData("-de")] + [InlineAutoMockedData("1de")] + public void InvalidFirstChar_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser) + { + var parser = new BDictionaryParser(bparser); + Func action = async () => await parser.ParseStringAsync(bencode); + + action.Should().Throw>().WithMessage("*Unexpected character*"); + } + + [Theory] + [InlineAutoMockedData("da")] + [InlineAutoMockedData("d4:spam3:egg")] + [InlineAutoMockedData("d ")] + public void MissingEndChar_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser, BString someKey, IBObject someValue) + { + // Arrange + bparser.ParseAsync(Arg.Any()) + .Returns(someKey); + + bparser.ParseAsync(Arg.Any()) + .Returns(someValue) + .AndSkipsAheadAsync(bencode.Length - 1); + + // Act + var parser = new BDictionaryParser(bparser); + Func action = async () => await parser.ParseStringAsync(bencode); + + // Assert + action.Should().Throw>().WithMessage("*Missing end character of object*"); + } + + [Theory] + [InlineAutoMockedData] + public void InvalidKey_ThrowsInvalidBencodeExceptionAsync(IBencodeParser bparser) + { + bparser.ParseAsync(Arg.Any()).Throws(); + + var parser = new BDictionaryParser(bparser); + + Func action = async () => await parser.ParseStringAsync("di42ee"); + + action.Should().Throw>().WithMessage("*Could not parse dictionary key*"); + } + + [Theory] + [InlineAutoMockedData] + public void InvalidValue_ThrowsInvalidBencodeExceptionAsync(IBencodeParser bparser, BString someKey) + { + bparser.ParseAsync(Arg.Any()).Returns(someKey); + bparser.ParseAsync(Arg.Any()).Throws(); + + var parser = new BDictionaryParser(bparser); + + Func action = async () => await parser.ParseStringAsync("di42ee"); + + action.Should().Throw>().WithMessage("*Could not parse dictionary value*"); + } + + [Theory] + [InlineAutoMockedData] + public void DuplicateKey_ThrowsInvalidBencodeExceptionAsync(IBencodeParser bparser, BString someKey, BString someValue) + { + bparser.ParseAsync(Arg.Any()).Returns(someKey, someKey); + bparser.ParseAsync(Arg.Any()).Returns(someValue); + + var parser = new BDictionaryParser(bparser); + + Func action = async () => await parser.ParseStringAsync("di42ee"); + + action.Should().Throw>().WithMessage("*The dictionary already contains the key*"); + } + } +} diff --git a/BencodeNET.Tests/Parsing/BDictionaryParserTests.cs b/BencodeNET.Tests/Parsing/BDictionaryParserTests.cs index 0392366d33..49ae7869f5 100644 --- a/BencodeNET.Tests/Parsing/BDictionaryParserTests.cs +++ b/BencodeNET.Tests/Parsing/BDictionaryParserTests.cs @@ -5,23 +5,33 @@ using BencodeNET.Parsing; using FluentAssertions; using NSubstitute; +using NSubstitute.ExceptionExtensions; using Xunit; namespace BencodeNET.Tests.Parsing { - public class BDictionaryParserTests + public partial class BDictionaryParserTests { [Theory] [InlineAutoMockedData("d4:spam3:egge")] public void CanParseSimple(string bencode, IBencodeParser bparser) { + // Arange var key = new BString("key"); var value = new BString("value"); - SetupBencodeParser(bparser, bencode, key, value, hasEndChar:true); + bparser.Parse(Arg.Any()) + .Returns(key); + + bparser.Parse(Arg.Any()) + .Returns(value) + .AndSkipsAhead(bencode.Length - 2); + + // Act var parser = new BDictionaryParser(bparser); var bdictionary = parser.ParseString(bencode); + // Assert bdictionary.Count.Should().Be(1); bdictionary.Should().ContainKey(key); bdictionary[key].Should().BeSameAs(value); @@ -38,12 +48,26 @@ public void CanParseEmptyDictionary(string bencode, IBencodeParser bparser) } [Theory] + [InlineAutoMockedData("")] [InlineAutoMockedData("d")] public void BelowMinimumLength2_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser) { var parser = new BDictionaryParser(bparser); Action action = () => parser.ParseString(bencode); + action.Should().Throw>().WithMessage("*Invalid length*"); + } + + [Theory] + [InlineAutoMockedData("")] + [InlineAutoMockedData("d")] + public void BelowMinimumLength2_WhenStreamLengthNotSupported_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser) + { + var stream = new LengthNotSupportedStream(bencode); + + var parser = new BDictionaryParser(bparser); + Action action = () => parser.Parse(stream); + action.Should().Throw>(); } @@ -57,38 +81,70 @@ public void InvalidFirstChar_ThrowsInvalidBencodeException(string bencode, IBenc var parser = new BDictionaryParser(bparser); Action action = () => parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>().WithMessage("*Unexpected character*"); } [Theory] [InlineAutoMockedData("da")] [InlineAutoMockedData("d4:spam3:egg")] [InlineAutoMockedData("d ")] - public void MissingEndChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser) + public void MissingEndChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser, BString someKey, IBObject someValue) { - SetupBencodeParser(bparser, bencode, new BString("key"), new BString("value"), hasEndChar:false); + // Arrange + bparser.Parse(Arg.Any()) + .Returns(someKey); + bparser.Parse(Arg.Any()) + .Returns(someValue) + .AndSkipsAhead(bencode.Length - 1); + + // Act var parser = new BDictionaryParser(bparser); Action action = () => parser.ParseString(bencode); - action.Should().Throw>(); + // Assert + action.Should().Throw>().WithMessage("*Missing end character of object*"); } - private static void SetupBencodeParser(IBencodeParser bparser, string bencode, BString key, IBObject value, bool hasEndChar) + [Theory] + [InlineAutoMockedData] + public void InvalidKey_ThrowsInvalidBencodeException(IBencodeParser bparser) { - bparser.Parse(Arg.Any()) - .Returns(key); + bparser.Parse(Arg.Any()).Throws(); - bparser.Parse(Arg.Any()) - .Returns(value) - .AndDoes(x => - { - // Set stream position to end of list, skipping all "parsed" content - var stream = x.Arg(); - stream.Position += Math.Max(1, bencode.Length - 1); - - if (hasEndChar) stream.Position--; - }); + var parser = new BDictionaryParser(bparser); + + Action action = () => parser.ParseString("di42ee"); + + action.Should().Throw>().WithMessage("*Could not parse dictionary key*"); + } + + [Theory] + [InlineAutoMockedData] + public void InvalidValue_ThrowsInvalidBencodeException(IBencodeParser bparser, BString someKey) + { + bparser.Parse(Arg.Any()).Returns(someKey); + bparser.Parse(Arg.Any()).Throws(); + + var parser = new BDictionaryParser(bparser); + + Action action = () => parser.ParseString("di42ee"); + + action.Should().Throw>().WithMessage("*Could not parse dictionary value*"); + } + + [Theory] + [InlineAutoMockedData] + public void DuplicateKey_ThrowsInvalidBencodeException(IBencodeParser bparser, BString someKey, BString someValue) + { + bparser.Parse(Arg.Any()).Returns(someKey, someKey); + bparser.Parse(Arg.Any()).Returns(someValue); + + var parser = new BDictionaryParser(bparser); + + Action action = () => parser.ParseString("di42ee"); + + action.Should().Throw>().WithMessage("*The dictionary already contains the key*"); } } } diff --git a/BencodeNET.Tests/Parsing/BListParserTests.Async.cs b/BencodeNET.Tests/Parsing/BListParserTests.Async.cs new file mode 100644 index 0000000000..778e4d31bb --- /dev/null +++ b/BencodeNET.Tests/Parsing/BListParserTests.Async.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading.Tasks; +using BencodeNET.Exceptions; +using BencodeNET.IO; +using BencodeNET.Objects; +using BencodeNET.Parsing; +using FluentAssertions; +using NSubstitute; +using Xunit; + +namespace BencodeNET.Tests.Parsing +{ + public partial class BListParserTests + { + [Theory] + [InlineAutoMockedData("l-something-e")] + [InlineAutoMockedData("l4:spame")] + [InlineAutoMockedData("l4:spami42ee")] + public async Task CanParseSimpleAsync(string bencode, IBencodeParser bparser) + { + // Arrange + var bstring = new BString("test"); + bparser.ParseAsync(Arg.Any()) + .Returns(bstring) + .AndSkipsAheadAsync(bencode.Length - 2); + + // Act + var parser = new BListParser(bparser); + var blist = await parser.ParseStringAsync(bencode); + + // Assert + blist.Count.Should().Be(1); + blist[0].Should().BeOfType(); + blist[0].Should().BeSameAs(bstring); + await bparser.Received(1).ParseAsync(Arg.Any()); + } + + [Theory] + [InlineAutoMockedData("le")] + public async Task CanParseEmptyListAsync(string bencode, IBencodeParser bparser) + { + var parser = new BListParser(bparser); + var blist = await parser.ParseStringAsync(bencode); + + blist.Count.Should().Be(0); + await bparser.DidNotReceive().ParseAsync(Arg.Any()); + } + + [Theory] + [InlineAutoMockedData("")] + [InlineAutoMockedData("l")] + public void BelowMinimumLength2_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser) + { + var parser = new BListParser(bparser); + Func action = async () => await parser.ParseStringAsync(bencode); + + action.Should().Throw>().WithMessage("*reached end of stream*"); + } + + [Theory] + [InlineAutoMockedData("4e")] + [InlineAutoMockedData("ae")] + [InlineAutoMockedData(":e")] + [InlineAutoMockedData("-e")] + [InlineAutoMockedData(".e")] + [InlineAutoMockedData("ee")] + public void InvalidFirstChar_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser) + { + var parser = new BListParser(bparser); + Func action = async () => await parser.ParseStringAsync(bencode); + + action.Should().Throw>().WithMessage("*Unexpected character*"); + } + + [Theory] + [InlineAutoMockedData("l4:spam")] + [InlineAutoMockedData("l ")] + [InlineAutoMockedData("l:")] + public void MissingEndChar_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser, IBObject something) + { + // Arrange + bparser.ParseAsync(Arg.Any()) + .Returns(something) + .AndSkipsAheadAsync(bencode.Length - 1); + + // Act + var parser = new BListParser(bparser); + Func action = async () => await parser.ParseStringAsync(bencode); + + // Assert + action.Should().Throw>().WithMessage("*Missing end character of object*"); + } + } +} diff --git a/BencodeNET.Tests/Parsing/BListParserTests.cs b/BencodeNET.Tests/Parsing/BListParserTests.cs index abd0f25ce6..73adbcb3af 100644 --- a/BencodeNET.Tests/Parsing/BListParserTests.cs +++ b/BencodeNET.Tests/Parsing/BListParserTests.cs @@ -9,7 +9,7 @@ namespace BencodeNET.Tests.Parsing { - public class BListParserTests + public partial class BListParserTests { [Theory] [InlineAutoMockedData("l-something-e")] @@ -19,7 +19,9 @@ public void CanParseSimple(string bencode, IBencodeParser bparser) { // Arrange var bstring = new BString("test"); - SetupBencodeParser(bparser, bencode, bstring, hasEndChar: true); + bparser.Parse(Arg.Any()) + .Returns(bstring) + .AndSkipsAhead(bencode.Length - 2); // Act var parser = new BListParser(bparser); @@ -29,7 +31,7 @@ public void CanParseSimple(string bencode, IBencodeParser bparser) blist.Count.Should().Be(1); blist[0].Should().BeOfType(); blist[0].Should().BeSameAs(bstring); - bparser.Received(1).Parse(Arg.Any()); + bparser.Received(1).Parse(Arg.Any()); } [Theory] @@ -40,17 +42,18 @@ public void CanParseEmptyList(string bencode, IBencodeParser bparser) var blist = parser.ParseString(bencode); blist.Count.Should().Be(0); - bparser.Received(0).Parse(Arg.Any()); + bparser.DidNotReceive().Parse(Arg.Any()); } [Theory] + [InlineAutoMockedData("")] [InlineAutoMockedData("l")] public void BelowMinimumLength2_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser) { var parser = new BListParser(bparser); Action action = () => parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>().WithMessage("*Invalid length*"); } [Theory] @@ -60,45 +63,48 @@ public void BelowMinimumLength2_ThrowsInvalidBencodeException(string bencode, IB [InlineAutoMockedData("-")] [InlineAutoMockedData(".")] [InlineAutoMockedData("e")] + public void BelowMinimumLength2_WhenStreamLengthNotSupported_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser) + { + var stream = new LengthNotSupportedStream(bencode); + + var parser = new BListParser(bparser); + Action action = () => parser.Parse(stream); + + action.Should().Throw>().WithMessage("*Unexpected character*"); + } + + [Theory] + [InlineAutoMockedData("4e")] + [InlineAutoMockedData("ae")] + [InlineAutoMockedData(":e")] + [InlineAutoMockedData("-e")] + [InlineAutoMockedData(".e")] + [InlineAutoMockedData("ee")] public void InvalidFirstChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser) { var parser = new BListParser(bparser); Action action = () => parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>().WithMessage("*Unexpected character*"); } [Theory] - [InlineAutoMockedData("l")] [InlineAutoMockedData("l4:spam")] [InlineAutoMockedData("l ")] [InlineAutoMockedData("l:")] - public void MissingEndChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser) + public void MissingEndChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser, IBObject something) { // Arrange - var bstring = new BString("test"); - SetupBencodeParser(bparser, bencode, bstring, hasEndChar:false); + bparser.Parse(Arg.Any()) + .Returns(something) + .AndSkipsAhead(bencode.Length - 1); // Act var parser = new BListParser(bparser); Action action = () => parser.ParseString(bencode); // Assert - action.Should().Throw>(); - } - - private static void SetupBencodeParser(IBencodeParser bparser, string bencode, IBObject obj, bool hasEndChar) - { - bparser.Parse(Arg.Any()) - .Returns(obj) - .AndDoes(x => - { - // Set stream position to end of list, skipping all "parsed" content - var stream = x.Arg(); - stream.Position += Math.Max(1, bencode.Length - 1); - - if (hasEndChar) stream.Position--; - }); + action.Should().Throw>().WithMessage("*Missing end character of object*"); } } } diff --git a/BencodeNET.Tests/Parsing/BNumberParserTests.Async.cs b/BencodeNET.Tests/Parsing/BNumberParserTests.Async.cs new file mode 100644 index 0000000000..2a658aeae9 --- /dev/null +++ b/BencodeNET.Tests/Parsing/BNumberParserTests.Async.cs @@ -0,0 +1,175 @@ +using System; +using System.Threading.Tasks; +using BencodeNET.Exceptions; +using BencodeNET.Objects; +using BencodeNET.Parsing; +using FluentAssertions; +using Xunit; + +namespace BencodeNET.Tests.Parsing +{ + public partial class BNumberParserTests + { + [Theory] + [InlineData("i1e", 1)] + [InlineData("i2e", 2)] + [InlineData("i3e", 3)] + [InlineData("i42e", 42)] + [InlineData("i100e", 100)] + [InlineData("i1234567890e", 1234567890)] + public async Task CanParsePositiveAsync(string bencode, int value) + { + var bnumber = await Parser.ParseStringAsync(bencode); + bnumber.Should().Be(value); + } + + [Fact] + public async Task CanParseZeroAsync() + { + var bnumber = await Parser.ParseStringAsync("i0e"); + bnumber.Should().Be(0); + } + + [Theory] + [InlineData("i-1e", -1)] + [InlineData("i-2e", -2)] + [InlineData("i-3e", -3)] + [InlineData("i-42e", -42)] + [InlineData("i-100e", -100)] + [InlineData("i-1234567890e", -1234567890)] + public async Task CanParseNegativeAsync(string bencode, int value) + { + var bnumber = await Parser.ParseStringAsync(bencode); + bnumber.Should().Be(value); + } + + [Theory] + [InlineData("i9223372036854775807e", 9223372036854775807)] + [InlineData("i-9223372036854775808e", -9223372036854775808)] + public async Task CanParseInt64Async(string bencode, long value) + { + var bnumber = await Parser.ParseStringAsync(bencode); + bnumber.Should().Be(value); + } + + [Theory] + [InlineData("i01e")] + [InlineData("i012e")] + [InlineData("i01234567890e")] + [InlineData("i00001e")] + public void LeadingZeros_ThrowsInvalidBencodeExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*Leading '0's are not valid.*") + .Which.StreamPosition.Should().Be(0); + } + + [Fact] + public void MinusZero_ThrowsInvalidBencodeExceptionAsync() + { + Func action = async () => await Parser.ParseStringAsync("i-0e"); + action.Should().Throw>() + .WithMessage("*'-0' is not a valid number.*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("i12")] + [InlineData("i123")] + public void MissingEndChar_ThrowsInvalidBencodeExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*Missing end character of object.*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("42e")] + [InlineData("a42e")] + [InlineData("d42e")] + [InlineData("l42e")] + [InlineData("100e")] + [InlineData("1234567890e")] + public void InvalidFirstChar_ThrowsInvalidBencodeExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*Unexpected character. Expected 'i'*") + .Which.StreamPosition.Should().Be(0); + } + + [Fact] + public void JustNegativeSign_ThrowsInvalidBencodeExceptionAsync() + { + Func action = async () => await Parser.ParseStringAsync("i-e"); + action.Should().Throw>() + .WithMessage("*It contains no digits.*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("i--1e")] + [InlineData("i--42e")] + [InlineData("i---100e")] + [InlineData("i----1234567890e")] + public void MoreThanOneNegativeSign_ThrowsInvalidBencodeExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*The value '*' is not a valid number.*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("iasdfe")] + [InlineData("i!#¤%&e")] + [InlineData("i.e")] + [InlineData("i42.e")] + [InlineData("i42ae")] + public void NonDigit_ThrowsInvalidBencodeExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*The value '*' is not a valid number.*") + .Which.StreamPosition.Should().Be(0); + } + + + [Theory] + [InlineData("", "reached end of stream")] + [InlineData("i", "contains no digits")] + [InlineData("ie", "contains no digits")] + public void BelowMinimumLength_ThrowsInvalidBencodeExceptionAsync(string bencode, string exceptionMessage) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage($"*{exceptionMessage}*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("i9223372036854775808e")] + [InlineData("i-9223372036854775809e")] + public void LargerThanInt64_ThrowsUnsupportedExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*The value '*' is not a valid long (Int64)*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("i12345678901234567890e")] + [InlineData("i123456789012345678901e")] + [InlineData("i123456789012345678901234567890e")] + public void LongerThanMaxDigits19_ThrowsUnsupportedExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*The number '*' has more than 19 digits and cannot be stored as a long*") + .Which.StreamPosition.Should().Be(0); + } + } +} diff --git a/BencodeNET.Tests/Parsing/BNumberParserTests.cs b/BencodeNET.Tests/Parsing/BNumberParserTests.cs index 341f98d735..9ad6a12e7e 100644 --- a/BencodeNET.Tests/Parsing/BNumberParserTests.cs +++ b/BencodeNET.Tests/Parsing/BNumberParserTests.cs @@ -7,7 +7,7 @@ namespace BencodeNET.Tests.Parsing { - public class BNumberParserTests + public partial class BNumberParserTests { private BNumberParser Parser { get; } @@ -66,43 +66,53 @@ public void CanParseInt64(string bencode, long value) public void LeadingZeros_ThrowsInvalidBencodeException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*Leading '0's are not valid.*") + .Which.StreamPosition.Should().Be(0); } [Fact] public void MinusZero_ThrowsInvalidBencodeException() { Action action = () => Parser.ParseString("i-0e"); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*'-0' is not a valid number.*") + .Which.StreamPosition.Should().Be(0); } [Theory] - [InlineData("i")] - [InlineData("i1")] - [InlineData("i2")] + [InlineData("i12")] [InlineData("i123")] public void MissingEndChar_ThrowsInvalidBencodeException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*Missing end character of object.*") + .Which.StreamPosition.Should().Be(0); } [Theory] - [InlineData("1e")] [InlineData("42e")] + [InlineData("a42e")] + [InlineData("d42e")] + [InlineData("l42e")] [InlineData("100e")] [InlineData("1234567890e")] public void InvalidFirstChar_ThrowsInvalidBencodeException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*Unexpected character. Expected 'i'*") + .Which.StreamPosition.Should().Be(0); } [Fact] public void JustNegativeSign_ThrowsInvalidBencodeException() { Action action = () => Parser.ParseString("i-e"); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*It contains no digits.*") + .Which.StreamPosition.Should().Be(0); } [Theory] @@ -113,11 +123,12 @@ public void JustNegativeSign_ThrowsInvalidBencodeException() public void MoreThanOneNegativeSign_ThrowsInvalidBencodeException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*The value '*' is not a valid number.*") + .Which.StreamPosition.Should().Be(0); } [Theory] - [InlineData("i-e")] [InlineData("iasdfe")] [InlineData("i!#¤%&e")] [InlineData("i.e")] @@ -126,14 +137,34 @@ public void MoreThanOneNegativeSign_ThrowsInvalidBencodeException(string bencode public void NonDigit_ThrowsInvalidBencodeException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*The value '*' is not a valid number.*") + .Which.StreamPosition.Should().Be(0); } - [Fact] - public void BelowMinimumLength_ThrowsInvalidBencodeException() + + [Theory] + [InlineData("")] + [InlineData("i")] + [InlineData("ie")] + public void BelowMinimumLength_ThrowsInvalidBencodeException(string bencode) + { + Action action = () => Parser.ParseString(bencode); + action.Should().Throw>() + .WithMessage("*Invalid length.*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("")] + [InlineData("i")] + [InlineData("ie")] + public void BelowMinimumLength_WhenStreamWithoutLengthSupport_ThrowsInvalidException(string bencode) { - Action action = () => Parser.ParseString("ie"); - action.Should().Throw>(); + var stream = new LengthNotSupportedStream(bencode); + Action action = () => Parser.Parse(stream); + action.Should().Throw>() + .Which.StreamPosition.Should().Be(0); } [Theory] @@ -142,7 +173,9 @@ public void BelowMinimumLength_ThrowsInvalidBencodeException() public void LargerThanInt64_ThrowsUnsupportedException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*The value '*' is not a valid long (Int64)*") + .Which.StreamPosition.Should().Be(0); } [Theory] @@ -152,7 +185,9 @@ public void LargerThanInt64_ThrowsUnsupportedException(string bencode) public void LongerThanMaxDigits19_ThrowsUnsupportedException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*The number '*' has more than 19 digits and cannot be stored as a long*") + .Which.StreamPosition.Should().Be(0); } } } diff --git a/BencodeNET.Tests/Parsing/BObjectParserTests.cs b/BencodeNET.Tests/Parsing/BObjectParserTests.cs index fee13ec5c9..cc0bc7f03b 100644 --- a/BencodeNET.Tests/Parsing/BObjectParserTests.cs +++ b/BencodeNET.Tests/Parsing/BObjectParserTests.cs @@ -1,5 +1,7 @@ using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.IO; using BencodeNET.Objects; using BencodeNET.Parsing; @@ -10,32 +12,6 @@ namespace BencodeNET.Tests.Parsing { public class BObjectParserTests { - [Theory] - [InlineAutoMockedData] - public void IBObjectParser_Parse_String_CallsOverriddenParse(IBObjectParser parserMock) - { - var parser = new MockBObjectParser(parserMock) as IBObjectParser; - - parser.ParseString("bencoded string"); - - parserMock.Received().Parse(Arg.Any()); - } - - [Theory] - [InlineAutoMockedData] - public void IBObjectParser_Parse_Stream_CallsOverriddenParse(IBObjectParser parserMock) - { - var parser = new MockBObjectParser(parserMock) as IBObjectParser; - var bytes = Encoding.UTF8.GetBytes("bencoded string"); - - using (var stream = new MemoryStream(bytes)) - { - parser.Parse(stream); - } - - parserMock.Received().Parse(Arg.Any()); - } - [Theory] [InlineAutoMockedData] public void Parse_String_CallsOverriddenParse(IBObjectParser parserMock) @@ -44,7 +20,7 @@ public void Parse_String_CallsOverriddenParse(IBObjectParser parserMoc parser.ParseString("bencoded string"); - parserMock.Received().Parse(Arg.Any()); + parserMock.Received().Parse(Arg.Any()); } [Theory] @@ -59,7 +35,7 @@ public void Parse_Stream_CallsOverriddenParse(IBObjectParser parserMoc parser.Parse(stream); } - parserMock.Received().Parse(Arg.Any()); + parserMock.Received().Parse(Arg.Any()); } class MockBObjectParser : BObjectParser @@ -71,12 +47,17 @@ public MockBObjectParser(IBObjectParser substitute) public IBObjectParser Substitute { get; set; } - protected override Encoding Encoding => Encoding.UTF8; + public override Encoding Encoding => Encoding.UTF8; - public override IBObject Parse(BencodeStream stream) + public override IBObject Parse(BencodeReader stream) { return Substitute.Parse(stream); } + + public override ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } } } } diff --git a/BencodeNET.Tests/Parsing/BStringParserTests.Async.cs b/BencodeNET.Tests/Parsing/BStringParserTests.Async.cs new file mode 100644 index 0000000000..0b86ede166 --- /dev/null +++ b/BencodeNET.Tests/Parsing/BStringParserTests.Async.cs @@ -0,0 +1,166 @@ +using System; +using System.IO.Pipelines; +using System.Text; +using System.Threading.Tasks; +using BencodeNET.Exceptions; +using BencodeNET.Objects; +using BencodeNET.Parsing; +using FluentAssertions; +using Xunit; + +namespace BencodeNET.Tests.Parsing +{ + public partial class BStringParserTests + { + [Theory] + [InlineData("4:spam")] + [InlineData("8:spameggs")] + [InlineData("9:spam eggs")] + [InlineData("9:spam:eggs")] + [InlineData("14:!@#¤%&/()=?$|")] + public async Task CanParseSimpleAsync(string bencode) + { + var parts = bencode.Split(new[] {':'}, 2); + var length = int.Parse(parts[0]); + var value = parts[1]; + + var bstring = await Parser.ParseStringAsync(bencode); + + bstring.Length.Should().Be(length); + bstring.Should().Be(value); + } + + [Fact] + public async Task CanParse_EmptyStringAsync() + { + var bstring = await Parser.ParseStringAsync("0:"); + + bstring.Length.Should().Be(0); + bstring.Should().Be(""); + } + + [Theory] + [InlineData("5:spam")] + [InlineData("6:spam")] + [InlineData("100:spam")] + public void LessCharsThanSpecified_ThrowsInvalidBencodeExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*but could only read * bytes*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("4spam", 1)] + [InlineData("10spam", 2)] + [InlineData("4-spam", 1)] + [InlineData("4.spam", 1)] + [InlineData("4;spam", 1)] + [InlineData("4,spam", 1)] + [InlineData("4|spam", 1)] + public void MissingDelimiter_ThrowsInvalidBencodeExceptionAsync(string bencode, int errorIndex) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*Unexpected character. Expected ':'*") + .Which.StreamPosition.Should().Be(errorIndex); + } + + [Theory] + [InlineData("spam")] + [InlineData("-spam")] + [InlineData(".spam")] + [InlineData(",spam")] + [InlineData(";spam")] + [InlineData("?spam")] + [InlineData("!spam")] + [InlineData("#spam")] + public void NonDigitFirstChar_ThrowsInvalidBencodeExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage($"*Unexpected character. Expected ':' but found '{bencode[0]}'*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("12345678901:spam")] + [InlineData("123456789012:spam")] + [InlineData("1234567890123:spam")] + [InlineData("12345678901234:spam")] + public void LengthAboveMaxDigits10_ThrowsUnsupportedExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*Length of string is more than * digits*") + .Which.StreamPosition.Should().Be(0); + } + + [Theory] + [InlineData("1:spam")] + [InlineData("12:spam")] + [InlineData("123:spam")] + [InlineData("1234:spam")] + [InlineData("12345:spam")] + [InlineData("123456:spam")] + [InlineData("1234567:spam")] + [InlineData("12345678:spam")] + [InlineData("123456789:spam")] + [InlineData("1234567890:spam")] + public void LengthAtOrBelowMaxDigits10_DoesNotThrowUnsupportedExceptionAsync(string bencode) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().NotThrow>(); + } + + [Fact] + public void LengthAboveInt32MaxValue_ThrowsUnsupportedExceptionAsync() + { + var bencode = "2147483648:spam"; + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*Length of string is * but maximum supported length is *") + .Which.StreamPosition.Should().Be(0); + } + + [Fact] + public void LengthBelowInt32MaxValue_DoesNotThrowUnsupportedExceptionAsync() + { + var bencode = "2147483647:spam"; + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().NotThrow>(); + } + + [Fact] + public async Task CanParseEncodedAsLatin1Async() + { + var encoding = Encoding.GetEncoding("LATIN1"); + var expected = new BString("æøå", encoding); + var parser = new BStringParser(encoding); + + // "3:æøå" + var bytes = new byte[] {51, 58, 230, 248, 229}; + var (reader, writer) = new Pipe(); + await writer.WriteAsync(bytes); + + var bstring = await parser.ParseAsync(reader); + + bstring.Should().Be(expected); + bstring.GetSizeInBytes().Should().Be(5); + } + + [Theory] + [InlineData("1-:a", 1)] + [InlineData("1abc:a", 1)] + [InlineData("123?:asdf", 3)] + [InlineData("3abc:abc", 1)] + public void InvalidLengthString_ThrowsInvalidExceptionAsync(string bencode, int errorIndex) + { + Func action = async () => await Parser.ParseStringAsync(bencode); + action.Should().Throw>() + .WithMessage("*Unexpected character. Expected ':'*") + .Which.StreamPosition.Should().Be(errorIndex); + } + } +} diff --git a/BencodeNET.Tests/Parsing/BStringParserTests.cs b/BencodeNET.Tests/Parsing/BStringParserTests.cs index abedd72e1c..f3ddf4c37f 100644 --- a/BencodeNET.Tests/Parsing/BStringParserTests.cs +++ b/BencodeNET.Tests/Parsing/BStringParserTests.cs @@ -8,7 +8,7 @@ namespace BencodeNET.Tests.Parsing { - public class BStringParserTests + public partial class BStringParserTests { private BStringParser Parser { get; } @@ -45,27 +45,31 @@ public void CanParse_EmptyString() } [Theory] - [InlineData("5:spam")] - [InlineData("6:spam")] - [InlineData("100:spam")] - public void LessCharsThanSpecified_ThrowsInvalidBencodeException(string bencode) + [InlineData("5:spam", 4)] + [InlineData("6:spam", 4)] + [InlineData("100:spam", 4)] + public void LessCharsThanSpecified_ThrowsInvalidBencodeException(string bencode, int expectedReadBytes) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage($"*but could only read {expectedReadBytes} bytes*") + .Which.StreamPosition.Should().Be(0); } [Theory] - [InlineData("4spam")] - [InlineData("10spam")] - [InlineData("4-spam")] - [InlineData("4.spam")] - [InlineData("4;spam")] - [InlineData("4,spam")] - [InlineData("4|spam")] - public void MissingDelimiter_ThrowsInvalidBencodeException(string bencode) + [InlineData("4spam", 1)] + [InlineData("10spam", 2)] + [InlineData("4-spam", 1)] + [InlineData("4.spam", 1)] + [InlineData("4;spam", 1)] + [InlineData("4,spam", 1)] + [InlineData("4|spam", 1)] + public void MissingDelimiter_ThrowsInvalidBencodeException(string bencode, int errorIndex) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*Unexpected character. Expected ':'*") + .Which.StreamPosition.Should().Be(errorIndex); } [Theory] @@ -80,7 +84,9 @@ public void MissingDelimiter_ThrowsInvalidBencodeException(string bencode) public void NonDigitFirstChar_ThrowsInvalidBencodeException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage($"*Unexpected character. Expected ':' but found '{bencode[0]}'*") + .Which.StreamPosition.Should().Be(0); } [Theory] @@ -89,7 +95,9 @@ public void NonDigitFirstChar_ThrowsInvalidBencodeException(string bencode) public void LessThanMinimumLength2_ThrowsInvalidBencodeException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*Invalid length*") + .Which.StreamPosition.Should().Be(0); } [Theory] @@ -100,7 +108,9 @@ public void LessThanMinimumLength2_ThrowsInvalidBencodeException(string bencode) public void LengthAboveMaxDigits10_ThrowsUnsupportedException(string bencode) { Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*Length of string is more than * digits*") + .Which.StreamPosition.Should().Be(0); } [Theory] @@ -125,7 +135,9 @@ public void LengthAboveInt32MaxValue_ThrowsUnsupportedException() { var bencode = "2147483648:spam"; Action action = () => Parser.ParseString(bencode); - action.Should().Throw>(); + action.Should().Throw>() + .WithMessage("*Length of string is * but maximum supported length is *") + .Which.StreamPosition.Should().Be(0); } [Fact] @@ -149,5 +161,30 @@ public void CanParseEncodedAsLatin1() bstring.Should().Be(expected); } + + [Theory] + [InlineData("1-:a", 1)] + [InlineData("1abc:a", 1)] + [InlineData("123?:asdf", 3)] + [InlineData("3abc:abc", 1)] + public void InvalidLengthString_ThrowsInvalidException(string bencode, int errorIndex) + { + Action action = () => Parser.ParseString(bencode); + action.Should().Throw>() + .WithMessage("*Unexpected character. Expected ':'*") + .Which.StreamPosition.Should().Be(errorIndex); + } + + [Theory] + [InlineData("")] + [InlineData("0")] + public void BelowMinimumLength_WhenStreamWithoutLengthSupport_ThrowsInvalidException(string bencode) + { + var stream = new LengthNotSupportedStream(bencode); + Action action = () => Parser.Parse(stream); + action.Should().Throw>() + .WithMessage("*Unexpected character. Expected ':' but reached end of stream*") + .Which.StreamPosition.Should().Be(0); + } } } diff --git a/BencodeNET.Tests/Parsing/BencodeParserTests.cs b/BencodeNET.Tests/Parsing/BencodeParserTests.cs index dc94129bc9..816811e255 100644 --- a/BencodeNET.Tests/Parsing/BencodeParserTests.cs +++ b/BencodeNET.Tests/Parsing/BencodeParserTests.cs @@ -1,71 +1,15 @@ using System; using BencodeNET.Exceptions; -using BencodeNET.IO; using BencodeNET.Objects; using BencodeNET.Parsing; using FluentAssertions; -using NSubstitute; using Xunit; namespace BencodeNET.Tests.Parsing { // TODO: "Integration" tests? Full decode tests - // TODO: stream/bencodestream methods public class BencodeParserTests { - [Theory] - [InlineAutoMockedData("0")] - [InlineAutoMockedData("1")] - [InlineAutoMockedData("2")] - [InlineAutoMockedData("3")] - [InlineAutoMockedData("4")] - [InlineAutoMockedData("5")] - [InlineAutoMockedData("6")] - [InlineAutoMockedData("7")] - [InlineAutoMockedData("8")] - [InlineAutoMockedData("9")] - public void FirstCharDigit_CallsStringParser(string bencode, IBObjectParser stringParser) - { - var bparser = new BencodeParser(); - bparser.Parsers.AddOrReplace(stringParser); - bparser.ParseString(bencode); - - stringParser.Received(1).Parse(Arg.Any()); - } - - [Theory] - [InlineAutoMockedData("i")] - public void FirstChar_I_CallsNumberParser(string bencode, IBObjectParser numberParser) - { - var bparser = new BencodeParser(); - bparser.Parsers.AddOrReplace(numberParser); - bparser.ParseString(bencode); - - numberParser.Received(1).Parse(Arg.Any()); - } - - [Theory] - [InlineAutoMockedData("l")] - public void FirstChar_L_CallsNumberParser(string bencode, IBObjectParser listParser) - { - var bparser = new BencodeParser(); - bparser.Parsers.AddOrReplace(listParser); - bparser.ParseString(bencode); - - listParser.Received(1).Parse(Arg.Any()); - } - - [Theory] - [InlineAutoMockedData("d")] - public void FirstChar_D_CallsNumberParser(string bencode, IBObjectParser dictionaryParser) - { - var bparser = new BencodeParser(); - bparser.Parsers.AddOrReplace(dictionaryParser); - bparser.ParseString(bencode); - - dictionaryParser.Received(1).Parse(Arg.Any()); - } - [Theory] #region Alphabet... [InlineAutoMockedData("a")] @@ -126,6 +70,14 @@ public void InvalidFirstChars_ThrowsInvalidBencodeException(string bencode) action.Should().Throw>(); } + [Fact] + public void EmptyString_ReturnsNull() + { + var bparser = new BencodeParser(); + var result = bparser.ParseString(""); + result.Should().BeNull(); + } + [Fact] public void CanParse_ListOfStrings() { diff --git a/BencodeNET.Tests/Parsing/ParseUtilTests.cs b/BencodeNET.Tests/Parsing/ParseUtilTests.cs index a80e128d58..2230ee4780 100644 --- a/BencodeNET.Tests/Parsing/ParseUtilTests.cs +++ b/BencodeNET.Tests/Parsing/ParseUtilTests.cs @@ -9,17 +9,14 @@ public class ParseUtilTests [Fact] public void TryParseLongFast_CanParseSimple() { - long value; - ParseUtil.TryParseLongFast("123", out value); - + ParseUtil.TryParseLongFast("123", out var value); value.Should().Be(123); } [Fact] public void TryParseLongFast_NullReturnsFalse() { - long value; - var result = ParseUtil.TryParseLongFast(null, out value); + var result = ParseUtil.TryParseLongFast((string) null, out _); result.Should().BeFalse(); } @@ -28,8 +25,7 @@ public void TryParseLongFast_NullReturnsFalse() [InlineAutoMockedData("-")] public void TryParseLongFast_ZeroLengthInputReturnsFalse(string input) { - long value; - var result = ParseUtil.TryParseLongFast(input, out value); + var result = ParseUtil.TryParseLongFast(input, out _); result.Should().BeFalse(); } @@ -38,24 +34,21 @@ public void TryParseLongFast_ZeroLengthInputReturnsFalse(string input) [InlineAutoMockedData("-12345678901234567890")] public void TryParseLongFast_InputLongerThanInt64MaxValueReturnsFalse(string input) { - long value; - var result = ParseUtil.TryParseLongFast(input, out value); + var result = ParseUtil.TryParseLongFast(input, out _); result.Should().BeFalse(); } [Fact] public void TryParseLongFast_InputBiggerThanInt64MaxValueReturnsFalse() { - long value; - var result = ParseUtil.TryParseLongFast("9223372036854775808", out value); + var result = ParseUtil.TryParseLongFast("9223372036854775808", out _); result.Should().BeFalse(); } [Fact] public void TryParseLongFast_InputSmallerThanInt64MinValueReturnsFalse() { - long value; - var result = ParseUtil.TryParseLongFast("-9223372036854775809", out value); + var result = ParseUtil.TryParseLongFast("-9223372036854775809", out _); result.Should().BeFalse(); } @@ -74,8 +67,7 @@ public void TryParseLongFast_InputSmallerThanInt64MinValueReturnsFalse() [InlineAutoMockedData("a")] public void TryParseLongFast_InputContainingNonDigitReturnsFalse(string input) { - long value; - var result = ParseUtil.TryParseLongFast(input, out value); + var result = ParseUtil.TryParseLongFast(input, out _); result.Should().BeFalse(); } @@ -86,10 +78,9 @@ public void TryParseLongFast_InputContainingNonDigitReturnsFalse(string input) [InlineAutoMockedData("-1", -1)] [InlineAutoMockedData("9223372036854775807", 9223372036854775807)] [InlineAutoMockedData("-9223372036854775808", -9223372036854775808)] - public void TryParseLongFast_ValidInputRetursTrueAndCorrectValue(string input, long expected) + public void TryParseLongFast_ValidInputReturnsTrueAndCorrectValue(string input, long expected) { - long value; - var result = ParseUtil.TryParseLongFast(input, out value); + var result = ParseUtil.TryParseLongFast(input, out var value); result.Should().BeTrue(); value.Should().Be(expected); diff --git a/BencodeNET.Tests/Parsing/TorrentParserTests.cs b/BencodeNET.Tests/Torrents/TorrentParserTests.cs similarity index 82% rename from BencodeNET.Tests/Parsing/TorrentParserTests.cs rename to BencodeNET.Tests/Torrents/TorrentParserTests.cs index e1b5f3ff6b..5ff2689caf 100644 --- a/BencodeNET.Tests/Parsing/TorrentParserTests.cs +++ b/BencodeNET.Tests/Torrents/TorrentParserTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text; -using BencodeNET.Exceptions; using BencodeNET.IO; using BencodeNET.Objects; using BencodeNET.Parsing; @@ -10,7 +9,7 @@ using NSubstitute; using Xunit; -namespace BencodeNET.Tests.Parsing +namespace BencodeNET.Tests.Torrents { public class TorrentParserTests { @@ -25,7 +24,7 @@ public class TorrentParserTests public TorrentParserTests() { BencodeParser = Substitute.For(); - BencodeParser.Parse((BencodeStream) null).ReturnsForAnyArgs(x => ParsedData); + BencodeParser.Parse((BencodeReader) null).ReturnsForAnyArgs(x => ParsedData); ValidSingleFileTorrentData = new BDictionary { @@ -60,7 +59,7 @@ public void Comment_IsParsed(string comment) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.Comment.Should().Be(comment); @@ -76,7 +75,7 @@ public void CreatedBy_IsParsed(string createdBy) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.CreatedBy.Should().Be(createdBy); @@ -91,7 +90,7 @@ public void CreationDate_IsParsed() // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.CreationDate.Should().Be(new DateTime(2016, 1, 1)); @@ -106,7 +105,7 @@ public void CreationDate_UnixTimeInMilliseconds_IsParsed() // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.CreationDate.Should().Be(new DateTime(2016, 1, 1)); @@ -121,7 +120,7 @@ public void CreationDate_InvalidValue_ReturnsEpoch() // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.CreationDate.Should().Be(new DateTime(1970, 1, 1)); @@ -140,7 +139,7 @@ public void Encoding_UTF8_CanBeParsed(string encoding) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.Encoding.Should().Be(Encoding.UTF8); @@ -157,7 +156,7 @@ public void Encoding_ASCII_CanBeParsed(string encoding) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.Encoding.Should().Be(Encoding.ASCII); @@ -176,7 +175,7 @@ public void Encoding_InvalidValidAsNull(string encoding) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.Encoding.Should().Be(null); @@ -193,7 +192,7 @@ public void Info_PieceLength_IsParsed(long pieceSize) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert torrent.PieceSize.Should().Be(pieceSize); @@ -210,7 +209,7 @@ public void Info_Pieces_IsParsed(byte[] pieces) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert torrent.Pieces.Should().Equal(pieces); @@ -231,7 +230,7 @@ public void Info_Private_ShouldBeTrueOnlyIfValueIsOne(int value, bool expectedRe // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert torrent.IsPrivate.Should().Be(expectedResult); @@ -248,7 +247,7 @@ public void ExtraFields_IsParsed(string extraKey, string extraValue, string extr // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert torrent.ExtraFields.Should().Contain(extraKey, (BString) extraValue); @@ -265,7 +264,7 @@ public void Announce_IsParsed(string announceUrl) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert torrent.Trackers.Should().HaveCount(1); @@ -286,7 +285,7 @@ public void AnnounceList_Single_IsParsed(IList announceList) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert torrent.Trackers.Should().HaveCount(1); @@ -308,7 +307,7 @@ public void AnnounceList_Multiple_IsParsed(IList announceList1, IList announceList1, IList announceList1, IList announceList2) @@ -333,7 +357,7 @@ public void AnnounceAndAnnounceList_IsParsed(string announceUrl, IList a // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert @@ -360,7 +384,7 @@ public void AnnounceAndAnnounceList_DoesNotContainDuplicatesInPrimaryList(string // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert torrent.Trackers.Should().HaveCount(1); @@ -381,7 +405,7 @@ public void SingleFileInfo_IsParsed(long length, string fileName, string md5Sum) // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream)null); + var torrent = parser.Parse((BencodeReader)null); // Assert torrent.Files.Should().BeNull(); @@ -419,7 +443,7 @@ public void MultiFileInfo_IsParsed(string directoryName, long length1, IList parser.Parse((BencodeStream)null); + var parser = new TorrentParser(BencodeParser, TorrentParserMode.Strict); + Action action = () => parser.Parse((BencodeReader)null); // Assert action.Should().Throw() @@ -452,7 +476,7 @@ public void Root_MissingInfoField_ThrowsInvalidTorrentException() } [Fact] - public void Info_ContainingBothLengthAndFilesField_ThrowsInvalidTorrentException() + public void Info_ContainingBothLengthAndFilesField_Strict_ThrowsInvalidTorrentException() { // Arrange ParsedData = ValidSingleFileTorrentData; @@ -461,8 +485,8 @@ public void Info_ContainingBothLengthAndFilesField_ThrowsInvalidTorrentException info[TorrentInfoFields.Files] = new BList(); // Act - var parser = new TorrentParser(BencodeParser); - Action action = () => parser.Parse((BencodeStream)null); + var parser = new TorrentParser(BencodeParser, TorrentParserMode.Strict); + Action action = () => parser.Parse((BencodeReader)null); // Assert action.Should().Throw() @@ -471,7 +495,7 @@ public void Info_ContainingBothLengthAndFilesField_ThrowsInvalidTorrentException } [Fact] - public void Info_MissingPieceLengthField_ThrowsInvalidTorrentException() + public void Info_MissingPieceLengthField_Strict_ThrowsInvalidTorrentException() { // Arrange ParsedData = ValidSingleFileTorrentData; @@ -479,8 +503,8 @@ public void Info_MissingPieceLengthField_ThrowsInvalidTorrentException() info.Remove(TorrentInfoFields.PieceLength); // Act - var parser = new TorrentParser(BencodeParser); - Action action = () => parser.Parse((BencodeStream)null); + var parser = new TorrentParser(BencodeParser, TorrentParserMode.Strict); + Action action = () => parser.Parse((BencodeReader)null); // Assert action.Should().Throw() @@ -488,7 +512,7 @@ public void Info_MissingPieceLengthField_ThrowsInvalidTorrentException() } [Fact] - public void Info_MissingPiecesField_ThrowsInvalidTorrentException() + public void Info_MissingPiecesField_Strict_ThrowsInvalidTorrentException() { // Arrange ParsedData = ValidSingleFileTorrentData; @@ -496,8 +520,8 @@ public void Info_MissingPiecesField_ThrowsInvalidTorrentException() info.Remove(TorrentInfoFields.Pieces); // Act - var parser = new TorrentParser(BencodeParser); - Action action = () => parser.Parse((BencodeStream)null); + var parser = new TorrentParser(BencodeParser, TorrentParserMode.Strict); + Action action = () => parser.Parse((BencodeReader)null); // Assert action.Should().Throw() @@ -505,7 +529,7 @@ public void Info_MissingPiecesField_ThrowsInvalidTorrentException() } [Fact] - public void Info_MissingNameField_ThrowsInvalidTorrentException() + public void Info_MissingNameField_Strict_ThrowsInvalidTorrentException() { // Arrange ParsedData = ValidSingleFileTorrentData; @@ -513,8 +537,8 @@ public void Info_MissingNameField_ThrowsInvalidTorrentException() info.Remove(TorrentInfoFields.Name); // Act - var parser = new TorrentParser(BencodeParser); - Action action = () => parser.Parse((BencodeStream)null); + var parser = new TorrentParser(BencodeParser, TorrentParserMode.Strict); + Action action = () => parser.Parse((BencodeReader)null); // Assert action.Should().Throw() @@ -522,7 +546,7 @@ public void Info_MissingNameField_ThrowsInvalidTorrentException() } [Fact] - public void MultiFileInfo_MissingFilesField_ThrowsInvalidTorrentException() + public void MultiFileInfo_MissingFilesField_Strict_ThrowsInvalidTorrentException() { // Arrange ParsedData = ValidMultiFileTorrentData; @@ -530,8 +554,8 @@ public void MultiFileInfo_MissingFilesField_ThrowsInvalidTorrentException() info.Remove(TorrentInfoFields.Files); // Act - var parser = new TorrentParser(BencodeParser); - Action action = () => parser.Parse((BencodeStream)null); + var parser = new TorrentParser(BencodeParser, TorrentParserMode.Strict); + Action action = () => parser.Parse((BencodeReader)null); // Assert action.Should().Throw() @@ -539,7 +563,7 @@ public void MultiFileInfo_MissingFilesField_ThrowsInvalidTorrentException() } [Fact] - public void MultiFile_Files_MissingLengthField_ThrowsInvalidTorrentException() + public void MultiFile_Files_MissingLengthField_Strict_ThrowsInvalidTorrentException() { // Arrange ParsedData = ValidMultiFileTorrentData; @@ -553,8 +577,8 @@ public void MultiFile_Files_MissingLengthField_ThrowsInvalidTorrentException() }; // Act - var parser = new TorrentParser(BencodeParser); - Action action = () => parser.Parse((BencodeStream)null); + var parser = new TorrentParser(BencodeParser, TorrentParserMode.Strict); + Action action = () => parser.Parse((BencodeReader)null); // Assert action.Should().Throw() @@ -562,7 +586,7 @@ public void MultiFile_Files_MissingLengthField_ThrowsInvalidTorrentException() } [Fact] - public void MultiFile_Files_MissingPathField_ThrowsInvalidTorrentException() + public void MultiFile_Files_MissingPathField_Strict_ThrowsInvalidTorrentException() { // Arrange ParsedData = ValidMultiFileTorrentData; @@ -576,8 +600,8 @@ public void MultiFile_Files_MissingPathField_ThrowsInvalidTorrentException() }; // Act - var parser = new TorrentParser(BencodeParser); - Action action = () => parser.Parse((BencodeStream)null); + var parser = new TorrentParser(BencodeParser, TorrentParserMode.Strict); + Action action = () => parser.Parse((BencodeReader)null); // Assert action.Should().Throw() @@ -593,7 +617,7 @@ public void OriginalInfoHash_IsSet() // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.OriginalInfoHash.Should().Be(expectedInfoHash); @@ -608,7 +632,7 @@ public void OriginalInfoHashBytes_IsSet() // Act var parser = new TorrentParser(BencodeParser); - var torrent = parser.Parse((BencodeStream) null); + var torrent = parser.Parse((BencodeReader) null); // Assert torrent.OriginalInfoHashBytes.Should().Equal(expectedInfoHashBytes); diff --git a/BencodeNET.Tests/Torrents/TorrentTests.cs b/BencodeNET.Tests/Torrents/TorrentTests.cs index c37ac9dc7a..a94ad23aa6 100644 --- a/BencodeNET.Tests/Torrents/TorrentTests.cs +++ b/BencodeNET.Tests/Torrents/TorrentTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; using BencodeNET.Exceptions; using BencodeNET.Objects; diff --git a/BencodeNET.sln b/BencodeNET.sln index 5825eeb7c3..530fbfd72b 100644 --- a/BencodeNET.sln +++ b/BencodeNET.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29403.142 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{65E984B8-653C-4BCD-AE37-4E21497B5B87}" ProjectSection(SolutionItems) = preProject @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore build.cake = build.cake build.ps1 = build.ps1 + CHANGELOG.md = CHANGELOG.md GitVersion.yml = GitVersion.yml LICENSE.md = LICENSE.md README.md = README.md diff --git a/BencodeNET/BencodeNET.csproj b/BencodeNET/BencodeNET.csproj index 0797d3c16e..1d9c1581f7 100644 --- a/BencodeNET/BencodeNET.csproj +++ b/BencodeNET/BencodeNET.csproj @@ -1,8 +1,11 @@ - + - net45;netstandard1.3;netstandard2.0 + netstandard2.0;netcoreapp2.1;netcoreapp3.0 + 7.3 + true True + BencodeNET.ruleset @@ -19,7 +22,7 @@ A library for encoding and decoding bencode (e.g. torrent files) Unlicense https://github.com/Krusen/BencodeNET - https://raw.githubusercontent.com/Krusen/BencodeNET/master/Assets/icon.png + icon.png https://github.com/Krusen/BencodeNET git @@ -29,10 +32,23 @@ True - - - - + + + + + + + + + + + + + + + + + diff --git a/BencodeNET/BencodeNET.ruleset b/BencodeNET/BencodeNET.ruleset new file mode 100644 index 0000000000..ed2f2d5ef3 --- /dev/null +++ b/BencodeNET/BencodeNET.ruleset @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/BencodeNET/Exceptions/BencodeException.cs b/BencodeNET/Exceptions/BencodeException.cs index a1887bab69..d4a7912402 100644 --- a/BencodeNET/Exceptions/BencodeException.cs +++ b/BencodeNET/Exceptions/BencodeException.cs @@ -1,7 +1,4 @@ using System; -#if !NETSTANDARD -using System.Runtime.Serialization; -#endif #pragma warning disable 1591 namespace BencodeNET.Exceptions @@ -9,9 +6,6 @@ namespace BencodeNET.Exceptions /// /// Represents generic errors in this bencode library. /// -#if !NETSTANDARD - [Serializable] -#endif public class BencodeException : Exception { public BencodeException() @@ -19,27 +13,17 @@ public BencodeException() public BencodeException(string message) : base(message) - { } public BencodeException(string message, Exception inner) : base(message, inner) { } - -#if !NETSTANDARD - protected BencodeException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } -#endif } /// /// Represents generic errors in this bencode library related to a specific . /// /// The related type. -#if !NETSTANDARD - [Serializable] -#endif public class BencodeException : BencodeException { /// @@ -58,27 +42,5 @@ public BencodeException(string message) public BencodeException(string message, Exception inner) : base(message, inner) { } - -#if !NETSTANDARD - protected BencodeException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - if (info == null) return; - RelatedType = Type.GetType(info.GetString(nameof(RelatedType)), false); - } - - /// - /// Sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// The parameter is a null reference. - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - - info.AddValue(nameof(RelatedType), RelatedType.AssemblyQualifiedName); - } -#endif } } \ No newline at end of file diff --git a/BencodeNET/Exceptions/InvalidBencodeException.cs b/BencodeNET/Exceptions/InvalidBencodeException.cs index 7f97d3311a..4eb7d105b1 100644 --- a/BencodeNET/Exceptions/InvalidBencodeException.cs +++ b/BencodeNET/Exceptions/InvalidBencodeException.cs @@ -1,7 +1,4 @@ using System; -#if !NETSTANDARD -using System.Runtime.Serialization; -#endif #pragma warning disable 1591 namespace BencodeNET.Exceptions @@ -10,9 +7,6 @@ namespace BencodeNET.Exceptions /// Represents parse errors when encountering invalid bencode of some sort. /// /// The type being parsed. -#if !NETSTANDARD - [Serializable] -#endif public class InvalidBencodeException : BencodeException { /// @@ -35,31 +29,15 @@ public InvalidBencodeException(string message, Exception inner) public InvalidBencodeException(string message, Exception inner, long streamPosition) : base($"Failed to parse {typeof(T).Name}. {message}", inner) { - StreamPosition = streamPosition; + StreamPosition = Math.Max(0, streamPosition); } public InvalidBencodeException(string message, long streamPosition) : base($"Failed to parse {typeof(T).Name}. {message}") { - StreamPosition = streamPosition; + StreamPosition = Math.Max(0, streamPosition); } -#if !NETSTANDARD - protected InvalidBencodeException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - if (info == null) return; - StreamPosition = info.GetInt64(nameof(StreamPosition)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - - info.AddValue(nameof(StreamPosition), StreamPosition); - } -#endif - internal static InvalidBencodeException InvalidBeginningChar(char invalidChar, long streamPosition) { var message = @@ -67,19 +45,12 @@ internal static InvalidBencodeException InvalidBeginningChar(char invalidChar return new InvalidBencodeException(message, streamPosition); } - internal static InvalidBencodeException InvalidEndChar(char invalidChar, long streamPosition) + internal static InvalidBencodeException MissingEndChar(long streamPosition) { - var message = - $"Invalid end character of object. Expected 'e' but found '{invalidChar}' at position {streamPosition}."; + var message = "Missing end character of object. Expected 'e' but reached end of stream."; return new InvalidBencodeException(message, streamPosition); } - internal static InvalidBencodeException MissingEndChar() - { - var message = "Missing end character of object. Expected 'e' but reached the end of the stream."; - return new InvalidBencodeException(message); - } - internal static InvalidBencodeException BelowMinimumLength(int minimumLength, long actualLength, long streamPosition) { var message = @@ -89,7 +60,9 @@ internal static InvalidBencodeException BelowMinimumLength(int minimumLength, internal static InvalidBencodeException UnexpectedChar(char expected, char unexpected, long streamPosition) { - var message = $"Unexpected character. Expected '{expected}' but found '{unexpected}' at position {streamPosition}."; + var message = unexpected == default + ? $"Unexpected character. Expected '{expected}' but reached end of stream." + : $"Unexpected character. Expected '{expected}' but found '{unexpected}' at position {streamPosition}."; return new InvalidBencodeException(message, streamPosition); } } diff --git a/BencodeNET/Exceptions/UnsupportedBencodeException.cs b/BencodeNET/Exceptions/UnsupportedBencodeException.cs index c8e969ce7f..c5647d1bd1 100644 --- a/BencodeNET/Exceptions/UnsupportedBencodeException.cs +++ b/BencodeNET/Exceptions/UnsupportedBencodeException.cs @@ -1,7 +1,4 @@ using System; -#if !NETSTANDARD -using System.Runtime.Serialization; -#endif #pragma warning disable 1591 namespace BencodeNET.Exceptions @@ -11,9 +8,6 @@ namespace BencodeNET.Exceptions /// Usually numbers larger than or strings longer than that. /// /// -#if !NETSTANDARD - [Serializable] -#endif public class UnsupportedBencodeException : BencodeException { public long StreamPosition { get; set; } @@ -34,29 +28,5 @@ public UnsupportedBencodeException(string message, long streamPosition) { StreamPosition = streamPosition; } - -#if !NETSTANDARD - protected UnsupportedBencodeException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - if (info == null) - return; - - StreamPosition = info.GetInt64(nameof(StreamPosition)); - } - - /// - /// Sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// The parameter is a null reference. - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - - info.AddValue(nameof(StreamPosition), StreamPosition); - } -#endif } } diff --git a/BencodeNET/IO/BencodeReader.cs b/BencodeNET/IO/BencodeReader.cs new file mode 100644 index 0000000000..a4c4a20a9b --- /dev/null +++ b/BencodeNET/IO/BencodeReader.cs @@ -0,0 +1,168 @@ +using System; +using System.IO; + +namespace BencodeNET.IO +{ + /// + /// Reads bencode from a stream. + /// + public class BencodeReader : IDisposable + { + private readonly byte[] _tinyBuffer = new byte[1]; + + private readonly Stream _stream; + private readonly bool _leaveOpen; + private readonly bool _supportsLength; + + private bool _hasPeeked; + private char _peekedChar; + + /// + /// The previously read/consumed char (does not include peeked char). + /// + public char PreviousChar { get; private set; } + + /// + /// The position in the stream (does not included peeked char). + /// + public long Position { get; set; } + + /// + /// The length of the stream, or null if the stream doesn't support the feature. + /// + public long? Length => _supportsLength ? _stream.Length : (long?) null; + + /// + /// Returns true if the end of the stream has been reached. + /// This is true if either is greater than or if next char is default(char). + /// + public bool EndOfStream => Position > Length || PeekChar() == default; + + /// + /// Creates a new for the specified . + /// + /// The stream to read from. + public BencodeReader(Stream stream) + : this(stream, leaveOpen: false) + { + } + + /// + /// Creates a new for the specified + /// using the default buffer size of 40,960 bytes and the option of leaving the stream open after disposing of this instance. + /// + /// The stream to read from. + /// Indicates if the stream should be left open when this is disposed. + public BencodeReader(Stream stream, bool leaveOpen) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _leaveOpen = leaveOpen; + try + { + _ = stream.Length; + _supportsLength = true; + } + catch + { + _supportsLength = false; + } + + if (!_stream.CanRead) throw new ArgumentException("The stream is not readable.", nameof(stream)); + } + + /// + /// Peeks at the next character in the stream, or default(char) if the end of the stream has been reached. + /// + public char PeekChar() + { + if (_hasPeeked) + return _peekedChar; + + var read = _stream.Read(_tinyBuffer, 0, 1); + + _peekedChar = read == 0 ? default : (char)_tinyBuffer[0]; + _hasPeeked = true; + + return _peekedChar; + } + + /// + /// Reads the next character from the stream. + /// Returns default(char) if the end of the stream has been reached. + /// + public char ReadChar() + { + if (_hasPeeked) + { + _hasPeeked = _peekedChar == default; // If null then EOS so don't reset peek as peeking again will just be EOS again + if (_peekedChar != default) + Position++; + return _peekedChar; + } + + var read = _stream.Read(_tinyBuffer, 0, 1); + + PreviousChar = read == 0 + ? default + : (char) _tinyBuffer[0]; + + if (read > 0) + Position++; + + return PreviousChar; + } + + /// + /// Reads into the by reading from the stream. + /// Returns the number of bytes actually read from the stream. + /// + /// The buffer to read into. + /// The number of bytes actually read from the stream and filled into the buffer. + public int Read(byte[] buffer) + { + var totalRead = 0; + if (_hasPeeked && _peekedChar != default) + { + buffer[0] = (byte) _peekedChar; + totalRead = 1; + _hasPeeked = false; + + // Just return right away if only reading this 1 byte + if (buffer.Length == 1) + { + Position++; + return 1; + } + } + + int read = -1; + while (read != 0 && totalRead < buffer.Length) + { + read = _stream.Read(buffer, totalRead, buffer.Length - totalRead); + totalRead += read; + } + + if (totalRead > 0) + PreviousChar = (char) buffer[totalRead - 1]; + + Position += totalRead; + + return totalRead; + } + + /// + public void Dispose() + { + Dispose(true); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (!disposing) return; + + if (_stream != null && !_leaveOpen) + _stream.Dispose(); + } + } +} diff --git a/BencodeNET/IO/BencodeStream.cs b/BencodeNET/IO/BencodeStream.cs deleted file mode 100644 index 41fba4e706..0000000000 --- a/BencodeNET/IO/BencodeStream.cs +++ /dev/null @@ -1,324 +0,0 @@ -using System; -using System.IO; -using System.Text; - -namespace BencodeNET.IO -{ - /// - /// A wrapper for that makes it easier and faster to work - /// with bencode and to read/write one byte at a time. Also has methods for peeking - /// at the next byte (caching the read) and for reading the previous byte in stream. - /// - public class BencodeStream : IDisposable - { - private static readonly byte[] EmptyByteArray = new byte[0]; - - private bool _hasPeeked; - private int _peekedByte; - - /// - /// Creates a new by converting the string - /// to bytes using and storing them in a . - /// - /// - public BencodeStream(string str) : this(str, Encoding.UTF8) - { } - - /// - /// Creates a new by converting the string - /// to bytes using the specified encoding and storing them in a . - /// - /// - /// - public BencodeStream(string str, Encoding encoding) : this(encoding.GetBytes(str)) - { } - - /// - /// Creates a new from the specified bytes - /// using a as the . - /// - /// - public BencodeStream(byte[] bytes) : this(new MemoryStream(bytes), false) - { } - - /// - /// Creates a new using the specified stream. - /// - /// The underlying stream to use. - /// Indicates if the specified stream should be left open when this is disposed. - public BencodeStream(Stream stream, bool leaveOpen = false) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - if (!stream.CanSeek) throw new ArgumentException("Only seekable streams are supported.", nameof(stream)); - - InnerStream = stream; - LeaveOpen = leaveOpen; - } - - /// - /// The inner stream that this is working with. - /// - public Stream InnerStream { get; protected set; } - - /// - /// If true will not be disposed when this is disposed. - /// - public bool LeaveOpen { get; } - - /// - /// Gets the lenght in bytes of the stream. - /// - public long Length => InnerStream.Length; - - /// - /// Gets or sets the position within the stream. - /// - public long Position - { - get => InnerStream.Position; - set - { - _hasPeeked = false; - InnerStream.Position = value; - } - } - - /// - /// Indicates if the current position is at or after the end of the stream. - /// - public bool EndOfStream => Position >= Length; - - /// - /// Sets the position within the stream. - /// - /// A byte offset relative to the parameter. - /// A value indicating the reference point used to obtain the new position. - /// - public long Seek(long offset, SeekOrigin origin) - { - _hasPeeked = false; - return InnerStream.Seek(offset, origin); - } - - /// - /// Reads the next byte in the stream without advancing the position. - /// This can safely be called multiple times as the read byte is cached until the position - /// in the stream is changed or a read operation is performed. - /// - /// The next byte in the stream. - public int Peek() - { - if (_hasPeeked) - return _peekedByte; - - var position = InnerStream.Position; - _peekedByte = InnerStream.ReadByte(); - _hasPeeked = true; - InnerStream.Position = position; - - return _peekedByte; - } - - /// - /// Reads the next char in the stream without advancing the position. - /// This can safely be called multiple times as the read char is cached until the position - /// in the stream is changed or a read operation is performed. - /// - /// The next char in the stream. - public char PeekChar() - { - if (Peek() == -1) - return default(char); - return (char) Peek(); - } - - /// - /// Reads the next byte in the stream. - /// If a or a has been performed - /// the peeked value is returned and the position is incremented by 1. - /// - /// The next býte in the stream. - public int Read() - { - if (!_hasPeeked) - return InnerStream.ReadByte(); - - if (_peekedByte == -1) - return _peekedByte; - - _hasPeeked = false; - InnerStream.Position += 1; - return _peekedByte; - } - - /// - /// Reads the next char in the stream. - /// If a or a has been performed - /// the peeked value is returned and the position is incremented by 1. - /// - /// The next char in the stream. - public char ReadChar() - { - var value = Read(); - if (value == -1) - return default(char); - return (char) value; - } - - /// - /// Reads the specified amount of bytes from the stream. - /// - /// The number of bytes to read. - /// The read bytes. - public byte[] Read(int bytesToRead) - { - if (bytesToRead < 0) throw new ArgumentOutOfRangeException(nameof(bytesToRead)); - if (bytesToRead == 0) return EmptyByteArray; - - var bytes = new byte[bytesToRead]; - - var offset = 0; - - if (_hasPeeked) - { - if (_peekedByte == -1) - return EmptyByteArray; - - bytes[0] = (byte)_peekedByte; - offset = 1; - } - - _hasPeeked = false; - - if (offset > 0) - InnerStream.Position += offset; - - var readBytes = InnerStream.Read(bytes, offset, bytesToRead-offset) + offset; - if (readBytes != bytesToRead) - Array.Resize(ref bytes, readBytes); - - return bytes; - } - - /// - /// Reads the previous byte in the stream and decrements the position by 1. - /// - /// The previous byte in stream. - public int ReadPrevious() - { - if (InnerStream.Position == 0) - return -1; - - _hasPeeked = false; - - InnerStream.Position -= 1; - - var bytes = new byte[1]; - - var readBytes = InnerStream.Read(bytes, 0, 1); - if (readBytes == 0) - return -1; - - return bytes[0]; - } - - /// - /// Reads the previous char in the stream and decrements the position by 1. - /// - /// The previous char in the stream. - public char ReadPreviousChar() - { - var value = ReadPrevious(); - if (value == -1) - return default(char); - return (char)value; - } - - /// - /// Writes a number to the stream. - /// - /// The number to write to the stream. - public void Write(int number) - { - var bytes = Encoding.ASCII.GetBytes(number.ToString()); - Write(bytes); - } - - /// - /// Writes the number to the stream. - /// - /// The number to write to the stream. - public void Write(long number) - { - var bytes = Encoding.ASCII.GetBytes(number.ToString()); - Write(bytes); - } - - /// - /// Writes a char to the stream. - /// - /// The char to write to the stream. - public void Write(char c) - { - InnerStream.Write(c); - } - - /// - /// Writes an array of bytes to the stream. - /// - /// The bytes to write to the stream. - public void Write(byte[] bytes) - { - Write(bytes, 0, bytes.Length); - } - - /// - /// Writes a sequence of bytes to the stream and advances the position by the number of bytes written. - /// - /// An array of bytes to copy from. - /// The zero-based offset in to start copying from to the stream. - /// The number of bytes to be written to the stream - public void Write(byte[] buffer, int offset, int count) - { - InnerStream.Write(buffer, offset, count); - } - - /// - /// Clears all buffers for this stream and causes any buffered data to be written. - /// - public void Flush() - { - InnerStream.Flush(); - } - - /// - /// Releases all resources used by the . - /// is also disposed unless is true. - /// - public void Dispose() - { - Dispose(true); - } - - /// - /// Disposes of unless is true. - /// - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (InnerStream != null && !LeaveOpen) - InnerStream.Dispose(); - InnerStream = null; - } - } - -#pragma warning disable 1591 - public static implicit operator BencodeStream(Stream stream) - { - return new BencodeStream(stream); - } -#pragma warning restore 1591 - } -} diff --git a/BencodeNET/IO/PipeBencodeReader.cs b/BencodeNET/IO/PipeBencodeReader.cs new file mode 100644 index 0000000000..28d1f89b2a --- /dev/null +++ b/BencodeNET/IO/PipeBencodeReader.cs @@ -0,0 +1,190 @@ +using System; +using System.Buffers; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +namespace BencodeNET.IO +{ + /// + /// Reads chars and bytes from a . + /// + public class PipeBencodeReader + { + /// + /// The to read from. + /// + protected PipeReader Reader { get; } + + /// + /// Indicates if the has been completed (i.e. "end of stream"). + /// + protected bool ReaderCompleted { get; set; } + + /// + /// The position in the pipe (number of read bytes/characters) (does not included peeked char). + /// + public virtual long Position { get; protected set; } + + /// + /// The previously read/consumed char (does not include peeked char). + /// + public virtual char PreviousChar { get; protected set; } + + /// + /// Creates a that reads from the specified . + /// + /// + public PipeBencodeReader(PipeReader reader) + { + Reader = reader; + } + + /// + /// Peek at the next char in the pipe, without advancing the reader. + /// + public virtual ValueTask PeekCharAsync(CancellationToken cancellationToken = default) + => ReadCharAsync(peek: true, cancellationToken); + + /// + /// Read the next char in the pipe and advance the reader. + /// + public virtual ValueTask ReadCharAsync(CancellationToken cancellationToken = default) + => ReadCharAsync(peek: false, cancellationToken); + + private ValueTask ReadCharAsync(bool peek = false, CancellationToken cancellationToken = default) + { + if (ReaderCompleted) + return new ValueTask(default(char)); + + if (Reader.TryRead(out var result)) + return new ValueTask(ReadCharConsume(result.Buffer, peek)); + + return ReadCharAwaitedAsync(peek, cancellationToken); + } + + private async ValueTask ReadCharAwaitedAsync(bool peek, CancellationToken cancellationToken) + { + var result = await Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + return ReadCharConsume(result.Buffer, peek); + } + + /// + /// Reads the next char in the pipe and consumes it (advances the reader), + /// unless is true. + /// + /// The buffer to read from + /// If true the char will not be consumed, i.e. the reader should not be advanced. + protected virtual char ReadCharConsume(in ReadOnlySequence buffer, bool peek) + { + if (buffer.IsEmpty) + { + // TODO: Add IsCompleted check? + ReaderCompleted = true; + return default; + } + + var c = (char) buffer.First.Span[0]; + + if (peek) + { + // Advance reader to start (i.e. don't advance) + Reader.AdvanceTo(buffer.Start); + return c; + } + + // Consume char by advancing reader + Position++; + PreviousChar = c; + Reader.AdvanceTo(buffer.GetPosition(1)); + return c; + } + + /// + /// Read bytes from the pipe. + /// Returns the number of bytes actually read. + /// + /// The amount of bytes to read. + /// + public virtual ValueTask ReadAsync(Memory bytes, CancellationToken cancellationToken = default) + { + if (bytes.Length == 0 || ReaderCompleted) + return new ValueTask(0); + + if (Reader.TryRead(out var result) && TryReadConsume(result, bytes.Span, out var bytesRead)) + { + return new ValueTask(bytesRead); + } + + return ReadAwaitedAsync(bytes, cancellationToken); + } + + private async ValueTask ReadAwaitedAsync(Memory bytes, CancellationToken cancellationToken) + { + while (true) + { + var result = await Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (TryReadConsume(result, bytes.Span, out var bytesRead)) + { + return bytesRead; + } + } + } + + /// + /// Attempts to read the specified bytes from the reader and advances the reader if successful. + /// If the end of the pipe is reached then the available bytes is read and returned, if any. + /// + /// Returns true if any bytes was read or the reader was completed. + /// + /// + /// The read result from the pipe read operation. + /// The bytes to read. + /// The number of bytes read. + /// + protected virtual bool TryReadConsume(ReadResult result, in Span bytes, out long bytesRead) + { + if (result.IsCanceled) throw new InvalidOperationException("Read operation cancelled."); + + var buffer = result.Buffer; + + // Check if enough bytes have been read + if (buffer.Length >= bytes.Length) + { + // Copy requested amount of bytes from buffer and advance reader + buffer.Slice(0, bytes.Length).CopyTo(bytes); + Position += bytes.Length; + PreviousChar = (char) bytes[bytes.Length - 1]; + bytesRead = bytes.Length; + Reader.AdvanceTo(buffer.GetPosition(bytes.Length)); + return true; + } + + if (result.IsCompleted) + { + ReaderCompleted = true; + + if (buffer.IsEmpty) + { + bytesRead = 0; + return true; + } + + // End of pipe reached, less bytes available than requested + // Copy available bytes and advance reader to the end + buffer.CopyTo(bytes); + Position += buffer.Length; + PreviousChar = (char) buffer.Slice(buffer.Length - 1).First.Span[0]; + bytesRead = buffer.Length; + Reader.AdvanceTo(buffer.End); + return true; + } + + // Not enough bytes read, advance reader + Reader.AdvanceTo(buffer.Start, buffer.End); + + bytesRead = -1; + return false; // Consume unsuccessful + } + } +} \ No newline at end of file diff --git a/BencodeNET/Objects/BDictionary.cs b/BencodeNET/Objects/BDictionary.cs index 537fcd71dc..5164e35d53 100644 --- a/BencodeNET/Objects/BDictionary.cs +++ b/BencodeNET/Objects/BDictionary.cs @@ -1,8 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; using System.Linq; -using BencodeNET.IO; +using System.Threading; +using System.Threading.Tasks; namespace BencodeNET.Objects { @@ -48,20 +51,14 @@ public BDictionary(IDictionary dictionary) /// /// /// - public void Add(string key, string value) - { - Add(new BString(key), new BString(value)); - } + public void Add(string key, string value) => Add(new BString(key), new BString(value)); /// /// Adds the specified key and value to the dictionary as . /// /// /// - public void Add(string key, long value) - { - Add(new BString(key), new BNumber(value)); - } + public void Add(string key, long value) => Add(new BString(key), new BNumber(value)); /// /// Gets the value associated with the specified key and casts it as . @@ -99,55 +96,96 @@ public void MergeWith(BDictionary dictionary, ExistingKeyAction existingKeyActio if (existingKeyAction == ExistingKeyAction.Skip) continue; - // Replace strings and numbers - if (field.Value is BString || field.Value is BNumber) + switch (field.Value) { - this[field.Key] = field.Value; - continue; - } + // Replace strings and numbers + case BString _: + case BNumber _: + this[field.Key] = field.Value; + continue; - // Append list to existing list or replace other types - var newList = field.Value as BList; - if (newList != null) - { - var existingList = Get(field.Key); - if (existingList == null || existingKeyAction == ExistingKeyAction.Replace) + // Append list to existing list or replace other types + case BList newList: { - this[field.Key] = field.Value; + var existingList = Get(field.Key); + if (existingList == null || existingKeyAction == ExistingKeyAction.Replace) + { + this[field.Key] = field.Value; + continue; + } + existingList.AddRange(newList); continue; } - existingList.AddRange(newList); - continue; - } - // Merge dictionary with existing or replace other types - var newDictionary = field.Value as BDictionary; - if (newDictionary != null) - { - var existingDictionary = Get(field.Key); - if (existingDictionary == null || existingKeyAction == ExistingKeyAction.Replace) + // Merge dictionary with existing or replace other types + case BDictionary newDictionary: { - this[field.Key] = field.Value; - continue; + var existingDictionary = Get(field.Key); + if (existingDictionary == null || existingKeyAction == ExistingKeyAction.Replace) + { + this[field.Key] = field.Value; + continue; + } + existingDictionary.MergeWith(newDictionary); + break; } - existingDictionary.MergeWith(newDictionary); } } } -#pragma warning disable 1591 - protected override void EncodeObject(BencodeStream stream) + /// + public override int GetSizeInBytes() + { + var size = 2; + foreach (var entry in this) + { + size += entry.Key.GetSizeInBytes() + entry.Value.GetSizeInBytes(); + } + return size; + } + + /// + protected override void EncodeObject(Stream stream) { stream.Write('d'); - foreach (var kvPair in this) + foreach (var entry in this) { - kvPair.Key.EncodeTo(stream); - kvPair.Value.EncodeTo(stream); + entry.Key.EncodeTo(stream); + entry.Value.EncodeTo(stream); } stream.Write('e'); } + /// + protected override void EncodeObject(PipeWriter writer) + { + writer.WriteChar('d'); + foreach (var entry in this) + { + entry.Key.EncodeTo(writer); + entry.Value.EncodeTo(writer); + } + + writer.WriteChar('e'); + } + + /// + protected override async ValueTask EncodeObjectAsync(PipeWriter writer, CancellationToken cancellationToken) + { + writer.WriteChar('d'); + foreach (var entry in this) + { + cancellationToken.ThrowIfCancellationRequested(); + await entry.Key.EncodeToAsync(writer, cancellationToken).ConfigureAwait(false); + await entry.Value.EncodeToAsync(writer, cancellationToken).ConfigureAwait(false); + } + writer.WriteChar('e'); + + return await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } + #region IDictionary Members +#pragma warning disable 1591 public ICollection Keys => Value.Keys; @@ -162,13 +200,8 @@ protected override void EncodeObject(BencodeStream stream) /// public IBObject this[BString key] { - get { return ContainsKey(key) ? Value[key] : null; } - set - { - if (value == null) - throw new ArgumentNullException(nameof(value), "A null value cannot be added to a BDictionary"); - Value[key] = value; - } + get => ContainsKey(key) ? Value[key] : null; + set => Value[key] = value ?? throw new ArgumentNullException(nameof(value), "A null value cannot be added to a BDictionary"); } public void Add(KeyValuePair item) @@ -183,57 +216,30 @@ public void Add(BString key, IBObject value) Value.Add(key, value); } - public void Clear() - { - Value.Clear(); - } + public void Clear() => Value.Clear(); - public bool Contains(KeyValuePair item) - { - return Value.Contains(item); - } + public bool Contains(KeyValuePair item) => Value.Contains(item); - public bool ContainsKey(BString key) - { - return Value.ContainsKey(key); - } + public bool ContainsKey(BString key) => Value.ContainsKey(key); - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - Value.CopyTo(array, arrayIndex); - } + public void CopyTo(KeyValuePair[] array, int arrayIndex) => Value.CopyTo(array, arrayIndex); - public IEnumerator> GetEnumerator() - { - return Value.GetEnumerator(); - } + public IEnumerator> GetEnumerator() => Value.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public bool Remove(KeyValuePair item) - { - return Value.Remove(item); - } + public bool Remove(KeyValuePair item) => Value.Remove(item); - public bool Remove(BString key) - { - return Value.Remove(key); - } + public bool Remove(BString key) => Value.Remove(key); - public bool TryGetValue(BString key, out IBObject value) - { - return Value.TryGetValue(key, out value); - } + public bool TryGetValue(BString key, out IBObject value) => Value.TryGetValue(key, out value); - #endregion #pragma warning restore 1591 + #endregion } /// - /// Specifices the action to take when encountering an already existing key when merging two . + /// Specifies the action to take when encountering an already existing key when merging two . /// public enum ExistingKeyAction { diff --git a/BencodeNET/Objects/BList.cs b/BencodeNET/Objects/BList.cs index 89793b0cad..ac90faca40 100644 --- a/BencodeNET/Objects/BList.cs +++ b/BencodeNET/Objects/BList.cs @@ -1,9 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; using System.Linq; using System.Text; -using BencodeNET.IO; +using System.Threading; +using System.Threading.Tasks; namespace BencodeNET.Objects { @@ -60,38 +63,26 @@ public BList(IEnumerable objects) /// Adds a string to the list using . /// /// - public void Add(string value) - { - Add(new BString(value)); - } + public void Add(string value) => Add(new BString(value)); /// /// Adds a string to the list using the specified encoding. /// /// /// - public void Add(string value, Encoding encoding) - { - Add(new BString(value, encoding)); - } + public void Add(string value, Encoding encoding) => Add(new BString(value, encoding)); /// /// Adds an integer to the list. /// /// - public void Add(int value) - { - Add((IBObject) new BNumber(value)); - } + public void Add(int value) => Add((IBObject) new BNumber(value)); /// /// Adds a long to the list. /// /// - public void Add(long value) - { - Add((IBObject) new BNumber(value)); - } + public void Add(long value) => Add((IBObject) new BNumber(value)); /// /// Appends a list to the end of this instance. @@ -120,10 +111,7 @@ public void AddRange(BList list) /// Assumes all elements are /// and returns an enumerable of their string representation. /// - public IEnumerable AsStrings() - { - return AsStrings(Encoding.UTF8); - } + public IEnumerable AsStrings() => AsStrings(Encoding.UTF8); /// /// Assumes all elements are and returns @@ -165,17 +153,52 @@ public IEnumerable AsNumbers() } } -#pragma warning disable 1591 - protected override void EncodeObject(BencodeStream stream) + /// + public override int GetSizeInBytes() + { + var size = 2; + for (var i = 0; i < this.Count; i++) + { + size += this[i].GetSizeInBytes(); + } + return size; + } + + /// + protected override void EncodeObject(Stream stream) { stream.Write('l'); - foreach (var item in this) + for (var i = 0; i < this.Count; i++) { - item.EncodeTo(stream); + this[i].EncodeTo(stream); } stream.Write('e'); } -#pragma warning restore 1591 + + /// + protected override void EncodeObject(PipeWriter writer) + { + writer.WriteChar('l'); + for (var i = 0; i < this.Count; i++) + { + this[i].EncodeTo(writer); + } + writer.WriteChar('e'); + } + + /// + protected override async ValueTask EncodeObjectAsync(PipeWriter writer, CancellationToken cancellationToken) + { + writer.WriteChar('l'); + for (var i = 0; i < this.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + await this[i].EncodeToAsync(writer, cancellationToken).ConfigureAwait(false); + } + writer.WriteChar('e'); + + return await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } #region IList Members #pragma warning disable 1591 @@ -186,12 +209,8 @@ protected override void EncodeObject(BencodeStream stream) public IBObject this[int index] { - get { return Value[index]; } - set - { - if (value == null) throw new ArgumentNullException(nameof(value)); - Value[index] = value; - } + get => Value[index]; + set => Value[index] = value ?? throw new ArgumentNullException(nameof(value)); } public void Add(IBObject item) @@ -200,50 +219,23 @@ public void Add(IBObject item) Value.Add(item); } - public void Clear() - { - Value.Clear(); - } + public void Clear() => Value.Clear(); - public bool Contains(IBObject item) - { - return Value.Contains(item); - } + public bool Contains(IBObject item) => Value.Contains(item); - public void CopyTo(IBObject[] array, int arrayIndex) - { - Value.CopyTo(array, arrayIndex); - } + public void CopyTo(IBObject[] array, int arrayIndex) => Value.CopyTo(array, arrayIndex); - public IEnumerator GetEnumerator() - { - return Value.GetEnumerator(); - } + public IEnumerator GetEnumerator() => Value.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public int IndexOf(IBObject item) - { - return Value.IndexOf(item); - } + public int IndexOf(IBObject item) => Value.IndexOf(item); - public void Insert(int index, IBObject item) - { - Value.Insert(index, item); - } + public void Insert(int index, IBObject item) => Value.Insert(index, item); - public bool Remove(IBObject item) - { - return Value.Remove(item); - } + public bool Remove(IBObject item) => Value.Remove(item); - public void RemoveAt(int index) - { - Value.RemoveAt(index); - } + public void RemoveAt(int index) => Value.RemoveAt(index); #pragma warning restore 1591 #endregion @@ -252,7 +244,7 @@ public void RemoveAt(int index) /// /// Represents a bencoded list of type which implements . /// - public class BList : BList, IList where T : class, IBObject + public sealed class BList : BList, IList where T : class, IBObject { /// /// The underlying list. @@ -285,11 +277,7 @@ public BList(IEnumerable objects) if (obj == null) throw new InvalidCastException($"The object at index {index} is not of type {typeof (T).FullName}"); return obj; } - set - { - if (value == null) throw new ArgumentNullException(nameof(value)); - Value[index] = value; - } + set => Value[index] = value ?? throw new ArgumentNullException(nameof(value)); } public void Add(T item) @@ -298,43 +286,31 @@ public void Add(T item) Value.Add(item); } - public bool Contains(T item) - { - return Value.Contains(item); - } + public bool Contains(T item) => Value.Contains(item); - public void CopyTo(T[] array, int arrayIndex) - { - Value.CopyTo(array.Cast().ToArray(), arrayIndex); - } + public void CopyTo(T[] array, int arrayIndex) => Value.CopyTo(array.Cast().ToArray(), arrayIndex); public new IEnumerator GetEnumerator() { var i = 0; - var enumerator = Value.GetEnumerator(); - while (enumerator.MoveNext()) + using (var enumerator = Value.GetEnumerator()) { - var obj = enumerator.Current as T; - if (obj == null) throw new InvalidCastException($"The object at index {i} is not of type {typeof(T).FullName}"); - yield return (T) enumerator.Current; - i++; + while (enumerator.MoveNext()) + { + var obj = enumerator.Current as T; + if (obj == null) + throw new InvalidCastException($"The object at index {i} is not of type {typeof(T).FullName}"); + yield return (T) enumerator.Current; + i++; + } } } - public int IndexOf(T item) - { - return Value.IndexOf(item); - } + public int IndexOf(T item) => Value.IndexOf(item); - public void Insert(int index, T item) - { - Value.Insert(index, item); - } + public void Insert(int index, T item) => Value.Insert(index, item); - public bool Remove(T item) - { - return Value.Remove(item); - } + public bool Remove(T item) => Value.Remove(item); #pragma warning restore 1591 #endregion diff --git a/BencodeNET/Objects/BNumber.cs b/BencodeNET/Objects/BNumber.cs index ab88375f6c..5a1cdcc4ec 100644 --- a/BencodeNET/Objects/BNumber.cs +++ b/BencodeNET/Objects/BNumber.cs @@ -1,5 +1,7 @@ using System; -using BencodeNET.IO; +using System.IO; +using System.IO.Pipelines; +using System.Text; namespace BencodeNET.Objects { @@ -9,7 +11,7 @@ namespace BencodeNET.Objects /// /// The underlying value is a . /// - public class BNumber : BObject, IComparable + public sealed class BNumber : BObject, IComparable { private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); @@ -42,14 +44,34 @@ public BNumber(DateTime? datetime) Value = datetime?.Subtract(Epoch).Ticks / TimeSpan.TicksPerSecond ?? 0; } -#pragma warning disable 1591 - protected override void EncodeObject(BencodeStream stream) + /// + public override int GetSizeInBytes() => Value.DigitCount() + 2; + + /// + protected override void EncodeObject(Stream stream) { stream.Write('i'); stream.Write(Value); stream.Write('e'); } - + + /// + protected override void EncodeObject(PipeWriter writer) + { + var size = GetSizeInBytes(); + var buffer = writer.GetSpan(size).Slice(0, size); + + buffer[0] = (byte) 'i'; + buffer = buffer.Slice(1); + + Encoding.ASCII.GetBytes(Value.ToString().AsSpan(), buffer); + + buffer[buffer.Length - 1] = (byte) 'e'; + + writer.Advance(size); + } + +#pragma warning disable 1591 public static implicit operator int?(BNumber bint) { if (bint == null) return null; @@ -78,7 +100,6 @@ protected override void EncodeObject(BencodeStream stream) { if (bint == null) throw new InvalidCastException(); return bint.Value > 0; - } public static implicit operator DateTime?(BNumber number) @@ -100,37 +121,20 @@ protected override void EncodeObject(BencodeStream stream) return Epoch.AddSeconds(number); } - public static implicit operator BNumber(int value) - { - return new BNumber(value); - } + public static implicit operator BNumber(int value) => new BNumber(value); - public static implicit operator BNumber(long value) - { - return new BNumber(value); - } + public static implicit operator BNumber(long value) => new BNumber(value); - public static implicit operator BNumber(bool value) - { - return new BNumber(value ? 1 : 0); - } + public static implicit operator BNumber(bool value) => new BNumber(value ? 1 : 0); - public static implicit operator BNumber(DateTime? datetime) - { - return new BNumber(datetime); - } + public static implicit operator BNumber(DateTime? datetime) => new BNumber(datetime); public static bool operator ==(BNumber bnumber, BNumber other) { - if (ReferenceEquals(bnumber, null) && ReferenceEquals(other, null)) return true; - if (ReferenceEquals(bnumber, null) || ReferenceEquals(other, null)) return false; - return bnumber.Value == other.Value; + return bnumber?.Value == other?.Value; } - public static bool operator !=(BNumber bnumber, BNumber other) - { - return !(bnumber == other); - } + public static bool operator !=(BNumber bnumber, BNumber other) => !(bnumber == other); public override bool Equals(object other) { @@ -141,10 +145,7 @@ public override bool Equals(object other) /// /// Returns the hash code for this instance. /// - public override int GetHashCode() - { - return Value.GetHashCode(); - } + public override int GetHashCode() => Value.GetHashCode(); public int CompareTo(BNumber other) { @@ -154,25 +155,13 @@ public int CompareTo(BNumber other) return Value.CompareTo(other.Value); } - public override string ToString() - { - return Value.ToString(); - } + public override string ToString() => Value.ToString(); - public string ToString(string format) - { - return Value.ToString(format); - } + public string ToString(string format) => Value.ToString(format); - public string ToString(IFormatProvider formatProvider) - { - return Value.ToString(formatProvider); - } + public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); - public string ToString(string format, IFormatProvider formatProvider) - { - return Value.ToString(format, formatProvider); - } + public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider); #pragma warning restore 1591 } } diff --git a/BencodeNET/Objects/BObject.cs b/BencodeNET/Objects/BObject.cs index d69bfa2adb..68ec6102bf 100644 --- a/BencodeNET/Objects/BObject.cs +++ b/BencodeNET/Objects/BObject.cs @@ -1,6 +1,7 @@ using System.IO; -using System.Text; -using BencodeNET.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; namespace BencodeNET.Objects { @@ -13,43 +14,9 @@ internal BObject() { } /// - /// Encodes the object and returns the result as a string using . + /// Calculates the (encoded) size of the object in bytes. /// - /// - /// The object bencoded and converted to a string using . - /// - public virtual string EncodeAsString() - { - return EncodeAsString(Encoding.UTF8); - } - - /// - /// Encodes the object and returns the result as a string using the specified encoding. - /// - /// The encoding used to convert the encoded bytes to a string. - /// - /// The object bencoded and converted to a string using the specified encoding. - /// - public virtual string EncodeAsString(Encoding encoding) - { - using (var stream = EncodeTo(new MemoryStream())) - { - return encoding.GetString(stream.ToArray()); - } - } - - /// - /// Encodes the object and returns the raw bytes. - /// - /// The raw bytes of the bencoded object. - public virtual byte[] EncodeAsBytes() - { - using (var stream = new MemoryStream()) - { - EncodeTo(stream); - return stream.ToArray(); - } - } + public abstract int GetSizeInBytes(); /// /// Writes the object as bencode to the specified stream. @@ -59,31 +26,41 @@ public virtual byte[] EncodeAsBytes() /// The used stream. public TStream EncodeTo(TStream stream) where TStream : Stream { - EncodeObject(new BencodeStream(stream)); + var size = GetSizeInBytes(); + stream.TrySetLength(size); + EncodeObject(stream); return stream; } /// - /// Writes the object as bencode to the specified stream. + /// Writes the object as bencode to the specified without flushing the writer, + /// you should do that manually. /// - /// The stream to write to. - /// The used stream. - public BencodeStream EncodeTo(BencodeStream stream) + /// The writer to write to. + public void EncodeTo(PipeWriter writer) { - EncodeObject(stream); - return stream; + EncodeObject(writer); + } + + /// + /// Writes the object as bencode to the specified and flushes the writer afterwards. + /// + /// The writer to write to. + /// + public ValueTask EncodeToAsync(PipeWriter writer, CancellationToken cancellationToken = default) + { + return EncodeObjectAsync(writer, cancellationToken); } /// - /// Writes the object as bencode to the specified file path. + /// Writes the object asynchronously as bencode to the specified using a . /// - /// The file path to write the encoded object to. - public virtual void EncodeTo(string filePath) + /// The stream to write to. + /// The options for the . + /// + public ValueTask EncodeToAsync(Stream stream, StreamPipeWriterOptions writerOptions = null, CancellationToken cancellationToken = default) { - using (var stream = File.OpenWrite(filePath)) - { - EncodeTo(stream); - } + return EncodeObjectAsync(PipeWriter.Create(stream, writerOptions), cancellationToken); } /// @@ -91,7 +68,24 @@ public virtual void EncodeTo(string filePath) /// underlying value to bencode and write it to the stream. /// /// The stream to encode to. - protected abstract void EncodeObject(BencodeStream stream); + protected abstract void EncodeObject(Stream stream); + + /// + /// Implementations of this method should encode their underlying value to bencode and write it to the . + /// + /// The writer to encode to. + protected abstract void EncodeObject(PipeWriter writer); + + /// + /// Encodes and writes the underlying value to the and flushes the writer afterwards. + /// + /// The writer to encode to. + /// + protected virtual ValueTask EncodeObjectAsync(PipeWriter writer, CancellationToken cancellationToken) + { + EncodeObject(writer); + return writer.FlushAsync(cancellationToken); + } } /// diff --git a/BencodeNET/Objects/BObjectExtensions.cs b/BencodeNET/Objects/BObjectExtensions.cs new file mode 100644 index 0000000000..f797c8c5fd --- /dev/null +++ b/BencodeNET/Objects/BObjectExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Buffers; +using System.IO; +using System.Text; + +namespace BencodeNET.Objects +{ + /// + /// Extensions to simplify encoding directly as a string or byte array. + /// + public static class BObjectExtensions + { + /// + /// Encodes the object and returns the result as a string using . + /// + /// The object bencoded and converted to a string using . + public static string EncodeAsString(this IBObject bobject) => EncodeAsString(bobject, Encoding.UTF8); + + /// + /// Encodes the byte-string as bencode and returns the encoded string. + /// Uses the current value of the property. + /// + /// The byte-string as a bencoded string. + public static string EncodeAsString(this BString bstring) => EncodeAsString(bstring, bstring.Encoding); + + /// + /// Encodes the object and returns the result as a string using the specified encoding. + /// + /// + /// The encoding used to convert the encoded bytes to a string. + /// The object bencoded and converted to a string using the specified encoding. + public static string EncodeAsString(this IBObject bobject, Encoding encoding) + { + var size = bobject.GetSizeInBytes(); + var buffer = ArrayPool.Shared.Rent(size); + try + { + using (var stream = new MemoryStream(buffer)) + { + bobject.EncodeTo(stream); + return encoding.GetString(buffer.AsSpan().Slice(0, size)); + } + } + finally { ArrayPool.Shared.Return(buffer); } + } + + /// + /// Encodes the object and returns the raw bytes. + /// + /// The raw bytes of the bencoded object. + public static byte[] EncodeAsBytes(this IBObject bobject) + { + var size = bobject.GetSizeInBytes(); + var bytes = new byte[size]; + using (var stream = new MemoryStream(bytes)) + { + bobject.EncodeTo(stream); + return bytes; + } + } + + /// + /// Writes the object as bencode to the specified file path. + /// + /// + /// The file path to write the encoded object to. + public static void EncodeTo(this IBObject bobject, string filePath) + { + using (var stream = File.OpenWrite(filePath)) + { + bobject.EncodeTo(stream); + } + } + } +} diff --git a/BencodeNET/Objects/BString.cs b/BencodeNET/Objects/BString.cs index e9d3d6454f..8937ef94fd 100644 --- a/BencodeNET/Objects/BString.cs +++ b/BencodeNET/Objects/BString.cs @@ -1,7 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.IO; +using System.IO.Pipelines; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.IO; namespace BencodeNET.Objects @@ -13,7 +15,7 @@ namespace BencodeNET.Objects /// /// The underlying value is a array. /// - public class BString : BObject>, IComparable + public sealed class BString : BObject>, IComparable { /// /// The maximum number of digits that can be handled as the length part of a bencoded string. @@ -23,13 +25,12 @@ public class BString : BObject>, IComparable /// /// The underlying bytes of the string. /// - public override IReadOnlyList Value => _value; - private readonly byte[] _value; + public override ReadOnlyMemory Value { get; } /// /// Gets the length of the string in bytes. /// - public int Length => _value.Length; + public int Length => Value.Length; private static readonly Encoding DefaultEncoding = Encoding.UTF8; @@ -44,17 +45,23 @@ public Encoding Encoding } private Encoding _encoding; + /// + /// Creates an empty ('0:'). + /// + public BString() + : this((string)null) + { + } + /// /// Creates a from bytes with the specified encoding. /// /// The bytes representing the data. /// The encoding of the bytes. Defaults to . - public BString(IEnumerable bytes, Encoding encoding = null) + public BString(byte[] bytes, Encoding encoding = null) { - if (bytes == null) throw new ArgumentNullException(nameof(bytes)); - + Value = bytes ?? throw new ArgumentNullException(nameof(bytes)); _encoding = encoding ?? DefaultEncoding; - _value = bytes as byte[] ?? bytes.ToArray(); } /// @@ -65,73 +72,79 @@ public BString(IEnumerable bytes, Encoding encoding = null) /// public BString(string str, Encoding encoding = null) { - if (str == null) throw new ArgumentNullException(nameof(str)); - _encoding = encoding ?? DefaultEncoding; - _value = _encoding.GetBytes(str); - } - /// - /// Encodes this byte-string as bencode and returns the encoded string. - /// Uses the current value of the property. - /// - /// - /// This byte-string as a bencoded string. - /// - public override string EncodeAsString() - { - return EncodeAsString(_encoding); + if (string.IsNullOrEmpty(str)) + { + Value = Array.Empty(); + } + else + { + var maxByteCount = _encoding.GetMaxByteCount(str.Length); + var span = new byte[maxByteCount].AsSpan(); + + var length = _encoding.GetBytes(str.AsSpan(), span); + + Value = span.Slice(0, length).ToArray(); + } } -#pragma warning disable 1591 - protected override void EncodeObject(BencodeStream stream) + /// + public override int GetSizeInBytes() => Value.Length + 1 + Value.Length.DigitCount(); + + /// + protected override void EncodeObject(Stream stream) { - stream.Write(_value.Length); + stream.Write(Value.Length); stream.Write(':'); - stream.Write(_value); + stream.Write(Value.Span); } - public static implicit operator BString(string value) + /// + protected override void EncodeObject(PipeWriter writer) { - return new BString(value); + // Init + var size = GetSizeInBytes(); + var buffer = writer.GetSpan(size); + + // Write length + var writtenBytes = Encoding.GetBytes(Value.Length.ToString().AsSpan(), buffer); + + // Write ':' + buffer[writtenBytes] = (byte) ':'; + + // Write value + Value.Span.CopyTo(buffer.Slice(writtenBytes + 1)); + + // Commit + writer.Advance(size); } +#pragma warning disable 1591 + public static implicit operator BString(string value) => new BString(value); + public static bool operator ==(BString first, BString second) { - if (ReferenceEquals(first, null)) - return ReferenceEquals(second, null); + if (first is null) + return second is null; return first.Equals(second); } - public static bool operator !=(BString first, BString second) - { - return !(first == second); - } + public static bool operator !=(BString first, BString second) => !(first == second); - public override bool Equals(object other) - { - if (other is BString bstring) - return Value.SequenceEqual(bstring.Value); + public override bool Equals(object other) => other is BString bstring && Value.Span.SequenceEqual(bstring.Value.Span); - return false; - } - - public bool Equals(BString bstring) - { - if (bstring == null) - return false; - return Value.SequenceEqual(bstring.Value); - } + public bool Equals(BString bstring) => bstring != null && Value.Span.SequenceEqual(bstring.Value.Span); public override int GetHashCode() { - var bytesToHash = Math.Min(Value.Count, 32); + var bytesToHash = Math.Min(Value.Length, 32); long hashValue = 0; for (var i = 0; i < bytesToHash; i++) { - hashValue = (37 * hashValue + Value[i]) % int.MaxValue; + hashValue = (37 * hashValue + Value.Span[i]) % int.MaxValue; } return (int)hashValue; @@ -139,29 +152,7 @@ public override int GetHashCode() public int CompareTo(BString other) { - if (other == null) - return 1; - - var maxLength = Math.Max(this.Length, other.Length); - - for (var i = 0; i < maxLength; i++) - { - // This is shorter and thereby this is "less than" the other - if (i >= this.Length) - return -1; - - // The other is shorter and thereby this is "greater than" the other - if (i >= other.Length) - return 1; - - if (this.Value[i] > other.Value[i]) - return 1; - - if (this.Value[i] < other.Value[i]) - return -1; - } - - return 0; + return Value.Span.SequenceCompareTo(other.Value.Span); } #pragma warning restore 1591 @@ -173,7 +164,7 @@ public int CompareTo(BString other) /// public override string ToString() { - return _encoding.GetString(Value.ToArray()); + return _encoding.GetString(Value.Span); } /// @@ -186,7 +177,7 @@ public override string ToString() public string ToString(Encoding encoding) { encoding = encoding ?? _encoding; - return encoding.GetString(Value.ToArray()); + return encoding.GetString(Value.Span); } } } diff --git a/BencodeNET/Objects/IBObject.cs b/BencodeNET/Objects/IBObject.cs index cee4bc9ef5..192de1c54e 100644 --- a/BencodeNET/Objects/IBObject.cs +++ b/BencodeNET/Objects/IBObject.cs @@ -1,6 +1,7 @@ using System.IO; -using System.Text; -using BencodeNET.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; namespace BencodeNET.Objects { @@ -10,27 +11,9 @@ namespace BencodeNET.Objects public interface IBObject { /// - /// Encodes the object and returns the result as a string using . + /// Calculates the (encoded) size of the object in bytes. /// - /// - /// The object bencoded and converted to a string using . - /// - string EncodeAsString(); - - /// - /// Encodes the object and returns the result as a string using the specified encoding. - /// - /// The encoding used to convert the encoded bytes to a string. - /// - /// The object bencoded and converted to a string using the specified encoding. - /// - string EncodeAsString(Encoding encoding); - - /// - /// Encodes the object and returns the raw bytes. - /// - /// The raw bytes of the bencoded object. - byte[] EncodeAsBytes(); + int GetSizeInBytes(); /// /// Writes the object as bencode to the specified stream. @@ -41,16 +24,25 @@ public interface IBObject TStream EncodeTo(TStream stream) where TStream : Stream; /// - /// Writes the object as bencode to the specified stream. + /// Writes the object as bencode to the specified without flushing the writer, + /// you should do that manually. /// - /// The stream to write to. - /// The used stream. - BencodeStream EncodeTo(BencodeStream stream); + /// The writer to write to. + void EncodeTo(PipeWriter writer); + + /// + /// Writes the object as bencode to the specified and flushes the writer afterwards. + /// + /// The writer to write to. + /// + ValueTask EncodeToAsync(PipeWriter writer, CancellationToken cancellationToken = default); /// - /// Writes the object as bencode to the specified file. + /// Writes the object asynchronously as bencode to the specified using a . /// - /// The file path to write the encoded object to. - void EncodeTo(string filePath); + /// The stream to write to. + /// The options for the . + /// + ValueTask EncodeToAsync(Stream stream, StreamPipeWriterOptions writerOptions = null, CancellationToken cancellationToken = default); } } diff --git a/BencodeNET/Parsing/BDictionaryParser.cs b/BencodeNET/Parsing/BDictionaryParser.cs index 31bb6370b0..07f22f9c74 100644 --- a/BencodeNET/Parsing/BDictionaryParser.cs +++ b/BencodeNET/Parsing/BDictionaryParser.cs @@ -1,5 +1,7 @@ -using System; +using System; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.Exceptions; using BencodeNET.IO; using BencodeNET.Objects; @@ -22,9 +24,7 @@ public class BDictionaryParser : BObjectParser /// The parser used for contained keys and values. public BDictionaryParser(IBencodeParser bencodeParser) { - if (bencodeParser == null) throw new ArgumentNullException(nameof(bencodeParser)); - - BencodeParser = bencodeParser; + BencodeParser = bencodeParser ?? throw new ArgumentNullException(nameof(bencodeParser)); } /// @@ -35,38 +35,38 @@ public BDictionaryParser(IBencodeParser bencodeParser) /// /// The encoding used for parsing. /// - protected override Encoding Encoding => BencodeParser.Encoding; + public override Encoding Encoding => BencodeParser.Encoding; /// - /// Parses the next from the stream and its contained keys and values. + /// Parses the next and its contained keys and values from the reader. /// - /// The stream to parse from. + /// The reader to parse from. /// The parsed . - /// Invalid bencode - public override BDictionary Parse(BencodeStream stream) + /// Invalid bencode. + public override BDictionary Parse(BencodeReader reader) { - if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (reader == null) throw new ArgumentNullException(nameof(reader)); - var startPosition = stream.Position; + if (reader.Length < MinimumLength) + throw InvalidBencodeException.BelowMinimumLength(MinimumLength, reader.Length.Value, reader.Position); - if (stream.Length < MinimumLength) - throw InvalidBencodeException.BelowMinimumLength(MinimumLength, stream.Length, startPosition); + var startPosition = reader.Position; // Dictionaries must start with 'd' - if (stream.ReadChar() != 'd') - throw InvalidBencodeException.UnexpectedChar('d', stream.ReadPreviousChar(), startPosition); + if (reader.ReadChar() != 'd') + throw InvalidBencodeException.UnexpectedChar('d', reader.PreviousChar, startPosition); var dictionary = new BDictionary(); // Loop until next character is the end character 'e' or end of stream - while (stream.Peek() != 'e' && stream.Peek() != -1) + while (reader.PeekChar() != 'e' && reader.PeekChar() != default) { BString key; try { // Decode next string in stream as the key - key = BencodeParser.Parse(stream); + key = BencodeParser.Parse(reader); } - catch (BencodeException ex) + catch (BencodeException ex) { throw InvalidException("Could not parse dictionary key. Keys must be strings.", ex, startPosition); } @@ -75,30 +75,82 @@ public override BDictionary Parse(BencodeStream stream) try { // Decode next object in stream as the value - value = BencodeParser.Parse(stream); + value = BencodeParser.Parse(reader); } catch (BencodeException ex) { - throw InvalidException( - $"Could not parse dictionary value for the key '{key}'. There needs to be a value for each key.", - ex, startPosition); + throw InvalidException($"Could not parse dictionary value for the key '{key}'. There needs to be a value for each key.", ex, startPosition); } if (dictionary.ContainsKey(key)) { - throw InvalidException( - $"The dictionary already contains the key '{key}'. Duplicate keys are not supported.", startPosition); + throw InvalidException($"The dictionary already contains the key '{key}'. Duplicate keys are not supported.", startPosition); } dictionary.Add(key, value); } - if (stream.ReadChar() != 'e') + if (reader.ReadChar() != 'e') + throw InvalidBencodeException.MissingEndChar(startPosition); + + return dictionary; + } + + /// + /// Parses the next and its contained keys and values from the reader. + /// + /// The reader to parse from. + /// + /// The parsed . + /// Invalid bencode. + public override async ValueTask ParseAsync(PipeBencodeReader reader, CancellationToken cancellationToken = default) + { + if (reader == null) throw new ArgumentNullException(nameof(reader)); + + var startPosition = reader.Position; + + // Dictionaries must start with 'd' + if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'd') + throw InvalidBencodeException.UnexpectedChar('d', reader.PreviousChar, startPosition); + + var dictionary = new BDictionary(); + // Loop until next character is the end character 'e' or end of stream + while (await reader.PeekCharAsync(cancellationToken).ConfigureAwait(false) != 'e' && + await reader.PeekCharAsync(cancellationToken).ConfigureAwait(false) != default) { - if (stream.EndOfStream) throw InvalidBencodeException.MissingEndChar(); - throw InvalidBencodeException.InvalidEndChar(stream.ReadPreviousChar(), stream.Position); + BString key; + try + { + // Decode next string in stream as the key + key = await BencodeParser.ParseAsync(reader, cancellationToken).ConfigureAwait(false); + } + catch (BencodeException ex) + { + throw InvalidException("Could not parse dictionary key. Keys must be strings.", ex, startPosition); + } + + IBObject value; + try + { + // Decode next object in stream as the value + value = await BencodeParser.ParseAsync(reader, cancellationToken).ConfigureAwait(false); + } + catch (BencodeException ex) + { + throw InvalidException($"Could not parse dictionary value for the key '{key}'. There needs to be a value for each key.", ex, startPosition); + } + + if (dictionary.ContainsKey(key)) + { + throw InvalidException($"The dictionary already contains the key '{key}'. Duplicate keys are not supported.", startPosition); + } + + dictionary.Add(key, value); } + if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'e') + throw InvalidBencodeException.MissingEndChar(startPosition); + return dictionary; } diff --git a/BencodeNET/Parsing/BListParser.cs b/BencodeNET/Parsing/BListParser.cs index 5cd6131f9a..b128b7eb07 100644 --- a/BencodeNET/Parsing/BListParser.cs +++ b/BencodeNET/Parsing/BListParser.cs @@ -1,5 +1,7 @@ -using System; +using System; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.Exceptions; using BencodeNET.IO; using BencodeNET.Objects; @@ -22,9 +24,7 @@ public class BListParser : BObjectParser /// The parser used for parsing contained objects. public BListParser(IBencodeParser bencodeParser) { - if (bencodeParser == null) throw new ArgumentNullException(nameof(bencodeParser)); - - BencodeParser = bencodeParser; + BencodeParser = bencodeParser ?? throw new ArgumentNullException(nameof(bencodeParser)); } /// @@ -35,40 +35,72 @@ public BListParser(IBencodeParser bencodeParser) /// /// The encoding used for parsing. /// - protected override Encoding Encoding => BencodeParser.Encoding; + public override Encoding Encoding => BencodeParser.Encoding; /// - /// Parses the next from the stream. + /// Parses the next from the reader. /// - /// The stream to parse from. + /// The reader to parse from. /// The parsed . - /// Invalid bencode - public override BList Parse(BencodeStream stream) + /// Invalid bencode. + public override BList Parse(BencodeReader reader) { - if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (reader == null) throw new ArgumentNullException(nameof(reader)); + + if (reader.Length < MinimumLength) + throw InvalidBencodeException.BelowMinimumLength(MinimumLength, reader.Length.Value, reader.Position); - if (stream.Length < MinimumLength) - throw InvalidBencodeException.BelowMinimumLength(MinimumLength, stream.Length, stream.Position); + var startPosition = reader.Position; // Lists must start with 'l' - if (stream.ReadChar() != 'l') - throw InvalidBencodeException.UnexpectedChar('l', stream.ReadPreviousChar(), stream.Position); + if (reader.ReadChar() != 'l') + throw InvalidBencodeException.UnexpectedChar('l', reader.PreviousChar, startPosition); var list = new BList(); // Loop until next character is the end character 'e' or end of stream - while (stream.Peek() != 'e' && stream.Peek() != -1) + while (reader.PeekChar() != 'e' && reader.PeekChar() != default) { // Decode next object in stream - var bObject = BencodeParser.Parse(stream); + var bObject = BencodeParser.Parse(reader); list.Add(bObject); } - if (stream.ReadChar() != 'e') + if (reader.ReadChar() != 'e') + throw InvalidBencodeException.MissingEndChar(startPosition); + + return list; + } + + /// + /// Parses the next from the reader. + /// + /// The reader to parse from. + /// + /// The parsed . + /// Invalid bencode. + public override async ValueTask ParseAsync(PipeBencodeReader reader, CancellationToken cancellationToken = default) + { + if (reader == null) throw new ArgumentNullException(nameof(reader)); + + var startPosition = reader.Position; + + // Lists must start with 'l' + if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'l') + throw InvalidBencodeException.UnexpectedChar('l', reader.PreviousChar, startPosition); + + var list = new BList(); + // Loop until next character is the end character 'e' or end of stream + while (await reader.PeekCharAsync(cancellationToken).ConfigureAwait(false) != 'e' && + await reader.PeekCharAsync(cancellationToken).ConfigureAwait(false) != default) { - if (stream.EndOfStream) throw InvalidBencodeException.MissingEndChar(); - throw InvalidBencodeException.InvalidEndChar(stream.ReadPreviousChar(), stream.Position); + // Decode next object in stream + var bObject = await BencodeParser.ParseAsync(reader, cancellationToken).ConfigureAwait(false); + list.Add(bObject); } + if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'e') + throw InvalidBencodeException.MissingEndChar(startPosition); + return list; } } diff --git a/BencodeNET/Parsing/BNumberParser.cs b/BencodeNET/Parsing/BNumberParser.cs index 963f038046..2253e33a36 100644 --- a/BencodeNET/Parsing/BNumberParser.cs +++ b/BencodeNET/Parsing/BNumberParser.cs @@ -1,6 +1,9 @@ -using System; +using System; +using System.Buffers; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.Exceptions; using BencodeNET.IO; using BencodeNET.Objects; @@ -20,42 +23,89 @@ public class BNumberParser : BObjectParser /// /// The encoding used for parsing. /// - protected override Encoding Encoding => Encoding.UTF8; + public override Encoding Encoding => Encoding.UTF8; /// - /// Parses the next from the stream. + /// Parses the next from the reader. /// - /// The stream to parse from. + /// The reader to parse from. /// The parsed . - /// Invalid bencode - /// The bencode is unsupported by this library - public override BNumber Parse(BencodeStream stream) + /// Invalid bencode. + /// The bencode is unsupported by this library. + public override BNumber Parse(BencodeReader reader) { - if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (reader == null) throw new ArgumentNullException(nameof(reader)); - if (stream.Length < MinimumLength) - throw InvalidBencodeException.BelowMinimumLength(MinimumLength, stream.Length, stream.Position); + if (reader.Length < MinimumLength) + throw InvalidBencodeException.BelowMinimumLength(MinimumLength, reader.Length.Value, reader.Position); - var startPosition = stream.Position; + var startPosition = reader.Position; // Numbers must start with 'i' - if (stream.ReadChar() != 'i') - throw InvalidBencodeException.UnexpectedChar('i', stream.ReadPreviousChar(), stream.Position); + if (reader.ReadChar() != 'i') + throw InvalidBencodeException.UnexpectedChar('i', reader.PreviousChar, startPosition); - var digits = new StringBuilder(); - char c; - for (c = stream.ReadChar(); c != 'e' && c != default(char); c = stream.ReadChar()) + using (var digits = MemoryPool.Shared.Rent(BNumber.MaxDigits)) { - digits.Append(c); + var digitCount = 0; + for (var c = reader.ReadChar(); c != default && c != 'e'; c = reader.ReadChar()) + { + digits.Memory.Span[digitCount++] = c; + } + + if (digitCount == 0) + throw NoDigitsException(startPosition); + + // Last read character should be 'e' + if (reader.PreviousChar != 'e') + throw InvalidBencodeException.MissingEndChar(startPosition); + + return ParseNumber(digits.Memory.Span.Slice(0, digitCount), startPosition); } + } + + /// + /// Parses the next from the reader. + /// + /// The reader to parse from. + /// + /// The parsed . + /// Invalid bencode. + /// The bencode is unsupported by this library. + public override async ValueTask ParseAsync(PipeBencodeReader reader, CancellationToken cancellationToken = default) + { + if (reader == null) throw new ArgumentNullException(nameof(reader)); + + var startPosition = reader.Position; - // Last read character should be 'e' - if (c != 'e') + // Numbers must start with 'i' + if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'i') + throw InvalidBencodeException.UnexpectedChar('i', reader.PreviousChar, startPosition); + + using (var memoryOwner = MemoryPool.Shared.Rent(BNumber.MaxDigits)) { - if (stream.EndOfStream) throw InvalidBencodeException.MissingEndChar(); - throw InvalidBencodeException.InvalidEndChar(c, stream.Position); + var digits = memoryOwner.Memory; + var digitCount = 0; + for (var c = await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false); + c != default && c != 'e'; + c = await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false)) + { + digits.Span[digitCount++] = c; + } + + if (digitCount == 0) + throw NoDigitsException(startPosition); + + // Last read character should be 'e' + if (reader.PreviousChar != 'e') + throw InvalidBencodeException.MissingEndChar(startPosition); + + return ParseNumber(digits.Span.Slice(0, digitCount), startPosition); } + } + private BNumber ParseNumber(in ReadOnlySpan digits, long startPosition) + { var isNegative = digits[0] == '-'; var numberOfDigits = isNegative ? digits.Length - 1 : digits.Length; @@ -63,39 +113,45 @@ public override BNumber Parse(BencodeStream stream) if (numberOfDigits > BNumber.MaxDigits) { throw UnsupportedException( - $"The number '{digits}' has more than 19 digits and cannot be stored as a long (Int64) and therefore is not supported.", + $"The number '{digits.AsString()}' has more than 19 digits and cannot be stored as a long (Int64) and therefore is not supported.", startPosition); } // We need at least one digit if (numberOfDigits < 1) - throw InvalidException("It contains no digits.", startPosition); + throw NoDigitsException(startPosition); var firstDigit = isNegative ? digits[1] : digits[0]; // Leading zeros are not valid if (firstDigit == '0' && numberOfDigits > 1) - throw InvalidException($"Leading '0's are not valid. Found value '{digits}'.", startPosition); + throw InvalidException($"Leading '0's are not valid. Found value '{digits.AsString()}'.", startPosition); // '-0' is not valid either if (firstDigit == '0' && numberOfDigits == 1 && isNegative) throw InvalidException("'-0' is not a valid number.", startPosition); - long number; - if (!ParseUtil.TryParseLongFast(digits.ToString(), out number)) + if (!ParseUtil.TryParseLongFast(digits, out var number)) { - var nonSignChars = isNegative ? digits.ToString(1, digits.Length - 1) : digits.ToString(); - if (nonSignChars.Any(x => !x.IsDigit())) - throw InvalidException($"The value '{digits}' is not a valid number.", startPosition); + var nonSignChars = isNegative ? digits.Slice(1) : digits; + if (nonSignChars.AsString().Any(x => !x.IsDigit())) + throw InvalidException($"The value '{digits.AsString()}' is not a valid number.", startPosition); throw UnsupportedException( - $"The value '{digits}' is not a valid long (Int64). Supported values range from '{long.MinValue:N0}' to '{long.MaxValue:N0}'.", + $"The value '{digits.AsString()}' is not a valid long (Int64). Supported values range from '{long.MinValue:N0}' to '{long.MaxValue:N0}'.", startPosition); } return new BNumber(number); } + private static InvalidBencodeException NoDigitsException(long startPosition) + { + return new InvalidBencodeException( + $"It contains no digits. The number starts at position {startPosition}.", + startPosition); + } + private static InvalidBencodeException InvalidException(string message, long startPosition) { return new InvalidBencodeException( diff --git a/BencodeNET/Parsing/BObjectParser.cs b/BencodeNET/Parsing/BObjectParser.cs index 853411ce61..31882777e4 100644 --- a/BencodeNET/Parsing/BObjectParser.cs +++ b/BencodeNET/Parsing/BObjectParser.cs @@ -1,5 +1,8 @@ using System.IO; +using System.IO.Pipelines; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.IO; using BencodeNET.Objects; @@ -14,89 +17,57 @@ public abstract class BObjectParser : IBObjectParser where T : IBObject /// /// The encoding used for parsing. /// - protected abstract Encoding Encoding { get; } + public abstract Encoding Encoding { get; } - /// - /// Parses a bencoded string into an . - /// - /// The bencoded string to parse. - /// The parsed object. - IBObject IBObjectParser.ParseString(string bencodedString) + IBObject IBObjectParser.Parse(Stream stream) { - return ParseString(bencodedString); + return Parse(stream); } - /// - /// Parses a byte array into an . - /// - /// The bytes to parse. - /// The parsed object. - IBObject IBObjectParser.Parse(byte[] bytes) + IBObject IBObjectParser.Parse(BencodeReader reader) { - return Parse(bytes); + return Parse(reader); } - /// - /// Parses a stream into an . - /// - /// The stream to parse. - /// The parsed object. - IBObject IBObjectParser.Parse(Stream stream) + async ValueTask IBObjectParser.ParseAsync(PipeReader pipeReader, CancellationToken cancellationToken) { - return Parse(stream); + return await ParseAsync(new PipeBencodeReader(pipeReader), cancellationToken).ConfigureAwait(false); } - /// - /// Parses a bencoded stream into an . - /// - /// The bencoded stream to parse. - /// The parsed object. - IBObject IBObjectParser.Parse(BencodeStream stream) + async ValueTask IBObjectParser.ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken) { - return Parse(stream); + return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); } /// - /// Parses a bencoded string into an of type . + /// Parses a stream into an of type . /// - /// The bencoded string to parse. + /// The stream to parse. /// The parsed object. - public virtual T ParseString(string bencodedString) - { - using (var stream = bencodedString.AsStream(Encoding)) - { - return Parse(stream); - } - } + public virtual T Parse(Stream stream) => Parse(new BencodeReader(stream, leaveOpen: true)); /// - /// Parses a byte array into an of type . + /// Parses an of type from a . /// - /// The bytes to parse. + /// The reader to read from. /// The parsed object. - public virtual T Parse(byte[] bytes) - { - using (var stream = new MemoryStream(bytes)) - { - return Parse(stream); - } - } + public abstract T Parse(BencodeReader reader); /// - /// Parses a stream into an of type . + /// Parses an of type from a . /// - /// The stream to parse. + /// The pipe reader to read from. + /// /// The parsed object. - public virtual T Parse(Stream stream) - { - return Parse(new BencodeStream(stream)); - } + public ValueTask ParseAsync(PipeReader pipeReader, CancellationToken cancellationToken = default) + => ParseAsync(new PipeBencodeReader(pipeReader), cancellationToken); /// - /// Parses a bencoded stream into an of type . + /// Parses an of type from a . /// - /// The bencoded stream to parse. + /// The pipe reader to read from. + /// /// The parsed object. - public abstract T Parse(BencodeStream stream); + public abstract ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/BencodeNET/Parsing/BObjectParserExtensions.cs b/BencodeNET/Parsing/BObjectParserExtensions.cs new file mode 100644 index 0000000000..eac268927e --- /dev/null +++ b/BencodeNET/Parsing/BObjectParserExtensions.cs @@ -0,0 +1,67 @@ +using System.IO; +using BencodeNET.Objects; + +namespace BencodeNET.Parsing +{ + /// + /// Extensions to simplify parsing strings and byte arrays. + /// + public static class BObjectParserExtensions + { + /// + /// Parses a bencoded string into an . + /// + /// + /// The bencoded string to parse. + /// The parsed object. + public static IBObject ParseString(this IBObjectParser parser, string bencodedString) + { + using (var stream = bencodedString.AsStream(parser.Encoding)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a byte array into an . + /// + /// + /// The bytes to parse. + /// The parsed object. + public static IBObject Parse(this IBObjectParser parser, byte[] bytes) + { + using (var stream = new MemoryStream(bytes)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a bencoded string into an of type . + /// + /// + /// The bencoded string to parse. + /// The parsed object. + public static T ParseString(this IBObjectParser parser, string bencodedString) where T : IBObject + { + using (var stream = bencodedString.AsStream(parser.Encoding)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a byte array into an of type . + /// + /// + /// The bytes to parse. + /// The parsed object. + public static T Parse(this IBObjectParser parser, byte[] bytes) where T : IBObject + { + using (var stream = new MemoryStream(bytes)) + { + return parser.Parse(stream); + } + } + } +} diff --git a/BencodeNET/Parsing/BStringParser.cs b/BencodeNET/Parsing/BStringParser.cs index 131ec1aff6..4948c73304 100644 --- a/BencodeNET/Parsing/BStringParser.cs +++ b/BencodeNET/Parsing/BStringParser.cs @@ -1,5 +1,8 @@ -using System; +using System; +using System.Buffers; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.Exceptions; using BencodeNET.IO; using BencodeNET.Objects; @@ -11,6 +14,7 @@ namespace BencodeNET.Parsing /// public class BStringParser : BObjectParser { + /// /// The minimum stream length in bytes for a valid string ('0:'). /// @@ -29,50 +33,141 @@ public BStringParser() /// public BStringParser(Encoding encoding) { - if (encoding == null) throw new ArgumentNullException(nameof(encoding)); - - Encoding = encoding; + _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); } /// /// The encoding used when creating the when parsing. /// - protected override Encoding Encoding { get; } + public override Encoding Encoding => _encoding; + private Encoding _encoding; + + /// + /// Changes the encoding used for parsing. + /// + /// The new encoding to use. + public void ChangeEncoding(Encoding encoding) + { + _encoding = encoding; + } /// - /// Parses the next from the stream. + /// Parses the next from the reader. /// - /// The stream to parse from. + /// The reader to parse from. /// The parsed . - /// Invalid bencode - /// The bencode is unsupported by this library - public override BString Parse(BencodeStream stream) + /// Invalid bencode. + /// The bencode is unsupported by this library. + public override BString Parse(BencodeReader reader) { - if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (reader == null) throw new ArgumentNullException(nameof(reader)); // Minimum valid bencode string is '0:' meaning an empty string - if (stream.Length < MinimumLength) - throw InvalidBencodeException.BelowMinimumLength(MinimumLength, stream.Length, stream.Position); + if (reader.Length < MinimumLength) + throw InvalidBencodeException.BelowMinimumLength(MinimumLength, reader.Length.Value, reader.Position); - var startPosition = stream.Position; + var startPosition = reader.Position; - var lengthString = new StringBuilder(); - for (var c = stream.ReadChar(); c != ':' && c != default(char); c = stream.ReadChar()) + var buffer = ArrayPool.Shared.Rent(BString.LengthMaxDigits); + try { - // Because of memory limitations (~1-2 GB) we know for certain we cannot handle more than 10 digits (10GB) - if (lengthString.Length >= BString.LengthMaxDigits) + var lengthString = buffer.AsSpan(); + var lengthStringCount = 0; + for (var c = reader.ReadChar(); c != default && c.IsDigit(); c = reader.ReadChar()) { - throw UnsupportedException( - $"Length of string is more than {BString.LengthMaxDigits} digits (>10GB) and is not supported (max is ~1-2GB).", - startPosition); + EnsureLengthStringBelowMaxLength(lengthStringCount, startPosition); + + lengthString[lengthStringCount++] = c; } - lengthString.Append(c); + EnsurePreviousCharIsColon(reader.PreviousChar, reader.Position); + + var stringLength = ParseStringLength(lengthString, lengthStringCount, startPosition); + var bytes = new byte[stringLength]; + var bytesRead = reader.Read(bytes); + + EnsureExpectedBytesRead(bytesRead, stringLength, startPosition); + + return new BString(bytes, Encoding); } + finally + { + ArrayPool.Shared.Return(buffer); + } + } - long stringLength; - if (!ParseUtil.TryParseLongFast(lengthString.ToString(), out stringLength)) - throw InvalidException($"Invalid length '{lengthString}' of string.", startPosition); + /// + /// Parses the next from the reader. + /// + /// The reader to parse from. + /// + /// The parsed . + /// Invalid bencode. + /// The bencode is unsupported by this library. + public override async ValueTask ParseAsync(PipeBencodeReader reader, CancellationToken cancellationToken = default) + { + if (reader == null) throw new ArgumentNullException(nameof(reader)); + + var startPosition = reader.Position; + + using (var memoryOwner = MemoryPool.Shared.Rent(BString.LengthMaxDigits)) + { + var lengthString = memoryOwner.Memory; + var lengthStringCount = 0; + for (var c = await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false); + c != default && c.IsDigit(); + c = await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false)) + { + EnsureLengthStringBelowMaxLength(lengthStringCount, startPosition); + + lengthString.Span[lengthStringCount++] = c; + } + + EnsurePreviousCharIsColon(reader.PreviousChar, reader.Position); + + var stringLength = ParseStringLength(lengthString.Span, lengthStringCount, startPosition); + var bytes = new byte[stringLength]; + var bytesRead = await reader.ReadAsync(bytes, cancellationToken).ConfigureAwait(false); + + EnsureExpectedBytesRead(bytesRead, stringLength, startPosition); + + return new BString(bytes, Encoding); + } + } + + /// + /// Ensures that the length (number of digits) of the string-length part is not above + /// as that would equal 10 GB of data, which we cannot handle. + /// + private void EnsureLengthStringBelowMaxLength(int lengthStringCount, long startPosition) + { + // Because of memory limitations (~1-2 GB) we know for certain we cannot handle more than 10 digits (10GB) + if (lengthStringCount >= BString.LengthMaxDigits) + { + throw UnsupportedException( + $"Length of string is more than {BString.LengthMaxDigits} digits (>10GB) and is not supported (max is ~1-2GB).", + startPosition); + } + } + + /// + /// Ensure that the previously read char is a colon (:), + /// separating the string-length part and the actual string value. + /// + private void EnsurePreviousCharIsColon(char previousChar, long position) + { + if (previousChar != ':') throw InvalidBencodeException.UnexpectedChar(':', previousChar, position - 1); + } + + /// + /// Parses the string-length into a . + /// + private long ParseStringLength(Span lengthString, int lengthStringCount, long startPosition) + { + lengthString = lengthString.Slice(0, lengthStringCount); + + if (!ParseUtil.TryParseLongFast(lengthString, out var stringLength)) + throw InvalidException($"Invalid length '{lengthString.AsString()}' of string.", startPosition); // Int32.MaxValue is ~2GB and is the absolute maximum that can be handled in memory if (stringLength > int.MaxValue) @@ -82,17 +177,20 @@ public override BString Parse(BencodeStream stream) startPosition); } - var bytes = stream.Read((int)stringLength); + return stringLength; + } + /// + /// Ensures that number of bytes read matches the expected number parsed from the string-length part. + /// + private void EnsureExpectedBytesRead(long bytesRead, long stringLength, long startPosition) + { // If the two don't match we've reached the end of the stream before reading the expected number of chars - if (bytes.Length != stringLength) - { - throw InvalidException( - $"Expected string to be {stringLength:N0} bytes long but could only read {bytes.Length:N0} bytes.", - startPosition); - } + if (bytesRead == stringLength) return; - return new BString(bytes, Encoding); + throw InvalidException( + $"Expected string to be {stringLength:N0} bytes long but could only read {bytesRead:N0} bytes.", + startPosition); } private static InvalidBencodeException InvalidException(string message, long startPosition) diff --git a/BencodeNET/Parsing/BencodeParser.cs b/BencodeNET/Parsing/BencodeParser.cs index 6416431f75..f75c694b2e 100644 --- a/BencodeNET/Parsing/BencodeParser.cs +++ b/BencodeNET/Parsing/BencodeParser.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.Exceptions; using BencodeNET.IO; using BencodeNET.Objects; @@ -14,130 +14,50 @@ namespace BencodeNET.Parsing public class BencodeParser : IBencodeParser { /// - /// Creates an instance using and the default parsers. + /// List of parsers used by the . /// - public BencodeParser() - : this(Encoding.UTF8) - { } - - /// - /// Creates an instance using the specified encoding and the default parsers. - /// - /// The encoding to use when parsing. - public BencodeParser(Encoding encoding) - { - Encoding = encoding; - - Parsers = new BObjectParserList - { - new BStringParser(encoding), - new BNumberParser(), - new BListParser(this), - new BDictionaryParser(this), - new TorrentParser(this) - }; - } - - /// - /// Creates an instance using and the default parsers plus the specified parsers. - /// Existing default parsers for the same type will be replaced by the new parsers. - /// - /// The new parsers to add or replace. - public BencodeParser(IEnumerable> parsers) - : this(parsers, Encoding.UTF8) - { } - - /// - /// Creates an instance using and the default parsers plus the specified parsers. - /// Existing default parsers for the same type will be replaced by the new parsers. - /// - /// The new parsers to add or replace. - public BencodeParser(IDictionary parsers) - : this(parsers, Encoding.UTF8) - { } + public BObjectParserList Parsers { get; } /// - /// Creates an instance using the specified encoding and the default parsers plus the specified parsers. - /// Existing default parsers for the same type will be replaced by the new parsers. + /// The encoding use for parsing. /// - /// The new parsers to add or replace. - /// The encoding to use when parsing. - public BencodeParser(IEnumerable> parsers, Encoding encoding) + public Encoding Encoding { - Encoding = encoding; - - foreach (var entry in parsers) + get => _encoding; + set { - Parsers.AddOrReplace(entry.Key, entry.Value); + _encoding = value ?? throw new ArgumentNullException(nameof(value)); + Parsers.GetSpecific()?.ChangeEncoding(value); } } + private Encoding _encoding; /// - /// Creates an instance using the specified encoding and the default parsers plus the specified parsers. - /// Existing default parsers for the same type will be replaced by the new parsers. + /// Creates an instance using the specified encoding and the default parsers. + /// Encoding defaults to if not specified. /// - /// The new parsers to add or replace. /// The encoding to use when parsing. - public BencodeParser(IDictionary parsers, Encoding encoding) - : this((IEnumerable>)parsers, encoding) - { } - - /// - /// The encoding use for parsing. - /// - public Encoding Encoding { get; protected set; } - - /// - /// The parsers used by this instance when parsing bencoded. - /// - public BObjectParserList Parsers { get; } - - /// - /// Parses a bencoded string into an . - /// - /// The bencoded string to parse. - /// The parsed object. - public IBObject ParseString(string bencodedString) + public BencodeParser(Encoding encoding = null) { - using (var stream = bencodedString.AsStream(Encoding)) - { - return Parse(stream); - } - } + _encoding = encoding ?? Encoding.UTF8; - /// - /// Parses a bencoded array of bytes into an . - /// - /// The bencoded bytes to parse. - /// The parsed object. - public IBObject Parse(byte[] bytes) - { - using (var stream = new MemoryStream(bytes)) + Parsers = new BObjectParserList { - return Parse(stream); - } - } - - /// - /// Parses a stream into an . - /// - /// The stream to parse. - /// The parsed object. - public IBObject Parse(Stream stream) - { - return Parse(new BencodeStream(stream)); + new BNumberParser(), + new BStringParser(_encoding), + new BListParser(this), + new BDictionaryParser(this) + }; } /// - /// Parses a into an . + /// Parses an from the reader. /// - /// The stream to parse. - /// The parsed object. - public IBObject Parse(BencodeStream stream) + public virtual IBObject Parse(BencodeReader reader) { - if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (reader == null) throw new ArgumentNullException(nameof(reader)); - switch (stream.PeekChar()) + switch (reader.PeekChar()) { case '0': case '1': @@ -148,94 +68,71 @@ public IBObject Parse(BencodeStream stream) case '6': case '7': case '8': - case '9': return Parse(stream); - case 'i': return Parse(stream); - case 'l': return Parse(stream); - case 'd': return Parse(stream); + case '9': return Parse(reader); + case 'i': return Parse(reader); + case 'l': return Parse(reader); + case 'd': return Parse(reader); + case default(char): return null; } - throw InvalidBencodeException.InvalidBeginningChar(stream.PeekChar(), stream.Position); - } - - /// - /// Parses a bencoded file into an . - /// - /// The path to the file to parse. - /// The parsed object. - public IBObject Parse(string filePath) - { - using (var stream = File.OpenRead(filePath)) - { - return Parse(stream); - } + throw InvalidBencodeException.InvalidBeginningChar(reader.PeekChar(), reader.Position); } /// - /// Parses a bencoded string into an of type . + /// Parse an of type from the reader. /// /// The type of to parse as. - /// The bencoded string to parse. - /// The parsed object. - public T ParseString(string bencodedString) where T : class, IBObject + public virtual T Parse(BencodeReader reader) where T : class, IBObject { - using (var stream = bencodedString.AsStream(Encoding)) - { - return Parse(stream); - } + var parser = Parsers.Get(); + + if (parser == null) + throw new BencodeException($"Missing parser for the type '{typeof(T).FullName}'. Stream position: {reader.Position}"); + + return parser.Parse(reader); } /// - /// Parses a bencoded array of bytes into an of type . + /// Parse an from the . /// - /// The type of to parse as. - /// The bencoded bytes to parse. - /// The parsed object. - public T Parse(byte[] bytes) where T : class, IBObject + public virtual async ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default) { - using (var stream = new MemoryStream(bytes)) + if (pipeReader == null) throw new ArgumentNullException(nameof(pipeReader)); + + switch (await pipeReader.PeekCharAsync(cancellationToken).ConfigureAwait(false)) { - return Parse(stream); + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); + case 'i': return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); + case 'l': return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); + case 'd': return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); + case default(char): return null; } - } - /// - /// Parses a stream into an of type . - /// - /// The type of to parse as. - /// The bencoded string to parse. - /// The parsed object. - public T Parse(Stream stream) where T : class, IBObject - { - return Parse(new BencodeStream(stream)); + throw InvalidBencodeException.InvalidBeginningChar( + await pipeReader.PeekCharAsync(cancellationToken).ConfigureAwait(false), + pipeReader.Position); } /// - /// Parses a into an of type . + /// Parse an of type from the . /// - /// The type of to parse as. - /// The bencoded string to parse. - /// The parsed object. - public T Parse(BencodeStream stream) where T : class, IBObject + public virtual async ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default) where T : class, IBObject { var parser = Parsers.Get(); if (parser == null) - throw new BencodeException($"Missing parser for the type '{typeof(T).FullName}'. Stream position: {stream.Position}"); - - return parser.Parse(stream); - } + throw new BencodeException($"Missing parser for the type '{typeof(T).FullName}'. Stream position: {pipeReader.Position}"); - /// - /// Parses a bencoded file into an of type . - /// - /// The path to the file to parse. - /// The parsed object. - public T Parse(string filePath) where T : class, IBObject - { - using (var stream = File.OpenRead(filePath)) - { - return Parse(stream); - } + return await parser.ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); } } } diff --git a/BencodeNET/Parsing/BencodeParserExtensions.cs b/BencodeNET/Parsing/BencodeParserExtensions.cs new file mode 100644 index 0000000000..47354f9f36 --- /dev/null +++ b/BencodeNET/Parsing/BencodeParserExtensions.cs @@ -0,0 +1,168 @@ +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using BencodeNET.IO; +using BencodeNET.Objects; + +namespace BencodeNET.Parsing +{ + /// + /// Extensions to simplify parsing strings, byte arrays or files directly. + /// + public static class BencodeParserExtensions + { + /// + /// Parses a bencoded string into an . + /// + /// + /// The bencoded string to parse. + /// The parsed object. + public static IBObject ParseString(this IBencodeParser parser, string bencodedString) + { + using (var stream = bencodedString.AsStream(parser.Encoding)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a bencoded array of bytes into an . + /// + /// + /// The bencoded bytes to parse. + /// The parsed object. + public static IBObject Parse(this IBencodeParser parser, byte[] bytes) + { + using (var stream = new MemoryStream(bytes)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a bencoded file into an . + /// + /// + /// The path to the file to parse. + /// The parsed object. + public static IBObject Parse(this IBencodeParser parser, string filePath) + { + using (var stream = File.OpenRead(filePath)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a bencoded string into an of type . + /// + /// The type of to parse as. + /// + /// The bencoded string to parse. + /// The parsed object. + public static T ParseString(this IBencodeParser parser, string bencodedString) where T : class, IBObject + { + using (var stream = bencodedString.AsStream(parser.Encoding)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a bencoded array of bytes into an of type . + /// + /// The type of to parse as. + /// + /// The bencoded bytes to parse. + /// The parsed object. + public static T Parse(this IBencodeParser parser, byte[] bytes) where T : class, IBObject + { + using (var stream = new MemoryStream(bytes)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a bencoded file into an of type . + /// + /// + /// The path to the file to parse. + /// The parsed object. + public static T Parse(this IBencodeParser parser, string filePath) where T : class, IBObject + { + using (var stream = File.OpenRead(filePath)) + { + return parser.Parse(stream); + } + } + + /// + /// Parses a stream into an . + /// + /// + /// The stream to parse. + /// The parsed object. + public static IBObject Parse(this IBencodeParser parser, Stream stream) + { + using (var reader = new BencodeReader(stream, leaveOpen: true)) + { + return parser.Parse(reader); + } + } + + /// + /// Parses a stream into an of type . + /// + /// The type of to parse as. + /// + /// The stream to parse. + /// The parsed object. + public static T Parse(this IBencodeParser parser, Stream stream) where T : class, IBObject + { + using (var reader = new BencodeReader(stream, leaveOpen: true)) + { + return parser.Parse(reader); + } + } + + /// + /// Parses an from the . + /// + public static ValueTask ParseAsync(this IBencodeParser parser, PipeReader pipeReader, CancellationToken cancellationToken = default) + { + var reader = new PipeBencodeReader(pipeReader); + return parser.ParseAsync(reader, cancellationToken); + } + + /// + /// Parses an of type from the . + /// + /// The type of to parse as. + public static ValueTask ParseAsync(this IBencodeParser parser, PipeReader pipeReader, CancellationToken cancellationToken = default) where T : class, IBObject + { + var reader = new PipeBencodeReader(pipeReader); + return parser.ParseAsync(reader, cancellationToken); + } + + /// + /// Parses an from the asynchronously using a . + /// + public static ValueTask ParseAsync(this IBencodeParser parser, Stream stream, StreamPipeReaderOptions readerOptions = null, CancellationToken cancellationToken = default) + { + var reader = PipeReader.Create(stream, readerOptions); + return parser.ParseAsync(reader, cancellationToken); + } + + /// + /// Parses an of type from the asynchronously using a . + /// + /// The type of to parse as. + public static ValueTask ParseAsync(this IBencodeParser parser, Stream stream, StreamPipeReaderOptions readerOptions = null, CancellationToken cancellationToken = default) where T : class, IBObject + { + var reader = PipeReader.Create(stream, readerOptions); + return parser.ParseAsync(reader, cancellationToken); + } + } +} diff --git a/BencodeNET/Parsing/IBObjectParser.cs b/BencodeNET/Parsing/IBObjectParser.cs index d10653d2b5..1aa5623d92 100644 --- a/BencodeNET/Parsing/IBObjectParser.cs +++ b/BencodeNET/Parsing/IBObjectParser.cs @@ -1,4 +1,8 @@ using System.IO; +using System.IO.Pipelines; +using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.IO; using BencodeNET.Objects; @@ -10,65 +14,70 @@ namespace BencodeNET.Parsing public interface IBObjectParser { /// - /// Parses a bencoded string into an . + /// The encoding used for parsing. /// - /// The bencoded string to parse. - /// The parsed object. - IBObject ParseString(string bencodedString); + Encoding Encoding { get; } /// - /// Parses a byte array into an . + /// Parses a stream into an . /// - /// The bytes to parse. + /// The stream to parse. /// The parsed object. - IBObject Parse(byte[] bytes); + IBObject Parse(Stream stream); /// - /// Parses a stream into an . + /// Parses an from a . /// - /// The stream to parse. + IBObject Parse(BencodeReader reader); + + /// + /// Parses an from a . + /// + /// The pipe reader to read from. + /// /// The parsed object. - IBObject Parse(Stream stream); + ValueTask ParseAsync(PipeReader pipeReader, CancellationToken cancellationToken = default); /// - /// Parses a bencoded stream into an . + /// Parses an from a . /// - /// The bencoded stream to parse. + /// The pipe reader to read from. + /// /// The parsed object. - IBObject Parse(BencodeStream stream); + ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default); } /// /// A contract for parsing bencode from different sources as type inheriting . /// - public interface IBObjectParser : IBObjectParser where T : IBObject + public interface IBObjectParser : IBObjectParser where T : IBObject { /// - /// Parses a bencoded string into an of type . + /// Parses a stream into an of type . /// - /// The bencoded string to parse. + /// The stream to parse. /// The parsed object. - new T ParseString(string bencodedString); + new T Parse(Stream stream); /// - /// Parses a byte array into an of type . + /// Parses an of type from a . /// - /// The bytes to parse. - /// The parsed object. - new T Parse(byte[] bytes); + new T Parse(BencodeReader reader); /// - /// Parses a stream into an of type . + /// Parses an of type from a . /// - /// The stream to parse. + /// The pipe reader to read from. + /// /// The parsed object. - new T Parse(Stream stream); + new ValueTask ParseAsync(PipeReader pipeReader, CancellationToken cancellationToken = default); /// - /// Parses a bencoded stream into an of type . + /// Parses an of type from a . /// - /// The bencoded stream to parse. + /// The pipe reader to read from. + /// /// The parsed object. - new T Parse(BencodeStream stream); + new ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default); } } diff --git a/BencodeNET/Parsing/IBencodeParser.cs b/BencodeNET/Parsing/IBencodeParser.cs index f92f1c176a..b492a57c23 100644 --- a/BencodeNET/Parsing/IBencodeParser.cs +++ b/BencodeNET/Parsing/IBencodeParser.cs @@ -1,5 +1,8 @@ using System.IO; +using System.IO.Pipelines; using System.Text; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.IO; using BencodeNET.Objects; @@ -11,82 +14,36 @@ namespace BencodeNET.Parsing public interface IBencodeParser { /// - /// The encoding use for parsing. - /// - Encoding Encoding { get; } - - /// - /// Parses a bencoded string into an . - /// - /// The bencoded string to parse. - /// The parsed object. - IBObject ParseString(string bencodedString); - - /// - /// Parses a bencoded array of bytes into an . - /// - /// The bencoded bytes to parse. - /// The parsed object. - IBObject Parse(byte[] bytes); - - /// - /// Parses a stream into an . + /// List of parsers used by the . /// - /// The stream to parse. - /// The parsed object. - IBObject Parse(Stream stream); + BObjectParserList Parsers { get; } /// - /// Parses a into an . - /// - /// The stream to parse. - /// The parsed object. - IBObject Parse(BencodeStream stream); - - /// - /// Parses a bencoded file into an . - /// - /// The path to the file to parse. - /// The parsed object. - IBObject Parse(string filePath); - - /// - /// Parses a bencoded string into an of type . + /// The encoding use for parsing. /// - /// The type of to parse as. - /// The bencoded string to parse. - /// The parsed object. - T ParseString(string bencodedString) where T : class, IBObject; + Encoding Encoding { get; } /// - /// Parses a bencoded array of bytes into an of type . + /// Parses an from the reader. /// - /// The type of to parse as. - /// The bencoded bytes to parse. - /// The parsed object. - T Parse(byte[] bytes) where T : class, IBObject; + /// + IBObject Parse(BencodeReader reader); /// - /// Parses a stream into an of type . + /// Parse an of type from the reader. /// /// The type of to parse as. - /// The bencoded string to parse. - /// The parsed object. - T Parse(Stream stream) where T : class, IBObject; + /// + T Parse(BencodeReader reader) where T : class, IBObject; /// - /// Parses a into an of type . + /// Parse an from the . /// - /// The type of to parse as. - /// The bencoded string to parse. - /// The parsed object. - T Parse(BencodeStream stream) where T : class, IBObject; + ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default); /// - /// Parses a bencoded file into an of type . + /// Parse an of type from the . /// - /// The path to the file to parse. - /// The parsed object. - T Parse(string filePath) where T : class, IBObject; + ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default) where T : class, IBObject; } } \ No newline at end of file diff --git a/BencodeNET/Parsing/ParseUtil.cs b/BencodeNET/Parsing/ParseUtil.cs index 0c725696f2..437edfff7b 100644 --- a/BencodeNET/Parsing/ParseUtil.cs +++ b/BencodeNET/Parsing/ParseUtil.cs @@ -1,4 +1,6 @@ -namespace BencodeNET.Parsing +using System; + +namespace BencodeNET.Parsing { /// /// A collection of helper methods for parsing bencode. @@ -12,6 +14,13 @@ public static class ParseUtil /// because we skip some checks that are not needed. /// public static bool TryParseLongFast(string value, out long result) + => TryParseLongFast(value.AsSpan(), out result); + + /// + /// A faster implementation than + /// because we skip some checks that are not needed. + /// + public static bool TryParseLongFast(ReadOnlySpan value, out long result) { result = 0; diff --git a/BencodeNET/PolyFillExtensions.cs b/BencodeNET/PolyFillExtensions.cs new file mode 100644 index 0000000000..1f0b6080fb --- /dev/null +++ b/BencodeNET/PolyFillExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Buffers; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace BencodeNET +{ +#if !NETCOREAPP3_0 + internal static class PolyFillExtensions + { + public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan chars, Span bytes) + { + fixed (char* charsPtr = &MemoryMarshal.GetReference(chars)) + fixed (byte* bytesPtr = &MemoryMarshal.GetReference(bytes)) + { + return encoding.GetBytes(charsPtr, chars.Length, bytesPtr, bytes.Length); + } + } + + public static unsafe string GetString(this Encoding encoding, ReadOnlySpan bytes) + { + fixed (byte* bytesPtr = &MemoryMarshal.GetReference(bytes)) + { + return encoding.GetString(bytesPtr, bytes.Length); + } + } + + public static void Write(this Stream stream, ReadOnlySpan buffer) + { + var array = ArrayPool.Shared.Rent(buffer.Length); + try + { + buffer.CopyTo(array); + stream.Write(array, 0, buffer.Length); + } + finally { ArrayPool.Shared.Return(array); } + } + } +#endif +} diff --git a/BencodeNET/Exceptions/InvalidTorrentException.cs b/BencodeNET/Torrents/InvalidTorrentException.cs similarity index 51% rename from BencodeNET/Exceptions/InvalidTorrentException.cs rename to BencodeNET/Torrents/InvalidTorrentException.cs index 04013d272d..0dd4bea072 100644 --- a/BencodeNET/Exceptions/InvalidTorrentException.cs +++ b/BencodeNET/Torrents/InvalidTorrentException.cs @@ -1,17 +1,12 @@ using System; -#if !NETSTANDARD -using System.Runtime.Serialization; -#endif +using BencodeNET.Exceptions; #pragma warning disable 1591 -namespace BencodeNET.Exceptions +namespace BencodeNET.Torrents { /// /// Represents parse errors when parsing torrents. /// -#if !NETSTANDARD - [Serializable] -#endif public class InvalidTorrentException : BencodeException { public string InvalidField { get; set; } @@ -32,22 +27,6 @@ public InvalidTorrentException(string message, string invalidField) public InvalidTorrentException(string message, Exception inner) : base(message, inner) { } - -#if !NETSTANDARD - protected InvalidTorrentException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - if (info == null) return; - InvalidField = info.GetString(nameof(InvalidField)); - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - - info.AddValue(nameof(InvalidField), InvalidField); - } -#endif } } #pragma warning restore 1591 \ No newline at end of file diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs index 25fbb209f0..0c7b075a15 100644 --- a/BencodeNET/Torrents/Torrent.cs +++ b/BencodeNET/Torrents/Torrent.cs @@ -1,12 +1,15 @@ -using BencodeNET.Objects; using System; using System.Collections.Generic; using System.IO; +using System.IO.Pipelines; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.Exceptions; using BencodeNET.IO; +using BencodeNET.Objects; namespace BencodeNET.Torrents { @@ -157,6 +160,7 @@ public virtual string DisplayName public virtual long PieceSize { get; set; } // TODO: Split into list of 20-byte hashes and rename to something appropriate? + /// /// A concatenation of all 20-byte SHA1 hash values (one for each piece). /// Use to get/set this value as a hex string instead. @@ -329,17 +333,35 @@ public virtual string GetMagnetLink(MagnetLinkOptions options = MagnetLinkOption return TorrentUtil.CreateMagnetLink(this, options); } + /// + public override int GetSizeInBytes() => ToBDictionary().GetSizeInBytes(); + /// /// Encodes the torrent and writes it to the stream. /// /// - /// - protected override void EncodeObject(BencodeStream stream) + protected override void EncodeObject(Stream stream) { var torrent = ToBDictionary(); torrent.EncodeTo(stream); } + /// + /// Encodes the torrent and writes it to the . + /// + protected override void EncodeObject(PipeWriter writer) + { + var torrent = ToBDictionary(); + torrent.EncodeTo(writer); + } + + /// + protected override ValueTask EncodeObjectAsync(PipeWriter writer, CancellationToken cancellationToken) + { + var torrent = ToBDictionary(); + return torrent.EncodeToAsync(writer, cancellationToken); + } + #pragma warning disable 1591 public static bool operator ==(Torrent first, Torrent second) { diff --git a/BencodeNET/Parsing/TorrentParser.cs b/BencodeNET/Torrents/TorrentParser.cs similarity index 80% rename from BencodeNET/Parsing/TorrentParser.cs rename to BencodeNET/Torrents/TorrentParser.cs index aff5760006..4cd051eec2 100644 --- a/BencodeNET/Parsing/TorrentParser.cs +++ b/BencodeNET/Torrents/TorrentParser.cs @@ -1,13 +1,14 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; -using BencodeNET.Exceptions; +using System.Threading; +using System.Threading.Tasks; using BencodeNET.IO; using BencodeNET.Objects; -using BencodeNET.Torrents; +using BencodeNET.Parsing; -namespace BencodeNET.Parsing +namespace BencodeNET.Torrents { /// /// A parser for torrent files. @@ -20,13 +21,31 @@ public class TorrentParser : BObjectParser /// public TorrentParserMode ParseMode { get; set; } + + /// + /// Creates an instance using a default . + /// + public TorrentParser() + : this(new BencodeParser()) + { + } + + /// + /// Creates an instance with the specified using a default . + /// + /// The parser used for parsing the torrent . + public TorrentParser(TorrentParserMode torrentParserMode) + : this(new BencodeParser(), torrentParserMode) + { + } + /// /// Creates an instance using the specified for parsing /// the torrent . /// /// The parser used for parsing the torrent . public TorrentParser(IBencodeParser bencodeParser) - : this(bencodeParser, TorrentParserMode.Strict) + : this(bencodeParser, TorrentParserMode.Tolerant) { } @@ -38,9 +57,7 @@ public TorrentParser(IBencodeParser bencodeParser) /// The parsing mode to use. public TorrentParser(IBencodeParser bencodeParser, TorrentParserMode torrentParserMode) { - if (bencodeParser == null) throw new ArgumentNullException(nameof(bencodeParser)); - - BencodeParser = bencodeParser; + BencodeParser = bencodeParser ?? throw new ArgumentNullException(nameof(bencodeParser)); ParseMode = torrentParserMode; } @@ -52,21 +69,33 @@ public TorrentParser(IBencodeParser bencodeParser, TorrentParserMode torrentPars /// /// The encoding used for parsing. /// - protected override Encoding Encoding => BencodeParser.Encoding; + public override Encoding Encoding => BencodeParser.Encoding; /// - /// Parses the next from the stream as a . + /// Parses the next from the reader as a . /// - /// The stream to parse from. + /// The reader to parse from. /// The parsed . - public override Torrent Parse(BencodeStream stream) + public override Torrent Parse(BencodeReader reader) { - var data = BencodeParser.Parse(stream); + var data = BencodeParser.Parse(reader); return CreateTorrent(data); } /// - /// Creates a torrrent by reading the relevant data from the . + /// Parses the next from the reader as a . + /// + /// The reader to parse from. + /// + /// The parsed . + public override async ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default) + { + var data = await BencodeParser.ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); + return CreateTorrent(data); + } + + /// + /// Creates a torrent by reading the relevant data from the . /// /// The torrent bencode data. /// A matching the input. @@ -318,6 +347,8 @@ private void FixEncodingInDictionary(BDictionary data, Encoding encoding) /// A list of list of trackers (announce URLs). protected virtual IList> ParseTrackers(BDictionary data, Encoding encoding) { + // Specification: http://bittorrent.org/beps/bep_0012.html + var trackerList = new List>(); var primary = new List(); trackerList.Add(primary); @@ -329,18 +360,31 @@ protected virtual IList> ParseTrackers(BDictionary data, Encoding primary.Add(announce); } - // Get the 'announce-list' list´s - var announceLists = data.Get(TorrentFields.AnnounceList)?.AsType() as IList; - if (announceLists?.Any() == true) + // Get the 'announce-list' list's + var announceLists = data.Get(TorrentFields.AnnounceList); + if (announceLists == null) + return trackerList; + + // According to the specification it should be a list of lists + if (announceLists.All(x => x is BList)) { - // Add the first list to the primary list and remove duplicates - primary.AddRange(announceLists.First().AsStrings(encoding)); - trackerList[0] = primary.Distinct().ToList(); + var lists = announceLists.AsType() as IList; + if (lists.Any()) + { + // Add the first list to the primary list and remove duplicates + primary.AddRange(lists.First().AsStrings(encoding)); + trackerList[0] = primary.Distinct().ToList(); - // Add the other lists to the lists of lists of announce urls - trackerList.AddRange( - announceLists.Skip(1) - .Select(x => x.AsStrings(encoding).ToList())); + // Add the other lists to the lists of lists of announce urls + trackerList.AddRange(lists.Skip(1).Select(x => x.AsStrings(encoding).ToList())); + } + } + // It's not following the specification, it's strings instead of lists + else if (ParseMode == TorrentParserMode.Tolerant && announceLists.All(x => x is BString)) + { + // Add them all to the first list + primary.AddRange(announceLists.AsStrings(encoding)); + trackerList[0] = primary.Distinct().ToList(); } return trackerList; diff --git a/BencodeNET/Parsing/TorrentParserMode.cs b/BencodeNET/Torrents/TorrentParserMode.cs similarity index 93% rename from BencodeNET/Parsing/TorrentParserMode.cs rename to BencodeNET/Torrents/TorrentParserMode.cs index a16bc02c68..9e0bec101a 100644 --- a/BencodeNET/Parsing/TorrentParserMode.cs +++ b/BencodeNET/Torrents/TorrentParserMode.cs @@ -1,4 +1,4 @@ -namespace BencodeNET.Parsing +namespace BencodeNET.Torrents { /// /// Determines how strict to be when parsing torrent files. diff --git a/BencodeNET/UtilityExtensions.cs b/BencodeNET/UtilityExtensions.cs index 9ee13e3e4c..61470c4d2f 100644 --- a/BencodeNET/UtilityExtensions.cs +++ b/BencodeNET/UtilityExtensions.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; +using System.IO.Pipelines; using System.Linq; using System.Text; -#if NETSTANDARD1_3 -using System; -using System.Reflection; -#endif - namespace BencodeNET { internal static class UtilityExtensions @@ -24,23 +21,143 @@ public static MemoryStream AsStream(this string str, Encoding encoding) public static TValue GetValueOrDefault(this IDictionary dictionary, TKey key) { - return dictionary.TryGetValue(key, out TValue value) ? value : default(TValue); + return dictionary.TryGetValue(key, out var value) ? value : default; + } + + public static IEnumerable Flatten(this IEnumerable> source) + { + return source.SelectMany(x => x); + } + + public static int DigitCount(this int value) => DigitCount((long) value); + + public static int DigitCount(this long value) + { + var sign = value < 0 ? 1 : 0; + + if (value == long.MinValue) + return 20; + + value = Math.Abs(value); + + if (value < 10) + return sign + 1; + if (value < 100) + return sign + 2; + if (value < 1000) + return sign + 3; + if (value < 10000) + return sign + 4; + if (value < 100000) + return sign + 5; + if (value < 1000000) + return sign + 6; + if (value < 10000000) + return sign + 7; + if (value < 100000000) + return sign + 8; + if (value < 1000000000) + return sign + 9; + if (value < 10000000000) + return sign + 10; + if (value < 100000000000) + return sign + 11; + if (value < 1000000000000) + return sign + 12; + if (value < 10000000000000) + return sign + 13; + if (value < 100000000000000) + return sign + 14; + if (value < 1000000000000000) + return sign + 15; + if (value < 10000000000000000) + return sign + 16; + if (value < 100000000000000000) + return sign + 17; + if (value < 1000000000000000000) + return sign + 18; + + return sign + 19; + } + + public static bool TrySetLength(this Stream stream, long length) + { + if (!stream.CanWrite || !stream.CanSeek) + return false; + + try + { + if (stream.Length >= length) + return false; + + stream.SetLength(length); + return true; + } + catch + { + return false; + } + } + + public static void Write(this Stream stream, int number) + { + Span buffer = stackalloc byte[11]; + var bytesRead = Encoding.ASCII.GetBytes(number.ToString().AsSpan(), buffer); + stream.Write(buffer.Slice(0, bytesRead)); + } + + public static void Write(this Stream stream, long number) + { + Span buffer = stackalloc byte[20]; + var bytesRead = Encoding.ASCII.GetBytes(number.ToString().AsSpan(), buffer); + stream.Write(buffer.Slice(0, bytesRead)); } public static void Write(this Stream stream, char c) { - stream.WriteByte((byte)c); + stream.WriteByte((byte) c); } - public static IEnumerable Flatten(this IEnumerable> source) + public static void WriteChar(this PipeWriter writer, char c) { - return source.SelectMany(x => x); + writer.GetSpan(1)[0] = (byte) c; + writer.Advance(1); + } + + public static void WriteCharAt(this Span bytes, char c, int index) + { + bytes[index] = (byte) c; + } + +#if NETCOREAPP + public static string AsString(this ReadOnlySpan chars) + { + return new string(chars); + } + + public static string AsString(this Span chars) + { + return new string(chars); + } + + public static string AsString(this Memory chars) + { + return new string(chars.Span); + } +#else + public static string AsString(this ReadOnlySpan chars) + { + return new string(chars.ToArray()); + } + + public static string AsString(this Span chars) + { + return new string(chars.ToArray()); } -#if NETSTANDARD1_3 - public static bool IsAssignableFrom(this Type type, Type otherType) + public static string AsString(this Memory chars) { - return type.GetTypeInfo().IsAssignableFrom(otherType.GetTypeInfo()); + return new string(chars.ToArray()); } #endif } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..ff236e9da7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,135 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + + +## [3.0.0] +There is a few changes to the public API, but most people shouldn't be affected by this unless they have extended/overriden functionality. +Basic usage should not see any or only minor changes compared to v2.3.0. + +Implemented async support for parsing/encoding. + +Added build targets for .NET Core 2.1 and .NET Core 3.0 to take advantage of performance improvements +in `Stream` and `Encoding` APIs for .NET Core 2.1 or later. + +Rewrite of internal parsing for better performance, taking advantage of new `Span`/`Memory` +types - faster parsing and less memory allocation. + +Removed support for .NET Framework 4.5 and .NET Standard 1.3. +Lowest supported versions are now .NET Framework 4.6.1 (4.7.2 highly recommended) and .NET Standard 2.0. + + +### Added +- Implemented parsing/encoding using `PipeReader`/`PipeWriter` +- Added `BencodeReader` as replacement for `BencodeStream` +- Added `IBObject.GetSizeInBytes()` method, returning the size of the object in number of bytes. + +### Changed +- Improved parsing/encoding performance +- Reduced memory allocation on parsing/encoding +- Made `BString`, `BNumber`, `BList` and `BDictionary` classes `sealed` +- Made parse methods of `BencodeParser` virtual so it can be overriden if needed by anyone +- Constructor `BString(IEnumerable bytes, Encoding encoding = null)` changed to `BString(byte[] bytes, Encoding encoding = null)` +- Exposed value type of `BString` changed from `IReadOnlyList` (`byte[]` internally) to `ReadOnlyMemory` +- Removed parse method overloads on `IBencodeParser` and added matching extension methods instead +- Removed encode method overloads on `IBObject` and added matching extension methods instead +- Torrent parse mode now default to `TorrentParserMode.Tolerant` instead of `TorrentParserMode.Strict` +- Torrent related classes moved to `BencodeNET.Torrents` namespace + +### Removed +- Removed `BencodeStream` and replaced with `BencodeReader` +- Dropped .NET Standard 1.3 support; .NET Standard 2.0 is now lowest supported version +- Dropped .NET Framework 4.5 support; .NET Framework 4.6.1 is now lowest supported version (but 4.7.2 is highly recommended) +- Removed most constructors on `BencodeParser` leaving only `BencodeParser(Encoding encoding = null)` and + added `BencodeParser.Encoding` property to enable changing encoding. Parsers can still be added/replaced/removed + through `BencodeParser.Parsers` property. + +### Fixed +- Parsing from non-seekable `Stream`s is now possible +- Fixed issue parsing torrent files with non-standard 'announce-list' (#39) + + +## [2.3.0] - 2019-02-11 +### Added +- Added `BNumber` casting operators to `int?` and `long?` + + +## [2.2.9] - 2017-08-05 +### Added +- Added tolerant parse mode for torrents, which skips validation +- Save original info hash when parsing torrent + +### Changed +- Try to guess and handle timestamps in milliseconds in 'created' field + +### Fixed +- Handle invalid unix timestamps in 'created' field + + +## [2.2.2] - 2017-04-03 +### Added +- `BList.AsNumbers()` method +- `Torrent.PiecesAsHexString` property +- Attempt to use .torrent file encoding when parsing torrent itself + +### Changed +- `Torrent.Pieces` type changed to `byte[]` + +### Fixed +- `Torrent.Pieces` property + + +## [2.1.0] - 2016-10-07 +API has been more or less completely rewritten for better use with dependency injectiom +and generally better usability; albeit a bit more complex. + +### Added +- .NET Standard support + + +## [1.3.1] - 2016-06-27 +### Added +- Some XML documentation (intellisense) + +### Changed +- Better handling of `CreationDate` in torrent files + + +## [1.2.1] - 2015-09-26 +### Changed +- Further performance improvements when decoding strings and numbers (up to ~30% for a standard torrent file) +- XML documentation now included in nuget package + + +## [1.2.0] - 2015-09-21 +### Changed +- Big performance improvements when decoding + +### Removed +- BencodeStream.BaseStream property has been removed + + +## [1.1.0] - 2015-09-21 +### Added +- Torrent file abstractions including method to calculate info hash of a torrent file + + +## [1.0.0] - 2015-09-19 + + +[Unreleased]: ../../compare/v3.0.0...HEAD +[3.0.0]: ../../compare/v2.3.0...v3.0.0 +[2.3.0]: ../../compare/v2.2.9...v2.3.0 +[2.2.9]: ../../compare/v2.2.0...v2.2.9 +[2.2.2]: ../../compare/v2.1.0...v2.2.2 +[2.1.0]: ../../compare/v1.3.1...v2.1.0 +[1.3.1]: ../../compare/v1.3.0...v1.3.1 +[1.3.0]: ../../compare/v1.2.1...v1.3.0 +[1.2.1]: ../../compare/v1.2.0...v1.2.1 +[1.2.0]: ../../compare/v1.1.0...v1.2.0 +[1.1.0]: ../../compare/v1.0.0...v1.1.0 +[1.0.0]: ../../releases/tag/v1.0.0 \ No newline at end of file