├── .gitattributes
├── .github
└── FUNDING.yml
├── .gitignore
├── Assets
├── icon.png
├── icon_128.png
└── icon_64.png
├── BencodeNET.Tests
├── AutoFixture
│ ├── RepeatCountAttribute.cs
│ └── RepeatCountCustomization.cs
├── AutoMockedDataAttribute.cs
├── BencodeNET.Tests.csproj
├── Extensions.cs
├── Files
│ └── ubuntu-14.10-desktop-amd64.iso.torrent
├── IO
│ ├── BencodeReaderTests.cs
│ └── PipeBencodeReaderTests.cs
├── LengthNotSupportedStream.cs
├── Objects
│ ├── BDictionaryTests.cs
│ ├── BListTests.cs
│ ├── BNumberTests.cs
│ └── BStringTests.cs
├── Parsing
│ ├── BDictionaryParserTests.Async.cs
│ ├── BDictionaryParserTests.cs
│ ├── BListParserTests.Async.cs
│ ├── BListParserTests.cs
│ ├── BNumberParserTests.Async.cs
│ ├── BNumberParserTests.cs
│ ├── BObjectParserListTests.cs
│ ├── BObjectParserTests.cs
│ ├── BStringParserTests.Async.cs
│ ├── BStringParserTests.cs
│ ├── BencodeParserTests.cs
│ └── ParseUtilTests.cs
└── Torrents
│ ├── MultiFileInfoTests.cs
│ ├── TorrentParserTests.cs
│ ├── TorrentTests.cs
│ └── TorrentUtilTests.cs
├── BencodeNET.sln
├── BencodeNET
├── BencodeNET.csproj
├── BencodeNET.ruleset
├── Exceptions
│ ├── BencodeException.cs
│ ├── InvalidBencodeException.cs
│ └── UnsupportedBencodeException.cs
├── IO
│ ├── BencodeReader.cs
│ └── PipeBencodeReader.cs
├── Objects
│ ├── BDictionary.cs
│ ├── BList.cs
│ ├── BNumber.cs
│ ├── BObject.cs
│ ├── BObjectExtensions.cs
│ ├── BString.cs
│ └── IBObject.cs
├── Parsing
│ ├── BDictionaryParser.cs
│ ├── BListParser.cs
│ ├── BNumberParser.cs
│ ├── BObjectParser.cs
│ ├── BObjectParserExtensions.cs
│ ├── BObjectParserList.cs
│ ├── BStringParser.cs
│ ├── BencodeParser.cs
│ ├── BencodeParserExtensions.cs
│ ├── IBObjectParser.cs
│ ├── IBencodeParser.cs
│ └── ParseUtil.cs
├── Torrents
│ ├── InvalidTorrentException.cs
│ ├── MagnetLinkOptions.cs
│ ├── MultiFileInfo.cs
│ ├── MultiFileInfoList.cs
│ ├── SingleFileInfo.cs
│ ├── Torrent.cs
│ ├── TorrentFields.cs
│ ├── TorrentFileMode.cs
│ ├── TorrentParser.cs
│ ├── TorrentParserMode.cs
│ └── TorrentUtil.cs
└── UtilityExtensions.cs
├── CHANGELOG.md
├── GitVersion.yml
├── LICENSE.md
├── README.md
└── azure-pipelines.yml
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | #patreon: # Replace with a single Patreon username
5 | #open_collective: # Replace with a single Open Collective username
6 | #ko_fi: # Replace with a single Ko-fi username
7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | #liberapay: # Replace with a single Liberapay username
10 | #issuehunt: # Replace with a single IssueHunt username
11 | #otechie: # Replace with a single Otechie username
12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
14 | ko_fi: krusen
15 | custom: ['https://www.buymeacoffee.com/UCkS2tw']
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | tools/**
279 | !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 |
290 |
291 |
292 |
293 | /coverage.xml
294 | BencodeNET.Tests/xunit.runner.json
295 |
--------------------------------------------------------------------------------
/Assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krusen/BencodeNET/161e817295b6938237f22a19d1be28ea1944ee62/Assets/icon.png
--------------------------------------------------------------------------------
/Assets/icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krusen/BencodeNET/161e817295b6938237f22a19d1be28ea1944ee62/Assets/icon_128.png
--------------------------------------------------------------------------------
/Assets/icon_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krusen/BencodeNET/161e817295b6938237f22a19d1be28ea1944ee62/Assets/icon_64.png
--------------------------------------------------------------------------------
/BencodeNET.Tests/AutoFixture/RepeatCountAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using AutoFixture;
4 | using AutoFixture.Xunit2;
5 |
6 | namespace BencodeNET.Tests.AutoFixture
7 | {
8 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
9 | public class RepeatCountAttribute : CustomizeAttribute
10 | {
11 | private int Count { get; }
12 |
13 | public RepeatCountAttribute(int count)
14 | {
15 | Count = count;
16 | }
17 |
18 | public override ICustomization GetCustomization(ParameterInfo parameter)
19 | {
20 | return new RepeatCountCustomization(Count);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/AutoFixture/RepeatCountCustomization.cs:
--------------------------------------------------------------------------------
1 | using AutoFixture;
2 |
3 | namespace BencodeNET.Tests.AutoFixture
4 | {
5 | public class RepeatCountCustomization : ICustomization
6 | {
7 | private int Count { get; }
8 |
9 | public RepeatCountCustomization(int count)
10 | {
11 | Count = count;
12 | }
13 |
14 | public void Customize(IFixture fixture)
15 | {
16 | fixture.RepeatCount = Count;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/AutoMockedDataAttribute.cs:
--------------------------------------------------------------------------------
1 | using AutoFixture;
2 | using AutoFixture.AutoNSubstitute;
3 | using AutoFixture.Xunit2;
4 | using Xunit;
5 |
6 | namespace BencodeNET.Tests
7 | {
8 | public class AutoMockedDataAttribute : CompositeDataAttribute
9 | {
10 | public AutoMockedDataAttribute()
11 | : this(new BaseAutoMockedDataAttribute())
12 | { }
13 |
14 | public AutoMockedDataAttribute(params object[] values)
15 | : this(new BaseAutoMockedDataAttribute(), values)
16 | { }
17 |
18 | private AutoMockedDataAttribute(BaseAutoMockedDataAttribute baseAutoDataAttribute, params object[] values)
19 | : base(new InlineDataAttribute(values), baseAutoDataAttribute)
20 | { }
21 |
22 | private class BaseAutoMockedDataAttribute : AutoDataAttribute
23 | {
24 | public BaseAutoMockedDataAttribute()
25 | : base(Configure)
26 | {
27 | }
28 |
29 | private static IFixture Configure()
30 | {
31 | return new Fixture().Customize(new AutoNSubstituteCustomization { ConfigureMembers = true });
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/BencodeNET.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | 11
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | all
24 | runtime; build; native; contentfiles; analyzers; buildtransitive
25 |
26 |
27 |
28 |
29 | all
30 | runtime; build; native; contentfiles; analyzers; buildtransitive
31 |
32 |
33 |
34 |
35 | all
36 | runtime; build; native; contentfiles; analyzers; buildtransitive
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Pipelines;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using BencodeNET.IO;
7 | using BencodeNET.Objects;
8 | using BencodeNET.Parsing;
9 | using NSubstitute.Core;
10 |
11 | namespace BencodeNET.Tests
12 | {
13 | internal static class Extensions
14 | {
15 | internal static string AsString(this Stream stream)
16 | {
17 | stream.Position = 0;
18 | var sr = new StreamReader(stream, Encoding.UTF8);
19 | return sr.ReadToEnd();
20 | }
21 |
22 | internal static string AsString(this Stream stream, Encoding encoding)
23 | {
24 | stream.Position = 0;
25 | var sr = new StreamReader(stream, encoding);
26 | return sr.ReadToEnd();
27 | }
28 |
29 | internal static void SkipBytes(this BencodeReader reader, int length)
30 | {
31 | reader.Read(new byte[length]);
32 | }
33 |
34 | internal static Task SkipBytesAsync(this PipeBencodeReader reader, int length)
35 | {
36 | return reader.ReadAsync(new byte[length]).AsTask();
37 | }
38 |
39 | internal static ConfiguredCall AndSkipsAhead(this ConfiguredCall call, int length)
40 | {
41 | return call.AndDoes(x => x.Arg().SkipBytes(length));
42 | }
43 |
44 | internal static ConfiguredCall AndSkipsAheadAsync(this ConfiguredCall call, int length)
45 | {
46 | return call.AndDoes(async x => await x.Arg().SkipBytesAsync(length));
47 | }
48 |
49 | internal static async ValueTask ParseStringAsync(this IBObjectParser parser, string bencodedString)
50 | {
51 | var bytes = Encoding.UTF8.GetBytes(bencodedString).AsMemory();
52 | var (reader, writer) = new Pipe();
53 | await writer.WriteAsync(bytes);
54 | writer.Complete();
55 | return await parser.ParseAsync(reader);
56 | }
57 |
58 | internal static async ValueTask ParseStringAsync(this IBObjectParser parser, string bencodedString) where T : IBObject
59 | {
60 | var bytes = Encoding.UTF8.GetBytes(bencodedString).AsMemory();
61 | var (reader, writer) = new Pipe();
62 | await writer.WriteAsync(bytes);
63 | writer.Complete();
64 | return await parser.ParseAsync(reader);
65 | }
66 |
67 | internal static void Deconstruct(this Pipe pipe, out PipeReader reader, out PipeWriter writer)
68 | {
69 | reader = pipe.Reader;
70 | writer = pipe.Writer;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Files/ubuntu-14.10-desktop-amd64.iso.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Krusen/BencodeNET/161e817295b6938237f22a19d1be28ea1944ee62/BencodeNET.Tests/Files/ubuntu-14.10-desktop-amd64.iso.torrent
--------------------------------------------------------------------------------
/BencodeNET.Tests/LengthNotSupportedStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 |
5 | namespace BencodeNET.Tests
6 | {
7 | internal class LengthNotSupportedStream : MemoryStream
8 | {
9 | public LengthNotSupportedStream(string str)
10 | : this(str, Encoding.UTF8)
11 | {
12 | }
13 |
14 | public LengthNotSupportedStream(string str, Encoding encoding)
15 | : base(encoding.GetBytes(str))
16 | {
17 | }
18 |
19 | public override long Length => throw new NotSupportedException();
20 | }
21 | }
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BDictionaryParserTests.Async.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BencodeNET.Exceptions;
4 | using BencodeNET.IO;
5 | using BencodeNET.Objects;
6 | using BencodeNET.Parsing;
7 | using FluentAssertions;
8 | using NSubstitute;
9 | using NSubstitute.ExceptionExtensions;
10 | using Xunit;
11 |
12 | namespace BencodeNET.Tests.Parsing
13 | {
14 | public partial class BDictionaryParserTests
15 | {
16 | [Theory]
17 | [AutoMockedData("d4:spam3:egge")]
18 | public async Task CanParseSimpleAsync(string bencode, IBencodeParser bparser)
19 | {
20 | // Arrange
21 | var key = new BString("key");
22 | var value = new BString("value");
23 |
24 | bparser.ParseAsync(Arg.Any())
25 | .Returns(key);
26 |
27 | bparser.ParseAsync(Arg.Any())
28 | .Returns(value)
29 | .AndSkipsAheadAsync(bencode.Length - 2);
30 |
31 | // Act
32 | var parser = new BDictionaryParser(bparser);
33 | var bdictionary = await parser.ParseStringAsync(bencode);
34 |
35 | // Assert
36 | bdictionary.Count.Should().Be(1);
37 | bdictionary.Should().ContainKey(key);
38 | bdictionary[key].Should().BeSameAs(value);
39 | }
40 |
41 | [Theory]
42 | [AutoMockedData("de")]
43 | public async Task CanParseEmptyDictionaryAsync(string bencode, IBencodeParser bparser)
44 | {
45 | var parser = new BDictionaryParser(bparser);
46 | var bdictionary = await parser.ParseStringAsync(bencode);
47 |
48 | bdictionary.Count.Should().Be(0);
49 | }
50 |
51 | [Theory]
52 | [AutoMockedData("")]
53 | [AutoMockedData("d")]
54 | public async Task BelowMinimumLength2_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser)
55 | {
56 | var parser = new BDictionaryParser(bparser);
57 | var action = async () => await parser.ParseStringAsync(bencode);
58 |
59 | await action.Should().ThrowAsync>().WithMessage("*reached end of stream*");
60 | }
61 |
62 | [Theory]
63 | [AutoMockedData("ade")]
64 | [AutoMockedData(":de")]
65 | [AutoMockedData("-de")]
66 | [AutoMockedData("1de")]
67 | public async Task InvalidFirstChar_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser)
68 | {
69 | var parser = new BDictionaryParser(bparser);
70 | var action = async () => await parser.ParseStringAsync(bencode);
71 |
72 | await action.Should().ThrowAsync>().WithMessage("*Unexpected character*");
73 | }
74 |
75 | [Theory]
76 | [AutoMockedData("da")]
77 | [AutoMockedData("d4:spam3:egg")]
78 | [AutoMockedData("d ")]
79 | public async Task MissingEndChar_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser, BString someKey, IBObject someValue)
80 | {
81 | // Arrange
82 | bparser.ParseAsync(Arg.Any())
83 | .Returns(someKey);
84 |
85 | bparser.ParseAsync(Arg.Any())
86 | .Returns(someValue)
87 | .AndSkipsAheadAsync(bencode.Length - 1);
88 |
89 | // Act
90 | var parser = new BDictionaryParser(bparser);
91 | var action = async () => await parser.ParseStringAsync(bencode);
92 |
93 | // Assert
94 | await action.Should().ThrowAsync>().WithMessage("*Missing end character of object*");
95 | }
96 |
97 | [Theory]
98 | [AutoMockedData]
99 | public async Task InvalidKey_ThrowsInvalidBencodeExceptionAsync(IBencodeParser bparser)
100 | {
101 | bparser.ParseAsync(Arg.Any()).Throws();
102 |
103 | var parser = new BDictionaryParser(bparser);
104 |
105 | var action = async () => await parser.ParseStringAsync("di42ee");
106 |
107 | await action.Should().ThrowAsync>().WithMessage("*Could not parse dictionary key*");
108 | }
109 |
110 | [Theory]
111 | [AutoMockedData]
112 | public async Task InvalidValue_ThrowsInvalidBencodeExceptionAsync(IBencodeParser bparser, BString someKey)
113 | {
114 | bparser.ParseAsync(Arg.Any()).Returns(someKey);
115 | bparser.ParseAsync(Arg.Any()).Throws();
116 |
117 | var parser = new BDictionaryParser(bparser);
118 |
119 | var action = async () => await parser.ParseStringAsync("di42ee");
120 |
121 | await action.Should().ThrowAsync>().WithMessage("*Could not parse dictionary value*");
122 | }
123 |
124 | [Theory]
125 | [AutoMockedData]
126 | public async Task DuplicateKey_ThrowsInvalidBencodeExceptionAsync(IBencodeParser bparser, BString someKey, BString someValue)
127 | {
128 | bparser.ParseAsync(Arg.Any()).Returns(someKey, someKey);
129 | bparser.ParseAsync(Arg.Any()).Returns(someValue);
130 |
131 | var parser = new BDictionaryParser(bparser);
132 |
133 | var action = async () => await parser.ParseStringAsync("di42ee");
134 |
135 | await action.Should().ThrowAsync>().WithMessage("*The dictionary already contains the key*");
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BDictionaryParserTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BencodeNET.Exceptions;
3 | using BencodeNET.IO;
4 | using BencodeNET.Objects;
5 | using BencodeNET.Parsing;
6 | using FluentAssertions;
7 | using NSubstitute;
8 | using NSubstitute.ExceptionExtensions;
9 | using Xunit;
10 |
11 | namespace BencodeNET.Tests.Parsing
12 | {
13 | public partial class BDictionaryParserTests
14 | {
15 | [Theory]
16 | [AutoMockedData("d4:spam3:egge")]
17 | public void CanParseSimple(string bencode, IBencodeParser bparser)
18 | {
19 | // Arange
20 | var key = new BString("key");
21 | var value = new BString("value");
22 |
23 | bparser.Parse(Arg.Any())
24 | .Returns(key);
25 |
26 | bparser.Parse(Arg.Any())
27 | .Returns(value)
28 | .AndSkipsAhead(bencode.Length - 2);
29 |
30 | // Act
31 | var parser = new BDictionaryParser(bparser);
32 | var bdictionary = parser.ParseString(bencode);
33 |
34 | // Assert
35 | bdictionary.Count.Should().Be(1);
36 | bdictionary.Should().ContainKey(key);
37 | bdictionary[key].Should().BeSameAs(value);
38 | }
39 |
40 | [Theory]
41 | [AutoMockedData("de")]
42 | public void CanParseEmptyDictionary(string bencode, IBencodeParser bparser)
43 | {
44 | var parser = new BDictionaryParser(bparser);
45 | var bdictionary = parser.ParseString(bencode);
46 |
47 | bdictionary.Count.Should().Be(0);
48 | }
49 |
50 | [Theory]
51 | [AutoMockedData("")]
52 | [AutoMockedData("d")]
53 | public void BelowMinimumLength2_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser)
54 | {
55 | var parser = new BDictionaryParser(bparser);
56 | Action action = () => parser.ParseString(bencode);
57 |
58 | action.Should().Throw>().WithMessage("*Invalid length*");
59 | }
60 |
61 | [Theory]
62 | [AutoMockedData("")]
63 | [AutoMockedData("d")]
64 | public void BelowMinimumLength2_WhenStreamLengthNotSupported_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser)
65 | {
66 | var stream = new LengthNotSupportedStream(bencode);
67 |
68 | var parser = new BDictionaryParser(bparser);
69 | Action action = () => parser.Parse(stream);
70 |
71 | action.Should().Throw>();
72 | }
73 |
74 | [Theory]
75 | [AutoMockedData("ade")]
76 | [AutoMockedData(":de")]
77 | [AutoMockedData("-de")]
78 | [AutoMockedData("1de")]
79 | public void InvalidFirstChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser)
80 | {
81 | var parser = new BDictionaryParser(bparser);
82 | Action action = () => parser.ParseString(bencode);
83 |
84 | action.Should().Throw>().WithMessage("*Unexpected character*");
85 | }
86 |
87 | [Theory]
88 | [AutoMockedData("da")]
89 | [AutoMockedData("d4:spam3:egg")]
90 | [AutoMockedData("d ")]
91 | public void MissingEndChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser, BString someKey, IBObject someValue)
92 | {
93 | // Arrange
94 | bparser.Parse(Arg.Any())
95 | .Returns(someKey);
96 |
97 | bparser.Parse(Arg.Any())
98 | .Returns(someValue)
99 | .AndSkipsAhead(bencode.Length - 1);
100 |
101 | // Act
102 | var parser = new BDictionaryParser(bparser);
103 | Action action = () => parser.ParseString(bencode);
104 |
105 | // Assert
106 | action.Should().Throw>().WithMessage("*Missing end character of object*");
107 | }
108 |
109 | [Theory]
110 | [AutoMockedData]
111 | public void InvalidKey_ThrowsInvalidBencodeException(IBencodeParser bparser)
112 | {
113 | bparser.Parse(Arg.Any()).Throws();
114 |
115 | var parser = new BDictionaryParser(bparser);
116 |
117 | Action action = () => parser.ParseString("di42ee");
118 |
119 | action.Should().Throw>().WithMessage("*Could not parse dictionary key*");
120 | }
121 |
122 | [Theory]
123 | [AutoMockedData]
124 | public void InvalidValue_ThrowsInvalidBencodeException(IBencodeParser bparser, BString someKey)
125 | {
126 | bparser.Parse(Arg.Any()).Returns(someKey);
127 | bparser.Parse(Arg.Any()).Throws();
128 |
129 | var parser = new BDictionaryParser(bparser);
130 |
131 | Action action = () => parser.ParseString("di42ee");
132 |
133 | action.Should().Throw>().WithMessage("*Could not parse dictionary value*");
134 | }
135 |
136 | [Theory]
137 | [AutoMockedData]
138 | public void DuplicateKey_ThrowsInvalidBencodeException(IBencodeParser bparser, BString someKey, BString someValue)
139 | {
140 | bparser.Parse(Arg.Any()).Returns(someKey, someKey);
141 | bparser.Parse(Arg.Any()).Returns(someValue);
142 |
143 | var parser = new BDictionaryParser(bparser);
144 |
145 | Action action = () => parser.ParseString("di42ee");
146 |
147 | action.Should().Throw>().WithMessage("*The dictionary already contains the key*");
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BListParserTests.Async.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BencodeNET.Exceptions;
4 | using BencodeNET.IO;
5 | using BencodeNET.Objects;
6 | using BencodeNET.Parsing;
7 | using FluentAssertions;
8 | using NSubstitute;
9 | using Xunit;
10 |
11 | namespace BencodeNET.Tests.Parsing
12 | {
13 | public partial class BListParserTests
14 | {
15 | [Theory]
16 | [AutoMockedData("l-something-e")]
17 | [AutoMockedData("l4:spame")]
18 | [AutoMockedData("l4:spami42ee")]
19 | public async Task CanParseSimpleAsync(string bencode, IBencodeParser bparser)
20 | {
21 | // Arrange
22 | var bstring = new BString("test");
23 | bparser.ParseAsync(Arg.Any())
24 | .Returns(bstring)
25 | .AndSkipsAheadAsync(bencode.Length - 2);
26 |
27 | // Act
28 | var parser = new BListParser(bparser);
29 | var blist = await parser.ParseStringAsync(bencode);
30 |
31 | // Assert
32 | blist.Count.Should().Be(1);
33 | blist[0].Should().BeOfType();
34 | blist[0].Should().BeSameAs(bstring);
35 | await bparser.Received(1).ParseAsync(Arg.Any());
36 | }
37 |
38 | [Theory]
39 | [AutoMockedData("le")]
40 | public async Task CanParseEmptyListAsync(string bencode, IBencodeParser bparser)
41 | {
42 | var parser = new BListParser(bparser);
43 | var blist = await parser.ParseStringAsync(bencode);
44 |
45 | blist.Count.Should().Be(0);
46 | await bparser.DidNotReceive().ParseAsync(Arg.Any());
47 | }
48 |
49 | [Theory]
50 | [AutoMockedData("")]
51 | [AutoMockedData("l")]
52 | public async Task BelowMinimumLength2_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser)
53 | {
54 | var parser = new BListParser(bparser);
55 | var action = async () => await parser.ParseStringAsync(bencode);
56 |
57 | await action.Should().ThrowAsync>().WithMessage("*reached end of stream*");
58 | }
59 |
60 | [Theory]
61 | [AutoMockedData("4e")]
62 | [AutoMockedData("ae")]
63 | [AutoMockedData(":e")]
64 | [AutoMockedData("-e")]
65 | [AutoMockedData(".e")]
66 | [AutoMockedData("ee")]
67 | public async Task InvalidFirstChar_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser)
68 | {
69 | var parser = new BListParser(bparser);
70 | var action = async () => await parser.ParseStringAsync(bencode);
71 |
72 | await action.Should().ThrowAsync>().WithMessage("*Unexpected character*");
73 | }
74 |
75 | [Theory]
76 | [AutoMockedData("l4:spam")]
77 | [AutoMockedData("l ")]
78 | [AutoMockedData("l:")]
79 | public async Task MissingEndChar_ThrowsInvalidBencodeExceptionAsync(string bencode, IBencodeParser bparser, IBObject something)
80 | {
81 | // Arrange
82 | bparser.ParseAsync(Arg.Any())
83 | .Returns(something)
84 | .AndSkipsAheadAsync(bencode.Length - 1);
85 |
86 | // Act
87 | var parser = new BListParser(bparser);
88 | var action = async () => await parser.ParseStringAsync(bencode);
89 |
90 | // Assert
91 | await action.Should().ThrowAsync>().WithMessage("*Missing end character of object*");
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BListParserTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BencodeNET.Exceptions;
3 | using BencodeNET.IO;
4 | using BencodeNET.Objects;
5 | using BencodeNET.Parsing;
6 | using FluentAssertions;
7 | using NSubstitute;
8 | using Xunit;
9 |
10 | namespace BencodeNET.Tests.Parsing
11 | {
12 | public partial class BListParserTests
13 | {
14 | [Theory]
15 | [AutoMockedData("l-something-e")]
16 | [AutoMockedData("l4:spame")]
17 | [AutoMockedData("l4:spami42ee")]
18 | public void CanParseSimple(string bencode, IBencodeParser bparser)
19 | {
20 | // Arrange
21 | var bstring = new BString("test");
22 | bparser.Parse(Arg.Any())
23 | .Returns(bstring)
24 | .AndSkipsAhead(bencode.Length - 2);
25 |
26 | // Act
27 | var parser = new BListParser(bparser);
28 | var blist = parser.ParseString(bencode);
29 |
30 | // Assert
31 | blist.Count.Should().Be(1);
32 | blist[0].Should().BeOfType();
33 | blist[0].Should().BeSameAs(bstring);
34 | bparser.Received(1).Parse(Arg.Any());
35 | }
36 |
37 | [Theory]
38 | [AutoMockedData("le")]
39 | public void CanParseEmptyList(string bencode, IBencodeParser bparser)
40 | {
41 | var parser = new BListParser(bparser);
42 | var blist = parser.ParseString(bencode);
43 |
44 | blist.Count.Should().Be(0);
45 | bparser.DidNotReceive().Parse(Arg.Any());
46 | }
47 |
48 | [Theory]
49 | [AutoMockedData("")]
50 | [AutoMockedData("l")]
51 | public void BelowMinimumLength2_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser)
52 | {
53 | var parser = new BListParser(bparser);
54 | Action action = () => parser.ParseString(bencode);
55 |
56 | action.Should().Throw>().WithMessage("*Invalid length*");
57 | }
58 |
59 | [Theory]
60 | [AutoMockedData("4")]
61 | [AutoMockedData("a")]
62 | [AutoMockedData(":")]
63 | [AutoMockedData("-")]
64 | [AutoMockedData(".")]
65 | [AutoMockedData("e")]
66 | public void BelowMinimumLength2_WhenStreamLengthNotSupported_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser)
67 | {
68 | var stream = new LengthNotSupportedStream(bencode);
69 |
70 | var parser = new BListParser(bparser);
71 | Action action = () => parser.Parse(stream);
72 |
73 | action.Should().Throw>().WithMessage("*Unexpected character*");
74 | }
75 |
76 | [Theory]
77 | [AutoMockedData("4e")]
78 | [AutoMockedData("ae")]
79 | [AutoMockedData(":e")]
80 | [AutoMockedData("-e")]
81 | [AutoMockedData(".e")]
82 | [AutoMockedData("ee")]
83 | public void InvalidFirstChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser)
84 | {
85 | var parser = new BListParser(bparser);
86 | Action action = () => parser.ParseString(bencode);
87 |
88 | action.Should().Throw>().WithMessage("*Unexpected character*");
89 | }
90 |
91 | [Theory]
92 | [AutoMockedData("l4:spam")]
93 | [AutoMockedData("l ")]
94 | [AutoMockedData("l:")]
95 | public void MissingEndChar_ThrowsInvalidBencodeException(string bencode, IBencodeParser bparser, IBObject something)
96 | {
97 | // Arrange
98 | bparser.Parse(Arg.Any())
99 | .Returns(something)
100 | .AndSkipsAhead(bencode.Length - 1);
101 |
102 | // Act
103 | var parser = new BListParser(bparser);
104 | Action action = () => parser.ParseString(bencode);
105 |
106 | // Assert
107 | action.Should().Throw>().WithMessage("*Missing end character of object*");
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BNumberParserTests.Async.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using BencodeNET.Exceptions;
4 | using BencodeNET.Objects;
5 | using BencodeNET.Parsing;
6 | using FluentAssertions;
7 | using Xunit;
8 |
9 | namespace BencodeNET.Tests.Parsing
10 | {
11 | public partial class BNumberParserTests
12 | {
13 | [Theory]
14 | [InlineData("i1e", 1)]
15 | [InlineData("i2e", 2)]
16 | [InlineData("i3e", 3)]
17 | [InlineData("i42e", 42)]
18 | [InlineData("i100e", 100)]
19 | [InlineData("i1234567890e", 1234567890)]
20 | public async Task CanParsePositiveAsync(string bencode, int value)
21 | {
22 | var bnumber = await Parser.ParseStringAsync(bencode);
23 | bnumber.Should().Be(value);
24 | }
25 |
26 | [Fact]
27 | public async Task CanParseZeroAsync()
28 | {
29 | var bnumber = await Parser.ParseStringAsync("i0e");
30 | bnumber.Should().Be(0);
31 | }
32 |
33 | [Theory]
34 | [InlineData("i-1e", -1)]
35 | [InlineData("i-2e", -2)]
36 | [InlineData("i-3e", -3)]
37 | [InlineData("i-42e", -42)]
38 | [InlineData("i-100e", -100)]
39 | [InlineData("i-1234567890e", -1234567890)]
40 | public async Task CanParseNegativeAsync(string bencode, int value)
41 | {
42 | var bnumber = await Parser.ParseStringAsync(bencode);
43 | bnumber.Should().Be(value);
44 | }
45 |
46 | [Theory]
47 | [InlineData("i9223372036854775807e", 9223372036854775807)]
48 | [InlineData("i-9223372036854775808e", -9223372036854775808)]
49 | public async Task CanParseInt64Async(string bencode, long value)
50 | {
51 | var bnumber = await Parser.ParseStringAsync(bencode);
52 | bnumber.Should().Be(value);
53 | }
54 |
55 | [Theory]
56 | [InlineData("i01e")]
57 | [InlineData("i012e")]
58 | [InlineData("i01234567890e")]
59 | [InlineData("i00001e")]
60 | public async Task LeadingZeros_ThrowsInvalidBencodeExceptionAsync(string bencode)
61 | {
62 | var action = async () => await Parser.ParseStringAsync(bencode);
63 | (await action.Should().ThrowAsync>())
64 | .WithMessage("*Leading '0's are not valid.*")
65 | .Which.StreamPosition.Should().Be(0);
66 | }
67 |
68 | [Fact]
69 | public async Task MinusZero_ThrowsInvalidBencodeExceptionAsync()
70 | {
71 | var action = async () => await Parser.ParseStringAsync("i-0e");
72 | (await action.Should().ThrowAsync>())
73 | .WithMessage("*'-0' is not a valid number.*")
74 | .Which.StreamPosition.Should().Be(0);
75 | }
76 |
77 | [Theory]
78 | [InlineData("i12")]
79 | [InlineData("i123")]
80 | public async Task MissingEndChar_ThrowsInvalidBencodeExceptionAsync(string bencode)
81 | {
82 | var action = async () => await Parser.ParseStringAsync(bencode);
83 | (await action.Should().ThrowAsync>())
84 | .WithMessage("*Missing end character of object.*")
85 | .Which.StreamPosition.Should().Be(0);
86 | }
87 |
88 | [Theory]
89 | [InlineData("42e")]
90 | [InlineData("a42e")]
91 | [InlineData("d42e")]
92 | [InlineData("l42e")]
93 | [InlineData("100e")]
94 | [InlineData("1234567890e")]
95 | public async Task InvalidFirstChar_ThrowsInvalidBencodeExceptionAsync(string bencode)
96 | {
97 | var action = async () => await Parser.ParseStringAsync(bencode);
98 | (await action.Should().ThrowAsync>())
99 | .WithMessage("*Unexpected character. Expected 'i'*")
100 | .Which.StreamPosition.Should().Be(0);
101 | }
102 |
103 | [Fact]
104 | public async Task JustNegativeSign_ThrowsInvalidBencodeExceptionAsync()
105 | {
106 | var action = async () => await Parser.ParseStringAsync("i-e");
107 | (await action.Should().ThrowAsync>())
108 | .WithMessage("*It contains no digits.*")
109 | .Which.StreamPosition.Should().Be(0);
110 | }
111 |
112 | [Theory]
113 | [InlineData("i--1e")]
114 | [InlineData("i--42e")]
115 | [InlineData("i---100e")]
116 | [InlineData("i----1234567890e")]
117 | public async Task MoreThanOneNegativeSign_ThrowsInvalidBencodeExceptionAsync(string bencode)
118 | {
119 | var action = async () => await Parser.ParseStringAsync(bencode);
120 | (await action.Should().ThrowAsync>())
121 | .WithMessage("*The value '*' is not a valid number.*")
122 | .Which.StreamPosition.Should().Be(0);
123 | }
124 |
125 | [Theory]
126 | [InlineData("iasdfe")]
127 | [InlineData("i!#¤%&e")]
128 | [InlineData("i.e")]
129 | [InlineData("i42.e")]
130 | [InlineData("i42ae")]
131 | public async Task NonDigit_ThrowsInvalidBencodeExceptionAsync(string bencode)
132 | {
133 | var action = async () => await Parser.ParseStringAsync(bencode);
134 | (await action.Should().ThrowAsync>())
135 | .WithMessage("*The value '*' is not a valid number.*")
136 | .Which.StreamPosition.Should().Be(0);
137 | }
138 |
139 |
140 | [Theory]
141 | [InlineData("", "reached end of stream")]
142 | [InlineData("i", "contains no digits")]
143 | [InlineData("ie", "contains no digits")]
144 | public async Task BelowMinimumLength_ThrowsInvalidBencodeExceptionAsync(string bencode, string exceptionMessage)
145 | {
146 | var action = async () => await Parser.ParseStringAsync(bencode);
147 | (await action.Should().ThrowAsync>())
148 | .WithMessage($"*{exceptionMessage}*")
149 | .Which.StreamPosition.Should().Be(0);
150 | }
151 |
152 | [Theory]
153 | [InlineData("i9223372036854775808e")]
154 | [InlineData("i-9223372036854775809e")]
155 | public async Task LargerThanInt64_ThrowsUnsupportedExceptionAsync(string bencode)
156 | {
157 | var action = async () => await Parser.ParseStringAsync(bencode);
158 | (await action.Should().ThrowAsync>())
159 | .WithMessage("*The value '*' is not a valid long (Int64)*")
160 | .Which.StreamPosition.Should().Be(0);
161 | }
162 |
163 | [Theory]
164 | [InlineData("i12345678901234567890e")]
165 | [InlineData("i123456789012345678901e")]
166 | [InlineData("i123456789012345678901234567890e")]
167 | public async Task LongerThanMaxDigits19_ThrowsUnsupportedExceptionAsync(string bencode)
168 | {
169 | var action = async () => await Parser.ParseStringAsync(bencode);
170 | (await action.Should().ThrowAsync>())
171 | .WithMessage("*The number '*' has more than 19 digits and cannot be stored as a long*")
172 | .Which.StreamPosition.Should().Be(0);
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BNumberParserTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BencodeNET.Exceptions;
3 | using BencodeNET.Objects;
4 | using BencodeNET.Parsing;
5 | using FluentAssertions;
6 | using Xunit;
7 |
8 | namespace BencodeNET.Tests.Parsing
9 | {
10 | public partial class BNumberParserTests
11 | {
12 | private BNumberParser Parser { get; }
13 |
14 | public BNumberParserTests()
15 | {
16 | Parser = new BNumberParser();
17 | }
18 |
19 | [Theory]
20 | [InlineData("i1e", 1)]
21 | [InlineData("i2e", 2)]
22 | [InlineData("i3e", 3)]
23 | [InlineData("i42e", 42)]
24 | [InlineData("i100e", 100)]
25 | [InlineData("i1234567890e", 1234567890)]
26 | public void CanParsePositive(string bencode, int value)
27 | {
28 | var bnumber = Parser.ParseString(bencode);
29 | bnumber.Should().Be(value);
30 | }
31 |
32 | [Fact]
33 | public void CanParseZero()
34 | {
35 | var bnumber = Parser.ParseString("i0e");
36 | bnumber.Should().Be(0);
37 | }
38 |
39 | [Theory]
40 | [InlineData("i-1e", -1)]
41 | [InlineData("i-2e", -2)]
42 | [InlineData("i-3e", -3)]
43 | [InlineData("i-42e", -42)]
44 | [InlineData("i-100e", -100)]
45 | [InlineData("i-1234567890e", -1234567890)]
46 | public void CanParseNegative(string bencode, int value)
47 | {
48 | var bnumber = Parser.ParseString(bencode);
49 | bnumber.Should().Be(value);
50 | }
51 |
52 | [Theory]
53 | [InlineData("i9223372036854775807e", 9223372036854775807)]
54 | [InlineData("i-9223372036854775808e", -9223372036854775808)]
55 | public void CanParseInt64(string bencode, long value)
56 | {
57 | var bnumber = Parser.ParseString(bencode);
58 | bnumber.Should().Be(value);
59 | }
60 |
61 | [Theory]
62 | [InlineData("i01e")]
63 | [InlineData("i012e")]
64 | [InlineData("i01234567890e")]
65 | [InlineData("i00001e")]
66 | public void LeadingZeros_ThrowsInvalidBencodeException(string bencode)
67 | {
68 | Action action = () => Parser.ParseString(bencode);
69 | action.Should().Throw>()
70 | .WithMessage("*Leading '0's are not valid.*")
71 | .Which.StreamPosition.Should().Be(0);
72 | }
73 |
74 | [Fact]
75 | public void MinusZero_ThrowsInvalidBencodeException()
76 | {
77 | Action action = () => Parser.ParseString("i-0e");
78 | action.Should().Throw>()
79 | .WithMessage("*'-0' is not a valid number.*")
80 | .Which.StreamPosition.Should().Be(0);
81 | }
82 |
83 | [Theory]
84 | [InlineData("i12")]
85 | [InlineData("i123")]
86 | public void MissingEndChar_ThrowsInvalidBencodeException(string bencode)
87 | {
88 | Action action = () => Parser.ParseString(bencode);
89 | action.Should().Throw>()
90 | .WithMessage("*Missing end character of object.*")
91 | .Which.StreamPosition.Should().Be(0);
92 | }
93 |
94 | [Theory]
95 | [InlineData("42e")]
96 | [InlineData("a42e")]
97 | [InlineData("d42e")]
98 | [InlineData("l42e")]
99 | [InlineData("100e")]
100 | [InlineData("1234567890e")]
101 | public void InvalidFirstChar_ThrowsInvalidBencodeException(string bencode)
102 | {
103 | Action action = () => Parser.ParseString(bencode);
104 | action.Should().Throw>()
105 | .WithMessage("*Unexpected character. Expected 'i'*")
106 | .Which.StreamPosition.Should().Be(0);
107 | }
108 |
109 | [Fact]
110 | public void JustNegativeSign_ThrowsInvalidBencodeException()
111 | {
112 | Action action = () => Parser.ParseString("i-e");
113 | action.Should().Throw>()
114 | .WithMessage("*It contains no digits.*")
115 | .Which.StreamPosition.Should().Be(0);
116 | }
117 |
118 | [Theory]
119 | [InlineData("i--1e")]
120 | [InlineData("i--42e")]
121 | [InlineData("i---100e")]
122 | [InlineData("i----1234567890e")]
123 | public void MoreThanOneNegativeSign_ThrowsInvalidBencodeException(string bencode)
124 | {
125 | Action action = () => Parser.ParseString(bencode);
126 | action.Should().Throw>()
127 | .WithMessage("*The value '*' is not a valid number.*")
128 | .Which.StreamPosition.Should().Be(0);
129 | }
130 |
131 | [Theory]
132 | [InlineData("iasdfe")]
133 | [InlineData("i!#¤%&e")]
134 | [InlineData("i.e")]
135 | [InlineData("i42.e")]
136 | [InlineData("i42ae")]
137 | public void NonDigit_ThrowsInvalidBencodeException(string bencode)
138 | {
139 | Action action = () => Parser.ParseString(bencode);
140 | action.Should().Throw>()
141 | .WithMessage("*The value '*' is not a valid number.*")
142 | .Which.StreamPosition.Should().Be(0);
143 | }
144 |
145 |
146 | [Theory]
147 | [InlineData("")]
148 | [InlineData("i")]
149 | [InlineData("ie")]
150 | public void BelowMinimumLength_ThrowsInvalidBencodeException(string bencode)
151 | {
152 | Action action = () => Parser.ParseString(bencode);
153 | action.Should().Throw>()
154 | .WithMessage("*Invalid length.*")
155 | .Which.StreamPosition.Should().Be(0);
156 | }
157 |
158 | [Theory]
159 | [InlineData("")]
160 | [InlineData("i")]
161 | [InlineData("ie")]
162 | public void BelowMinimumLength_WhenStreamWithoutLengthSupport_ThrowsInvalidException(string bencode)
163 | {
164 | var stream = new LengthNotSupportedStream(bencode);
165 | Action action = () => Parser.Parse(stream);
166 | action.Should().Throw>()
167 | .Which.StreamPosition.Should().Be(0);
168 | }
169 |
170 | [Theory]
171 | [InlineData("i9223372036854775808e")]
172 | [InlineData("i-9223372036854775809e")]
173 | public void LargerThanInt64_ThrowsUnsupportedException(string bencode)
174 | {
175 | Action action = () => Parser.ParseString(bencode);
176 | action.Should().Throw>()
177 | .WithMessage("*The value '*' is not a valid long (Int64)*")
178 | .Which.StreamPosition.Should().Be(0);
179 | }
180 |
181 | [Theory]
182 | [InlineData("i12345678901234567890e")]
183 | [InlineData("i123456789012345678901e")]
184 | [InlineData("i123456789012345678901234567890e")]
185 | public void LongerThanMaxDigits19_ThrowsUnsupportedException(string bencode)
186 | {
187 | Action action = () => Parser.ParseString(bencode);
188 | action.Should().Throw>()
189 | .WithMessage("*The number '*' has more than 19 digits and cannot be stored as a long*")
190 | .Which.StreamPosition.Should().Be(0);
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BObjectParserListTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BencodeNET.Objects;
3 | using BencodeNET.Parsing;
4 | using FluentAssertions;
5 | using Xunit;
6 |
7 | namespace BencodeNET.Tests.Parsing
8 | {
9 | public class BObjectParserListTests
10 | {
11 | [Theory]
12 | [AutoMockedData]
13 | public void Add_GenericParser_ContainsOnlyThatParser(IBObjectParser parser)
14 | {
15 | var list = new BObjectParserList();
16 | list.Add(parser);
17 |
18 | list.Should().HaveCount(1);
19 | list.Should().ContainSingle(x => x.Value == parser);
20 | }
21 |
22 | [Theory]
23 | [AutoMockedData]
24 | public void Add_GenericParser_AddedWithGenericTypeAsKey(IBObjectParser parser)
25 | {
26 | var list = new BObjectParserList();
27 | list.Add(parser);
28 |
29 | list.Should().HaveCount(1);
30 | list.Should().ContainSingle(x => x.Key == typeof(IBObject));
31 | }
32 |
33 | [Theory]
34 | [AutoMockedData]
35 | public void Add_GenericParser_ReplacesExistingOfSameGenericType(IBObjectParser parser)
36 | {
37 | var list = new BObjectParserList();
38 | list.Add(parser);
39 | list.Add(parser);
40 |
41 | list.Should().HaveCount(1);
42 | list.Should().ContainSingle(x => x.Value == parser);
43 | }
44 |
45 | [Theory]
46 | [AutoMockedData()]
47 | public void Add_ParserWithType_ReplacesExistingOfSameGenericType(IBObjectParser parser)
48 | {
49 | var list = new BObjectParserList();
50 | list.Add(typeof(BString), parser);
51 | list.Add(typeof(BString), parser);
52 |
53 | list.Should().HaveCount(1);
54 | list.Should().ContainSingle(x => x.Value == parser);
55 | }
56 |
57 | [Theory]
58 | [AutoMockedData(typeof(object))]
59 | [AutoMockedData(typeof(string))]
60 | [AutoMockedData(typeof(int))]
61 | public void Add_ParserWithNonIBObjectType_ThrowsArgumentException(Type type, IBObjectParser parser)
62 | {
63 | var list = new BObjectParserList();
64 | Action action = () => list.Add(type, parser);
65 |
66 | action.Should().Throw("because only IBObject types are allowed");
67 | }
68 |
69 | [Theory]
70 | [AutoMockedData]
71 | public void Add_WithMultipleTypes_AddsParserForEachType(IBObjectParser parser)
72 | {
73 | var types = new[] {typeof (BString), typeof (BNumber), typeof (BList)};
74 |
75 | var list = new BObjectParserList();
76 | list.Add(types, parser);
77 |
78 | list.Should().HaveCount(3);
79 | list.Should().OnlyContain(x => x.Value == parser);
80 | }
81 |
82 | [Theory]
83 | [AutoMockedData]
84 | public void Clear_EmptiesList(IBObjectParser parser1, IBObjectParser parser2)
85 | {
86 | var list = new BObjectParserList {parser1, parser2};
87 | list.Clear();
88 |
89 | list.Should().BeEmpty();
90 | }
91 |
92 | [Fact]
93 | public void Indexer_Get_ReturnsNullIfKeyMissing()
94 | {
95 | var list = new BObjectParserList();
96 |
97 | var parser = list[typeof (object)];
98 |
99 | parser.Should().BeNull();
100 | }
101 |
102 | [Fact]
103 | public void Indexer_Get_ReturnsMatchingParserForType()
104 | {
105 | var stringParser = new BStringParser();
106 | var list = new BObjectParserList {stringParser};
107 |
108 | var parser = list[typeof(BString)];
109 |
110 | parser.Should().BeSameAs(stringParser);
111 | }
112 |
113 | [Fact]
114 | public void Indexer_Set_AddsParserForType()
115 | {
116 | var stringParser = new BStringParser();
117 |
118 | var list = new BObjectParserList();
119 | list[typeof (BString)] = stringParser;
120 |
121 | list.Should().HaveCount(1);
122 | list[typeof (BString)].Should().BeSameAs(stringParser);
123 | }
124 |
125 | [Fact]
126 | public void Indexer_Set_ReplacesExistingParserForType()
127 | {
128 | var stringParser1 = new BStringParser();
129 | var stringParser2 = new BStringParser();
130 | var list = new BObjectParserList { stringParser1 };
131 |
132 | list[typeof (BString)] = stringParser2;
133 |
134 | list.Should().HaveCount(1);
135 | list[typeof (BString)].Should().BeSameAs(stringParser2);
136 | }
137 |
138 | [Fact]
139 | public void Get_Generic_ReturnsMatchingParser()
140 | {
141 | var stringParser = new BStringParser();
142 | var list = new BObjectParserList {stringParser};
143 |
144 | var parser = list.Get();
145 |
146 | parser.Should().BeSameAs(stringParser);
147 | }
148 |
149 | [Fact]
150 | public void Get_Generic_ReturnsNullIfNoMatchingParser()
151 | {
152 | var list = new BObjectParserList();
153 |
154 | var parser = list.Get();
155 |
156 | parser.Should().BeNull();
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BObjectParserTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Text;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using BencodeNET.IO;
6 | using BencodeNET.Objects;
7 | using BencodeNET.Parsing;
8 | using NSubstitute;
9 | using Xunit;
10 |
11 | namespace BencodeNET.Tests.Parsing
12 | {
13 | public class BObjectParserTests
14 | {
15 | [Theory]
16 | [AutoMockedData]
17 | public void Parse_String_CallsOverriddenParse(IBObjectParser parserMock)
18 | {
19 | var parser = new MockBObjectParser(parserMock);
20 |
21 | parser.ParseString("bencoded string");
22 |
23 | parserMock.Received().Parse(Arg.Any());
24 | }
25 |
26 | [Theory]
27 | [AutoMockedData]
28 | public void Parse_Stream_CallsOverriddenParse(IBObjectParser parserMock)
29 | {
30 | var parser = new MockBObjectParser(parserMock);
31 | var bytes = Encoding.UTF8.GetBytes("bencoded string");
32 |
33 | using (var stream = new MemoryStream(bytes))
34 | {
35 | parser.Parse(stream);
36 | }
37 |
38 | parserMock.Received().Parse(Arg.Any());
39 | }
40 |
41 | class MockBObjectParser : BObjectParser
42 | {
43 | public MockBObjectParser(IBObjectParser substitute)
44 | {
45 | Substitute = substitute;
46 | }
47 |
48 | public IBObjectParser Substitute { get; set; }
49 |
50 | public override Encoding Encoding => Encoding.UTF8;
51 |
52 | public override IBObject Parse(BencodeReader stream)
53 | {
54 | return Substitute.Parse(stream);
55 | }
56 |
57 | public override ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default)
58 | {
59 | throw new System.NotImplementedException();
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BStringParserTests.Async.cs:
--------------------------------------------------------------------------------
1 | using System.IO.Pipelines;
2 | using System.Text;
3 | using System.Threading.Tasks;
4 | using BencodeNET.Exceptions;
5 | using BencodeNET.Objects;
6 | using BencodeNET.Parsing;
7 | using FluentAssertions;
8 | using Xunit;
9 |
10 | namespace BencodeNET.Tests.Parsing
11 | {
12 | public partial class BStringParserTests
13 | {
14 | [Theory]
15 | [InlineData("4:spam")]
16 | [InlineData("8:spameggs")]
17 | [InlineData("9:spam eggs")]
18 | [InlineData("9:spam:eggs")]
19 | [InlineData("14:!@#¤%&/()=?$|")]
20 | public async Task CanParseSimpleAsync(string bencode)
21 | {
22 | var parts = bencode.Split(new[] {':'}, 2);
23 | var length = int.Parse(parts[0]);
24 | var value = parts[1];
25 |
26 | var bstring = await Parser.ParseStringAsync(bencode);
27 |
28 | bstring.Length.Should().Be(length);
29 | bstring.Should().Be(value);
30 | }
31 |
32 | [Fact]
33 | public async Task CanParse_EmptyStringAsync()
34 | {
35 | var bstring = await Parser.ParseStringAsync("0:");
36 |
37 | bstring.Length.Should().Be(0);
38 | bstring.Should().Be("");
39 | }
40 |
41 | [Theory]
42 | [InlineData("5:spam")]
43 | [InlineData("6:spam")]
44 | [InlineData("100:spam")]
45 | public async Task LessCharsThanSpecified_ThrowsInvalidBencodeExceptionAsync(string bencode)
46 | {
47 | var action = async () => await Parser.ParseStringAsync(bencode);
48 | (await action.Should().ThrowAsync>())
49 | .WithMessage("*but could only read * bytes*")
50 | .Which.StreamPosition.Should().Be(0);
51 | }
52 |
53 | [Theory]
54 | [InlineData("4spam", 1)]
55 | [InlineData("10spam", 2)]
56 | [InlineData("4-spam", 1)]
57 | [InlineData("4.spam", 1)]
58 | [InlineData("4;spam", 1)]
59 | [InlineData("4,spam", 1)]
60 | [InlineData("4|spam", 1)]
61 | public async Task MissingDelimiter_ThrowsInvalidBencodeExceptionAsync(string bencode, int errorIndex)
62 | {
63 | var action = async () => await Parser.ParseStringAsync(bencode);
64 | (await action.Should().ThrowAsync>())
65 | .WithMessage("*Unexpected character. Expected ':'*")
66 | .Which.StreamPosition.Should().Be(errorIndex);
67 | }
68 |
69 | [Theory]
70 | [InlineData("spam")]
71 | [InlineData("-spam")]
72 | [InlineData(".spam")]
73 | [InlineData(",spam")]
74 | [InlineData(";spam")]
75 | [InlineData("?spam")]
76 | [InlineData("!spam")]
77 | [InlineData("#spam")]
78 | public async Task NonDigitFirstChar_ThrowsInvalidBencodeExceptionAsync(string bencode)
79 | {
80 | var action = async () => await Parser.ParseStringAsync(bencode);
81 | (await action.Should().ThrowAsync>())
82 | .WithMessage($"*Unexpected character. Expected ':' but found '{bencode[0]}'*")
83 | .Which.StreamPosition.Should().Be(0);
84 | }
85 |
86 | [Theory]
87 | [InlineData("12345678901:spam")]
88 | [InlineData("123456789012:spam")]
89 | [InlineData("1234567890123:spam")]
90 | [InlineData("12345678901234:spam")]
91 | public async Task LengthAboveMaxDigits10_ThrowsUnsupportedExceptionAsync(string bencode)
92 | {
93 | var action = async () => await Parser.ParseStringAsync(bencode);
94 | (await action.Should().ThrowAsync>())
95 | .WithMessage("*Length of string is more than * digits*")
96 | .Which.StreamPosition.Should().Be(0);
97 | }
98 |
99 | [Theory]
100 | [InlineData("1:spam")]
101 | [InlineData("12:spam")]
102 | [InlineData("123:spam")]
103 | [InlineData("1234:spam")]
104 | [InlineData("12345:spam")]
105 | [InlineData("123456:spam")]
106 | [InlineData("1234567:spam")]
107 | [InlineData("12345678:spam")]
108 | [InlineData("123456789:spam")]
109 | [InlineData("1234567890:spam")]
110 | public async Task LengthAtOrBelowMaxDigits10_DoesNotThrowUnsupportedExceptionAsync(string bencode)
111 | {
112 | var action = async () => await Parser.ParseStringAsync(bencode);
113 | await action.Should().NotThrowAsync>();
114 | }
115 |
116 | [Fact]
117 | public async Task LengthAboveInt32MaxValue_ThrowsUnsupportedExceptionAsync()
118 | {
119 | var bencode = "2147483648:spam";
120 | var action = async () => await Parser.ParseStringAsync(bencode);
121 | (await action.Should().ThrowAsync>())
122 | .WithMessage("*Length of string is * but maximum supported length is *")
123 | .Which.StreamPosition.Should().Be(0);
124 | }
125 |
126 | [Fact]
127 | public async Task LengthBelowInt32MaxValue_DoesNotThrowUnsupportedExceptionAsync()
128 | {
129 | var bencode = "2147483647:spam";
130 | var action = async () => await Parser.ParseStringAsync(bencode);
131 | await action.Should().NotThrowAsync>();
132 | }
133 |
134 | [Fact]
135 | public async Task CanParseEncodedAsLatin1Async()
136 | {
137 | var encoding = Encoding.GetEncoding("LATIN1");
138 | var expected = new BString("æøå", encoding);
139 | var parser = new BStringParser(encoding);
140 |
141 | // "3:æøå"
142 | var bytes = new byte[] {51, 58, 230, 248, 229};
143 | var (reader, writer) = new Pipe();
144 | await writer.WriteAsync(bytes);
145 |
146 | var bstring = await parser.ParseAsync(reader);
147 |
148 | bstring.Should().Be(expected);
149 | bstring.GetSizeInBytes().Should().Be(5);
150 | }
151 |
152 | [Theory]
153 | [InlineData("1-:a", 1)]
154 | [InlineData("1abc:a", 1)]
155 | [InlineData("123?:asdf", 3)]
156 | [InlineData("3abc:abc", 1)]
157 | public async Task InvalidLengthString_ThrowsInvalidExceptionAsync(string bencode, int errorIndex)
158 | {
159 | var action = async () => await Parser.ParseStringAsync(bencode);
160 | (await action.Should().ThrowAsync>())
161 | .WithMessage("*Unexpected character. Expected ':'*")
162 | .Which.StreamPosition.Should().Be(errorIndex);
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BStringParserTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using BencodeNET.Exceptions;
4 | using BencodeNET.Objects;
5 | using BencodeNET.Parsing;
6 | using FluentAssertions;
7 | using Xunit;
8 |
9 | namespace BencodeNET.Tests.Parsing
10 | {
11 | public partial class BStringParserTests
12 | {
13 | private BStringParser Parser { get; }
14 |
15 | public BStringParserTests()
16 | {
17 | Parser = new BStringParser();
18 | }
19 |
20 | [Theory]
21 | [InlineData("4:spam")]
22 | [InlineData("8:spameggs")]
23 | [InlineData("9:spam eggs")]
24 | [InlineData("9:spam:eggs")]
25 | [InlineData("14:!@#¤%&/()=?$|")]
26 | public void CanParseSimple(string bencode)
27 | {
28 | var parts = bencode.Split(new[] {':'}, 2);
29 | var length = int.Parse(parts[0]);
30 | var value = parts[1];
31 |
32 | var bstring = Parser.ParseString(bencode);
33 |
34 | bstring.Length.Should().Be(length);
35 | bstring.Should().Be(value);
36 | }
37 |
38 | [Fact]
39 | public void CanParse_EmptyString()
40 | {
41 | var bstring = Parser.ParseString("0:");
42 |
43 | bstring.Length.Should().Be(0);
44 | bstring.Should().Be("");
45 | }
46 |
47 | [Theory]
48 | [InlineData("5:spam", 4)]
49 | [InlineData("6:spam", 4)]
50 | [InlineData("100:spam", 4)]
51 | public void LessCharsThanSpecified_ThrowsInvalidBencodeException(string bencode, int expectedReadBytes)
52 | {
53 | Action action = () => Parser.ParseString(bencode);
54 | action.Should().Throw>()
55 | .WithMessage($"*but could only read {expectedReadBytes} bytes*")
56 | .Which.StreamPosition.Should().Be(0);
57 | }
58 |
59 | [Theory]
60 | [InlineData("4spam", 1)]
61 | [InlineData("10spam", 2)]
62 | [InlineData("4-spam", 1)]
63 | [InlineData("4.spam", 1)]
64 | [InlineData("4;spam", 1)]
65 | [InlineData("4,spam", 1)]
66 | [InlineData("4|spam", 1)]
67 | public void MissingDelimiter_ThrowsInvalidBencodeException(string bencode, int errorIndex)
68 | {
69 | Action action = () => Parser.ParseString(bencode);
70 | action.Should().Throw>()
71 | .WithMessage("*Unexpected character. Expected ':'*")
72 | .Which.StreamPosition.Should().Be(errorIndex);
73 | }
74 |
75 | [Theory]
76 | [InlineData("spam")]
77 | [InlineData("-spam")]
78 | [InlineData(".spam")]
79 | [InlineData(",spam")]
80 | [InlineData(";spam")]
81 | [InlineData("?spam")]
82 | [InlineData("!spam")]
83 | [InlineData("#spam")]
84 | public void NonDigitFirstChar_ThrowsInvalidBencodeException(string bencode)
85 | {
86 | Action action = () => Parser.ParseString(bencode);
87 | action.Should().Throw>()
88 | .WithMessage($"*Unexpected character. Expected ':' but found '{bencode[0]}'*")
89 | .Which.StreamPosition.Should().Be(0);
90 | }
91 |
92 | [Theory]
93 | [InlineData("0")]
94 | [InlineData("4")]
95 | public void LessThanMinimumLength2_ThrowsInvalidBencodeException(string bencode)
96 | {
97 | Action action = () => Parser.ParseString(bencode);
98 | action.Should().Throw>()
99 | .WithMessage("*Invalid length*")
100 | .Which.StreamPosition.Should().Be(0);
101 | }
102 |
103 | [Theory]
104 | [InlineData("12345678901:spam")]
105 | [InlineData("123456789012:spam")]
106 | [InlineData("1234567890123:spam")]
107 | [InlineData("12345678901234:spam")]
108 | public void LengthAboveMaxDigits10_ThrowsUnsupportedException(string bencode)
109 | {
110 | Action action = () => Parser.ParseString(bencode);
111 | action.Should().Throw>()
112 | .WithMessage("*Length of string is more than * digits*")
113 | .Which.StreamPosition.Should().Be(0);
114 | }
115 |
116 | [Theory]
117 | [InlineData("1:spam")]
118 | [InlineData("12:spam")]
119 | [InlineData("123:spam")]
120 | [InlineData("1234:spam")]
121 | [InlineData("12345:spam")]
122 | [InlineData("123456:spam")]
123 | [InlineData("1234567:spam")]
124 | [InlineData("12345678:spam")]
125 | [InlineData("123456789:spam")]
126 | [InlineData("1234567890:spam")]
127 | public void LengthAtOrBelowMaxDigits10_DoesNotThrowUnsupportedException(string bencode)
128 | {
129 | Action action = () => Parser.ParseString(bencode);
130 | action.Should().NotThrow>();
131 | }
132 |
133 | [Fact]
134 | public void LengthAboveInt32MaxValue_ThrowsUnsupportedException()
135 | {
136 | var bencode = "2147483648:spam";
137 | Action action = () => Parser.ParseString(bencode);
138 | action.Should().Throw>()
139 | .WithMessage("*Length of string is * but maximum supported length is *")
140 | .Which.StreamPosition.Should().Be(0);
141 | }
142 |
143 | [Fact]
144 | public void LengthBelowInt32MaxValue_DoesNotThrowUnsupportedException()
145 | {
146 | var bencode = "2147483647:spam";
147 | Action action = () => Parser.ParseString(bencode);
148 | action.Should().NotThrow>();
149 | }
150 |
151 | [Fact]
152 | public void CanParseEncodedAsLatin1()
153 | {
154 | var encoding = Encoding.GetEncoding("LATIN1");
155 | var expected = new BString("æøå", encoding);
156 | var parser = new BStringParser(encoding);
157 |
158 | // "3:æøå"
159 | var bytes = new byte[] {51, 58, 230, 248, 229};
160 | var bstring = parser.Parse(bytes);
161 |
162 | bstring.Should().Be(expected);
163 | }
164 |
165 | [Theory]
166 | [InlineData("1-:a", 1)]
167 | [InlineData("1abc:a", 1)]
168 | [InlineData("123?:asdf", 3)]
169 | [InlineData("3abc:abc", 1)]
170 | public void InvalidLengthString_ThrowsInvalidException(string bencode, int errorIndex)
171 | {
172 | Action action = () => Parser.ParseString(bencode);
173 | action.Should().Throw>()
174 | .WithMessage("*Unexpected character. Expected ':'*")
175 | .Which.StreamPosition.Should().Be(errorIndex);
176 | }
177 |
178 | [Theory]
179 | [InlineData("")]
180 | [InlineData("0")]
181 | public void BelowMinimumLength_WhenStreamWithoutLengthSupport_ThrowsInvalidException(string bencode)
182 | {
183 | var stream = new LengthNotSupportedStream(bencode);
184 | Action action = () => Parser.Parse(stream);
185 | action.Should().Throw>()
186 | .WithMessage("*Unexpected character. Expected ':' but reached end of stream*")
187 | .Which.StreamPosition.Should().Be(0);
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/BencodeParserTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BencodeNET.Exceptions;
3 | using BencodeNET.Objects;
4 | using BencodeNET.Parsing;
5 | using FluentAssertions;
6 | using Xunit;
7 |
8 | namespace BencodeNET.Tests.Parsing
9 | {
10 | // TODO: "Integration" tests? Full decode tests
11 | public class BencodeParserTests
12 | {
13 | [Theory]
14 | #region Alphabet...
15 | [AutoMockedData("a")]
16 | [AutoMockedData("b")]
17 | [AutoMockedData("c")]
18 | [AutoMockedData("e")]
19 | [AutoMockedData("f")]
20 | [AutoMockedData("g")]
21 | [AutoMockedData("h")]
22 | [AutoMockedData("j")]
23 | [AutoMockedData("k")]
24 | [AutoMockedData("m")]
25 | [AutoMockedData("n")]
26 | [AutoMockedData("o")]
27 | [AutoMockedData("p")]
28 | [AutoMockedData("q")]
29 | [AutoMockedData("r")]
30 | [AutoMockedData("s")]
31 | [AutoMockedData("t")]
32 | [AutoMockedData("u")]
33 | [AutoMockedData("v")]
34 | [AutoMockedData("w")]
35 | [AutoMockedData("x")]
36 | [AutoMockedData("y")]
37 | [AutoMockedData("z")]
38 | [AutoMockedData("A")]
39 | [AutoMockedData("B")]
40 | [AutoMockedData("C")]
41 | [AutoMockedData("D")]
42 | [AutoMockedData("E")]
43 | [AutoMockedData("F")]
44 | [AutoMockedData("G")]
45 | [AutoMockedData("H")]
46 | [AutoMockedData("I")]
47 | [AutoMockedData("J")]
48 | [AutoMockedData("K")]
49 | [AutoMockedData("L")]
50 | [AutoMockedData("M")]
51 | [AutoMockedData("N")]
52 | [AutoMockedData("O")]
53 | [AutoMockedData("P")]
54 | [AutoMockedData("Q")]
55 | [AutoMockedData("R")]
56 | [AutoMockedData("S")]
57 | [AutoMockedData("T")]
58 | [AutoMockedData("U")]
59 | [AutoMockedData("V")]
60 | [AutoMockedData("W")]
61 | [AutoMockedData("X")]
62 | [AutoMockedData("Y")]
63 | [AutoMockedData("Z")]
64 | #endregion
65 | public void InvalidFirstChars_ThrowsInvalidBencodeException(string bencode)
66 | {
67 | var bparser = new BencodeParser();
68 | Action action = () => bparser.ParseString(bencode);
69 |
70 | action.Should().Throw>();
71 | }
72 |
73 | [Fact]
74 | public void EmptyString_ReturnsNull()
75 | {
76 | var bparser = new BencodeParser();
77 | var result = bparser.ParseString("");
78 | result.Should().BeNull();
79 | }
80 |
81 | [Fact]
82 | public void CanParse_ListOfStrings()
83 | {
84 | var bencode = "l4:spam3:egge";
85 |
86 | var bparser = new BencodeParser();
87 | var blist = bparser.ParseString(bencode) as BList;
88 |
89 | blist.Should().HaveCount(2);
90 | blist[0].Should().BeOfType();
91 | blist[0].Should().Be((BString)"spam");
92 | blist[1].Should().BeOfType();
93 | blist[1].Should().Be((BString)"egg");
94 | }
95 |
96 | [Fact]
97 | public void CanParseGeneric_ListOfStrings()
98 | {
99 | var bencode = "l4:spam3:egge";
100 |
101 | var bparser = new BencodeParser();
102 | var blist = bparser.ParseString(bencode);
103 |
104 | blist.Should().HaveCount(2);
105 | blist[0].Should().BeOfType();
106 | blist[0].Should().Be((BString)"spam");
107 | blist[1].Should().BeOfType();
108 | blist[1].Should().Be((BString)"egg");
109 | }
110 |
111 | [Fact]
112 | public void CanParse_SimpleDictionary()
113 | {
114 | var bencode = "d4:spam3:egg3:fooi42ee";
115 |
116 | var bparser = new BencodeParser();
117 | var bdictionary = bparser.ParseString(bencode);
118 |
119 | bdictionary.Should().HaveCount(2);
120 | bdictionary.Should().ContainKey("spam");
121 | bdictionary.Should().ContainKey("foo");
122 | bdictionary["spam"].Should().BeOfType(typeof (BString));
123 | bdictionary["spam"].Should().Be((BString) "egg");
124 | bdictionary["foo"].Should().BeOfType(typeof (BNumber));
125 | bdictionary["foo"].Should().Be((BNumber) 42);
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Parsing/ParseUtilTests.cs:
--------------------------------------------------------------------------------
1 | using BencodeNET.Parsing;
2 | using FluentAssertions;
3 | using Xunit;
4 |
5 | namespace BencodeNET.Tests.Parsing
6 | {
7 | public class ParseUtilTests
8 | {
9 | [Fact]
10 | public void TryParseLongFast_CanParseSimple()
11 | {
12 | ParseUtil.TryParseLongFast("123", out var value);
13 | value.Should().Be(123);
14 | }
15 |
16 | [Fact]
17 | public void TryParseLongFast_NullReturnsFalse()
18 | {
19 | var result = ParseUtil.TryParseLongFast((string) null, out _);
20 | result.Should().BeFalse();
21 | }
22 |
23 | [Theory]
24 | [AutoMockedData("")]
25 | [AutoMockedData("-")]
26 | public void TryParseLongFast_ZeroLengthInputReturnsFalse(string input)
27 | {
28 | var result = ParseUtil.TryParseLongFast(input, out _);
29 | result.Should().BeFalse();
30 | }
31 |
32 | [Theory]
33 | [AutoMockedData("12345678901234567890")]
34 | [AutoMockedData("-12345678901234567890")]
35 | public void TryParseLongFast_InputLongerThanInt64MaxValueReturnsFalse(string input)
36 | {
37 | var result = ParseUtil.TryParseLongFast(input, out _);
38 | result.Should().BeFalse();
39 | }
40 |
41 | [Fact]
42 | public void TryParseLongFast_InputBiggerThanInt64MaxValueReturnsFalse()
43 | {
44 | var result = ParseUtil.TryParseLongFast("9223372036854775808", out _);
45 | result.Should().BeFalse();
46 | }
47 |
48 | [Fact]
49 | public void TryParseLongFast_InputSmallerThanInt64MinValueReturnsFalse()
50 | {
51 | var result = ParseUtil.TryParseLongFast("-9223372036854775809", out _);
52 | result.Should().BeFalse();
53 | }
54 |
55 | [Theory]
56 | [AutoMockedData("1.23")]
57 | [AutoMockedData("-1.23")]
58 | [AutoMockedData("1,23")]
59 | [AutoMockedData("-1,23")]
60 | [AutoMockedData("-1-23")]
61 | [AutoMockedData("-1-")]
62 | [AutoMockedData("-1.")]
63 | [AutoMockedData("-1a23")]
64 | [AutoMockedData("-1+23")]
65 | [AutoMockedData("+123")]
66 | [AutoMockedData("123a")]
67 | [AutoMockedData("a")]
68 | public void TryParseLongFast_InputContainingNonDigitReturnsFalse(string input)
69 | {
70 | var result = ParseUtil.TryParseLongFast(input, out _);
71 | result.Should().BeFalse();
72 | }
73 |
74 | [Theory]
75 | [AutoMockedData("0", 0)]
76 | [AutoMockedData("1", 1)]
77 | [AutoMockedData("123", 123)]
78 | [AutoMockedData("-1", -1)]
79 | [AutoMockedData("9223372036854775807", 9223372036854775807)]
80 | [AutoMockedData("-9223372036854775808", -9223372036854775808)]
81 | public void TryParseLongFast_ValidInputReturnsTrueAndCorrectValue(string input, long expected)
82 | {
83 | var result = ParseUtil.TryParseLongFast(input, out var value);
84 |
85 | result.Should().BeTrue();
86 | value.Should().Be(expected);
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Torrents/MultiFileInfoTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BencodeNET.Torrents;
3 | using FluentAssertions;
4 | using Xunit;
5 |
6 | namespace BencodeNET.Tests.Torrents
7 | {
8 | public class MultiFileInfoTests
9 | {
10 | [Theory]
11 | [AutoMockedData]
12 | public void FullPath_PathIsNull_ShouldNotThrowException(MultiFileInfo multiFileInfo)
13 | {
14 | // Arrange
15 | multiFileInfo.Path = null;
16 |
17 | // Act
18 | Action act = () => { var _ = multiFileInfo.FullPath; };
19 |
20 | // Assert
21 | act.Should().NotThrow();
22 | multiFileInfo.FullPath.Should().BeNull();
23 | }
24 |
25 | [Theory]
26 | [AutoMockedData]
27 | public void FullPathUtf8_PathUtf8IsNull_ShouldNotThrowException(MultiFileInfo multiFileInfo)
28 | {
29 | // Arrange
30 | multiFileInfo.PathUtf8 = null;
31 |
32 | // Act
33 | Action act = () => { var _ = multiFileInfo.FullPathUtf8; };
34 |
35 | // Assert
36 | act.Should().NotThrow();
37 | multiFileInfo.FullPathUtf8.Should().BeNull();
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/BencodeNET.Tests/Torrents/TorrentUtilTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using BencodeNET.Objects;
5 | using BencodeNET.Parsing;
6 | using BencodeNET.Torrents;
7 | using FluentAssertions;
8 | using NSubstitute;
9 | using Xunit;
10 |
11 | namespace BencodeNET.Tests.Torrents
12 | {
13 | public class TorrentUtilTests
14 | {
15 | private const string UbuntuTorrentFile = "Files\\ubuntu-14.10-desktop-amd64.iso.torrent";
16 |
17 | [Fact]
18 | public void CalculateInfoHash_CompleteTorrentFile()
19 | {
20 | var bdictionary = new BencodeParser().Parse(UbuntuTorrentFile);
21 | var info = bdictionary.Get(TorrentFields.Info);
22 | var hash = TorrentUtil.CalculateInfoHash(info);
23 |
24 | hash.Should().Be("B415C913643E5FF49FE37D304BBB5E6E11AD5101");
25 | }
26 |
27 | [Fact]
28 | public void CalculateInfoHash_SimpleInfoDictionary()
29 | {
30 | var info = new BDictionary
31 | {
32 | ["key"] = (BString) "value",
33 | ["list"] = new BList {1, 2, 3},
34 | ["number"] = (BNumber)42,
35 | ["dictionary"] = new BDictionary
36 | {
37 | ["key"] = (BString) "value"
38 | }
39 | };
40 |
41 | var hash = TorrentUtil.CalculateInfoHash(info);
42 |
43 | info.EncodeAsString().Should().Be("d10:dictionaryd3:key5:valuee3:key5:value4:listli1ei2ei3ee6:numberi42ee");
44 | hash.Should().Be("8715E7488A8964C6383E09A87287321FE6CBCC07");
45 | }
46 |
47 | [Theory]
48 | [AutoMockedData("")]
49 | [AutoMockedData((string)null)]
50 | public void CreateMagnetLink_NullOrEmptyInfoHash_ThrowsArgumentException(string infoHash)
51 | {
52 | Action action = () => TorrentUtil.CreateMagnetLink(infoHash, null, null, MagnetLinkOptions.None);
53 |
54 | action.Should().Throw("because a Magnet link is invalid without an info hash.");
55 | }
56 |
57 | [Theory]
58 | [AutoMockedData]
59 | public void CreateMagnetLink_NonEmptyInfoHash_IsIncluded(string infoHash)
60 | {
61 | var magnet = TorrentUtil.CreateMagnetLink(infoHash, null, null, MagnetLinkOptions.None);
62 |
63 | magnet.Should().Be($"magnet:?xt=urn:btih:{infoHash}");
64 | }
65 |
66 | [Theory]
67 | [AutoMockedData]
68 | public void CreateMagnetLink_NonEmptyDisplayName_IsIncluded(string infoHash, string displayName)
69 | {
70 | var magnet = TorrentUtil.CreateMagnetLink(infoHash, displayName, null, MagnetLinkOptions.None);
71 |
72 | magnet.Should().Be($"magnet:?xt=urn:btih:{infoHash}&dn={displayName}");
73 | }
74 |
75 | [Theory]
76 | [AutoMockedData]
77 | public void CreateMagnetLink_NonEmptyTracker_WithoutOptionIncludeTrackers_IsNotIncluded(string infoHash, string displayName, string tracker1)
78 | {
79 | var trackers = new List {tracker1};
80 |
81 | var magnet = TorrentUtil.CreateMagnetLink(infoHash, displayName, trackers, MagnetLinkOptions.None);
82 |
83 | magnet.Should().Be($"magnet:?xt=urn:btih:{infoHash}&dn={displayName}");
84 | }
85 |
86 | [Theory]
87 | [AutoMockedData]
88 | public void CreateMagnetLink_NonEmptyTracker_WithOptionIncludeTrackers_IsIncluded(string infoHash, string displayName, string tracker1)
89 | {
90 | var trackers = new List {tracker1};
91 |
92 | var magnet = TorrentUtil.CreateMagnetLink(infoHash, displayName, trackers, MagnetLinkOptions.IncludeTrackers);
93 |
94 | magnet.Should().Be($"magnet:?xt=urn:btih:{infoHash}&dn={displayName}&tr={tracker1}");
95 | }
96 |
97 | [Theory]
98 | [AutoMockedData]
99 | public void CreateMagnetLink_NonEmptyTrackers_WithOptionIncludeTrackers_AreIncluded(string infoHash, string displayName, string tracker1, string tracker2)
100 | {
101 | var trackers = new List {tracker1, tracker2};
102 |
103 | var magnet = TorrentUtil.CreateMagnetLink(infoHash, displayName, trackers, MagnetLinkOptions.IncludeTrackers);
104 |
105 | magnet.Should().Be($"magnet:?xt=urn:btih:{infoHash}&dn={displayName}&tr={tracker1}&tr={tracker2}");
106 | }
107 |
108 | [Theory]
109 | [AutoMockedData("https://en.wikipedia.org/wiki/Bencode", "https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FBencode")]
110 | public void CreateMagnetLink_Tracker_IsEscapedWhenEscapingEnabled(string tracker, string expected, string infoHash, string displayName)
111 | {
112 | var trackers = new List { tracker };
113 |
114 | var magnet = TorrentUtil.CreateMagnetLink(infoHash, displayName, trackers, MagnetLinkOptions.IncludeTrackers);
115 |
116 | magnet.Should().Be($"magnet:?xt=urn:btih:{infoHash}&dn={displayName}&tr={expected}");
117 | }
118 |
119 | [Theory]
120 | [AutoMockedData]
121 | public void CreateMagnetLink_Torrent_UsesInfoHashDisplayNameAndTrackersFromTorrent(string infoHash, string displayName, IList> trackers)
122 | {
123 | // Arrange
124 | var torrent = Substitute.For();
125 | torrent.GetInfoHash().Returns(infoHash);
126 | torrent.DisplayName.Returns(displayName);
127 | torrent.Trackers.Returns(trackers);
128 |
129 | // Act
130 | var expected = TorrentUtil.CreateMagnetLink(infoHash.ToLower(), displayName, trackers.SelectMany(x => x), MagnetLinkOptions.IncludeTrackers);
131 | var magnet = TorrentUtil.CreateMagnetLink(torrent);
132 |
133 | // Assert
134 | magnet.Should().Be(expected);
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/BencodeNET.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29403.142
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{65E984B8-653C-4BCD-AE37-4E21497B5B87}"
7 | ProjectSection(SolutionItems) = preProject
8 | .gitattributes = .gitattributes
9 | .gitignore = .gitignore
10 | azure-pipelines.yml = azure-pipelines.yml
11 | CHANGELOG.md = CHANGELOG.md
12 | GitVersion.yml = GitVersion.yml
13 | LICENSE.md = LICENSE.md
14 | README.md = README.md
15 | EndProjectSection
16 | EndProject
17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BencodeNET", "BencodeNET\BencodeNET.csproj", "{EA5E8A32-8EC7-4618-95BC-EAF95F1C2E52}"
18 | EndProject
19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BencodeNET.Tests", "BencodeNET.Tests\BencodeNET.Tests.csproj", "{5992CAC5-C0D3-4255-BFD7-C11C07969900}"
20 | EndProject
21 | Global
22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
23 | Debug|Any CPU = Debug|Any CPU
24 | Release|Any CPU = Release|Any CPU
25 | EndGlobalSection
26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
27 | {EA5E8A32-8EC7-4618-95BC-EAF95F1C2E52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {EA5E8A32-8EC7-4618-95BC-EAF95F1C2E52}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {EA5E8A32-8EC7-4618-95BC-EAF95F1C2E52}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {EA5E8A32-8EC7-4618-95BC-EAF95F1C2E52}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {5992CAC5-C0D3-4255-BFD7-C11C07969900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {5992CAC5-C0D3-4255-BFD7-C11C07969900}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {5992CAC5-C0D3-4255-BFD7-C11C07969900}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {5992CAC5-C0D3-4255-BFD7-C11C07969900}.Release|Any CPU.Build.0 = Release|Any CPU
35 | EndGlobalSection
36 | GlobalSection(SolutionProperties) = preSolution
37 | HideSolutionNode = FALSE
38 | EndGlobalSection
39 | GlobalSection(ExtensibilityGlobals) = postSolution
40 | SolutionGuid = {13FC1678-85AE-420D-A765-ED5FACE777A5}
41 | EndGlobalSection
42 | EndGlobal
43 |
--------------------------------------------------------------------------------
/BencodeNET/BencodeNET.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | 11
6 | True
7 | BencodeNET.ruleset
8 |
9 |
10 |
11 |
12 | full
13 |
14 |
15 |
16 | BencodeNET
17 | Søren Kruse
18 |
19 | BencodeNET
20 | A library for encoding and decoding bencode (e.g. torrent files)
21 | Unlicense
22 | https://github.com/Krusen/BencodeNET
23 | icon.png
24 | https://github.com/Krusen/BencodeNET
25 | git
26 |
27 | bencode;torrent;torrents
28 | False
29 | $(SemVer)
30 | True
31 | True
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/BencodeNET/BencodeNET.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/BencodeNET/Exceptions/BencodeException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | #pragma warning disable 1591
4 | namespace BencodeNET.Exceptions
5 | {
6 | ///
7 | /// Represents generic errors in this bencode library.
8 | ///
9 | public class BencodeException : Exception
10 | {
11 | public BencodeException()
12 | { }
13 |
14 | public BencodeException(string message)
15 | : base(message)
16 | { }
17 |
18 | public BencodeException(string message, Exception inner)
19 | : base(message, inner)
20 | { }
21 | }
22 |
23 | ///
24 | /// Represents generic errors in this bencode library related to a specific .
25 | ///
26 | /// The related type.
27 | public class BencodeException : BencodeException
28 | {
29 | ///
30 | /// The type related to this error. Usually the type being parsed.
31 | ///
32 | public Type RelatedType { get; } = typeof(T);
33 |
34 | public BencodeException()
35 | { }
36 |
37 | public BencodeException(string message)
38 | : base(message)
39 |
40 | { }
41 |
42 | public BencodeException(string message, Exception inner)
43 | : base(message, inner)
44 | { }
45 | }
46 | }
--------------------------------------------------------------------------------
/BencodeNET/Exceptions/InvalidBencodeException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | #pragma warning disable 1591
4 | namespace BencodeNET.Exceptions
5 | {
6 | ///
7 | /// Represents parse errors when encountering invalid bencode of some sort.
8 | ///
9 | /// The type being parsed.
10 | public class InvalidBencodeException : BencodeException
11 | {
12 | ///
13 | /// The position in the stream where the error happened or
14 | /// the starting position of the parsed object that caused the error.
15 | ///
16 | public long StreamPosition { get; set; }
17 |
18 | public InvalidBencodeException()
19 | { }
20 |
21 | public InvalidBencodeException(string message)
22 | : base(message)
23 | { }
24 |
25 | public InvalidBencodeException(string message, Exception inner)
26 | : base(message, inner)
27 | { }
28 |
29 | public InvalidBencodeException(string message, Exception inner, long streamPosition)
30 | : base($"Failed to parse {typeof(T).Name}. {message}", inner)
31 | {
32 | StreamPosition = Math.Max(0, streamPosition);
33 | }
34 |
35 | public InvalidBencodeException(string message, long streamPosition)
36 | : base($"Failed to parse {typeof(T).Name}. {message}")
37 | {
38 | StreamPosition = Math.Max(0, streamPosition);
39 | }
40 |
41 | internal static InvalidBencodeException InvalidBeginningChar(char invalidChar, long streamPosition)
42 | {
43 | var message =
44 | $"Invalid beginning character of object. Found '{invalidChar}' at position {streamPosition}. Valid characters are: 0-9, 'i', 'l' and 'd'";
45 | return new InvalidBencodeException(message, streamPosition);
46 | }
47 |
48 | internal static InvalidBencodeException MissingEndChar(long streamPosition)
49 | {
50 | var message = "Missing end character of object. Expected 'e' but reached end of stream.";
51 | return new InvalidBencodeException(message, streamPosition);
52 | }
53 |
54 | internal static InvalidBencodeException BelowMinimumLength(int minimumLength, long actualLength, long streamPosition)
55 | {
56 | var message =
57 | $"Invalid length. Minimum valid stream length for parsing '{typeof (T).FullName}' is {minimumLength} but the actual length was only {actualLength}.";
58 | return new InvalidBencodeException(message, streamPosition);
59 | }
60 |
61 | internal static InvalidBencodeException UnexpectedChar(char expected, char unexpected, long streamPosition)
62 | {
63 | var message = unexpected == default
64 | ? $"Unexpected character. Expected '{expected}' but reached end of stream."
65 | : $"Unexpected character. Expected '{expected}' but found '{unexpected}' at position {streamPosition}.";
66 | return new InvalidBencodeException(message, streamPosition);
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/BencodeNET/Exceptions/UnsupportedBencodeException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | #pragma warning disable 1591
4 | namespace BencodeNET.Exceptions
5 | {
6 | ///
7 | /// Represents parse errors for when encountering bencode that is potentially valid but not supported by this library.
8 | /// Usually numbers larger than or strings longer than that.
9 | ///
10 | ///
11 | public class UnsupportedBencodeException : BencodeException
12 | {
13 | public long StreamPosition { get; set; }
14 |
15 | public UnsupportedBencodeException()
16 | { }
17 |
18 | public UnsupportedBencodeException(string message)
19 | : base(message)
20 | { }
21 |
22 | public UnsupportedBencodeException(string message, Exception inner)
23 | : base(message, inner)
24 | { }
25 |
26 | public UnsupportedBencodeException(string message, long streamPosition)
27 | : base(message)
28 | {
29 | StreamPosition = streamPosition;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/BencodeNET/IO/BencodeReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace BencodeNET.IO
5 | {
6 | ///
7 | /// Reads bencode from a stream.
8 | ///
9 | public class BencodeReader : IDisposable
10 | {
11 | private readonly byte[] _tinyBuffer = new byte[1];
12 |
13 | private readonly Stream _stream;
14 | private readonly bool _leaveOpen;
15 | private readonly bool _supportsLength;
16 |
17 | private bool _hasPeeked;
18 | private char _peekedChar;
19 |
20 | ///
21 | /// The previously read/consumed char (does not include peeked char).
22 | ///
23 | public char PreviousChar { get; private set; }
24 |
25 | ///
26 | /// The position in the stream (does not included peeked char).
27 | ///
28 | public long Position { get; set; }
29 |
30 | ///
31 | /// The length of the stream, or null if the stream doesn't support the feature.
32 | ///
33 | public long? Length => _supportsLength ? _stream.Length : (long?) null;
34 |
35 | ///
36 | /// Returns true if the end of the stream has been reached.
37 | /// This is true if either is greater than or if next char is default(char).
38 | ///
39 | public bool EndOfStream => Position > Length || PeekChar() == default;
40 |
41 | ///
42 | /// Creates a new for the specified .
43 | ///
44 | /// The stream to read from.
45 | public BencodeReader(Stream stream)
46 | : this(stream, leaveOpen: false)
47 | {
48 | }
49 |
50 | ///
51 | /// Creates a new for the specified
52 | /// using the default buffer size of 40,960 bytes and the option of leaving the stream open after disposing of this instance.
53 | ///
54 | /// The stream to read from.
55 | /// Indicates if the stream should be left open when this is disposed.
56 | public BencodeReader(Stream stream, bool leaveOpen)
57 | {
58 | _stream = stream ?? throw new ArgumentNullException(nameof(stream));
59 | _leaveOpen = leaveOpen;
60 | try
61 | {
62 | _ = stream.Length;
63 | _supportsLength = true;
64 | }
65 | catch
66 | {
67 | _supportsLength = false;
68 | }
69 |
70 | if (!_stream.CanRead) throw new ArgumentException("The stream is not readable.", nameof(stream));
71 | }
72 |
73 | ///
74 | /// Peeks at the next character in the stream, or default(char) if the end of the stream has been reached.
75 | ///
76 | public char PeekChar()
77 | {
78 | if (_hasPeeked)
79 | return _peekedChar;
80 |
81 | var read = _stream.Read(_tinyBuffer, 0, 1);
82 |
83 | _peekedChar = read == 0 ? default : (char)_tinyBuffer[0];
84 | _hasPeeked = true;
85 |
86 | return _peekedChar;
87 | }
88 |
89 | ///
90 | /// Reads the next character from the stream.
91 | /// Returns default(char) if the end of the stream has been reached.
92 | ///
93 | public char ReadChar()
94 | {
95 | if (_hasPeeked)
96 | {
97 | _hasPeeked = _peekedChar == default; // If null then EOS so don't reset peek as peeking again will just be EOS again
98 | if (_peekedChar != default)
99 | Position++;
100 | return _peekedChar;
101 | }
102 |
103 | var read = _stream.Read(_tinyBuffer, 0, 1);
104 |
105 | PreviousChar = read == 0
106 | ? default
107 | : (char) _tinyBuffer[0];
108 |
109 | if (read > 0)
110 | Position++;
111 |
112 | return PreviousChar;
113 | }
114 |
115 | ///
116 | /// Reads into the by reading from the stream.
117 | /// Returns the number of bytes actually read from the stream.
118 | ///
119 | /// The buffer to read into.
120 | /// The number of bytes actually read from the stream and filled into the buffer.
121 | public int Read(byte[] buffer)
122 | {
123 | var totalRead = 0;
124 | if (_hasPeeked && _peekedChar != default)
125 | {
126 | buffer[0] = (byte) _peekedChar;
127 | totalRead = 1;
128 | _hasPeeked = false;
129 |
130 | // Just return right away if only reading this 1 byte
131 | if (buffer.Length == 1)
132 | {
133 | Position++;
134 | return 1;
135 | }
136 | }
137 |
138 | int read = -1;
139 | while (read != 0 && totalRead < buffer.Length)
140 | {
141 | read = _stream.Read(buffer, totalRead, buffer.Length - totalRead);
142 | totalRead += read;
143 | }
144 |
145 | if (totalRead > 0)
146 | PreviousChar = (char) buffer[totalRead - 1];
147 |
148 | Position += totalRead;
149 |
150 | return totalRead;
151 | }
152 |
153 | ///
154 | public void Dispose()
155 | {
156 | Dispose(true);
157 | }
158 |
159 | ///
160 | protected virtual void Dispose(bool disposing)
161 | {
162 | if (!disposing) return;
163 |
164 | if (_stream != null && !_leaveOpen)
165 | _stream.Dispose();
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/BencodeNET/IO/PipeBencodeReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.IO.Pipelines;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace BencodeNET.IO
8 | {
9 | ///
10 | /// Reads chars and bytes from a .
11 | ///
12 | public class PipeBencodeReader
13 | {
14 | ///
15 | /// The to read from.
16 | ///
17 | protected PipeReader Reader { get; }
18 |
19 | ///
20 | /// Indicates if the has been completed (i.e. "end of stream").
21 | ///
22 | protected bool ReaderCompleted { get; set; }
23 |
24 | ///
25 | /// The position in the pipe (number of read bytes/characters) (does not included peeked char).
26 | ///
27 | public virtual long Position { get; protected set; }
28 |
29 | ///
30 | /// The previously read/consumed char (does not include peeked char).
31 | ///
32 | public virtual char PreviousChar { get; protected set; }
33 |
34 | ///
35 | /// Creates a that reads from the specified .
36 | ///
37 | ///
38 | public PipeBencodeReader(PipeReader reader)
39 | {
40 | Reader = reader;
41 | }
42 |
43 | ///
44 | /// Peek at the next char in the pipe, without advancing the reader.
45 | ///
46 | public virtual ValueTask PeekCharAsync(CancellationToken cancellationToken = default)
47 | => ReadCharAsync(peek: true, cancellationToken);
48 |
49 | ///
50 | /// Read the next char in the pipe and advance the reader.
51 | ///
52 | public virtual ValueTask ReadCharAsync(CancellationToken cancellationToken = default)
53 | => ReadCharAsync(peek: false, cancellationToken);
54 |
55 | private ValueTask ReadCharAsync(bool peek = false, CancellationToken cancellationToken = default)
56 | {
57 | if (ReaderCompleted)
58 | return new ValueTask(default(char));
59 |
60 | if (Reader.TryRead(out var result))
61 | return new ValueTask(ReadCharConsume(result.Buffer, peek));
62 |
63 | return ReadCharAwaitedAsync(peek, cancellationToken);
64 | }
65 |
66 | private async ValueTask ReadCharAwaitedAsync(bool peek, CancellationToken cancellationToken)
67 | {
68 | var result = await Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
69 | return ReadCharConsume(result.Buffer, peek);
70 | }
71 |
72 | ///
73 | /// Reads the next char in the pipe and consumes it (advances the reader),
74 | /// unless is true.
75 | ///
76 | /// The buffer to read from
77 | /// If true the char will not be consumed, i.e. the reader should not be advanced.
78 | protected virtual char ReadCharConsume(in ReadOnlySequence buffer, bool peek)
79 | {
80 | if (buffer.IsEmpty)
81 | {
82 | // TODO: Add IsCompleted check?
83 | ReaderCompleted = true;
84 | return default;
85 | }
86 |
87 | var c = (char) buffer.First.Span[0];
88 |
89 | if (peek)
90 | {
91 | // Advance reader to start (i.e. don't advance)
92 | Reader.AdvanceTo(buffer.Start);
93 | return c;
94 | }
95 |
96 | // Consume char by advancing reader
97 | Position++;
98 | PreviousChar = c;
99 | Reader.AdvanceTo(buffer.GetPosition(1));
100 | return c;
101 | }
102 |
103 | ///
104 | /// Read bytes from the pipe.
105 | /// Returns the number of bytes actually read.
106 | ///
107 | /// The amount of bytes to read.
108 | ///
109 | public virtual ValueTask ReadAsync(Memory bytes, CancellationToken cancellationToken = default)
110 | {
111 | if (bytes.Length == 0 || ReaderCompleted)
112 | return new ValueTask(0);
113 |
114 | if (Reader.TryRead(out var result) && TryReadConsume(result, bytes.Span, out var bytesRead))
115 | {
116 | return new ValueTask(bytesRead);
117 | }
118 |
119 | return ReadAwaitedAsync(bytes, cancellationToken);
120 | }
121 |
122 | private async ValueTask ReadAwaitedAsync(Memory bytes, CancellationToken cancellationToken)
123 | {
124 | while (true)
125 | {
126 | var result = await Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
127 | if (TryReadConsume(result, bytes.Span, out var bytesRead))
128 | {
129 | return bytesRead;
130 | }
131 | }
132 | }
133 |
134 | ///
135 | /// Attempts to read the specified bytes from the reader and advances the reader if successful.
136 | /// If the end of the pipe is reached then the available bytes is read and returned, if any.
137 | ///
138 | /// Returns true if any bytes was read or the reader was completed.
139 | ///
140 | ///
141 | /// The read result from the pipe read operation.
142 | /// The bytes to read.
143 | /// The number of bytes read.
144 | ///
145 | protected virtual bool TryReadConsume(ReadResult result, in Span bytes, out long bytesRead)
146 | {
147 | if (result.IsCanceled) throw new InvalidOperationException("Read operation cancelled.");
148 |
149 | var buffer = result.Buffer;
150 |
151 | // Check if enough bytes have been read
152 | if (buffer.Length >= bytes.Length)
153 | {
154 | // Copy requested amount of bytes from buffer and advance reader
155 | buffer.Slice(0, bytes.Length).CopyTo(bytes);
156 | Position += bytes.Length;
157 | PreviousChar = (char) bytes[bytes.Length - 1];
158 | bytesRead = bytes.Length;
159 | Reader.AdvanceTo(buffer.GetPosition(bytes.Length));
160 | return true;
161 | }
162 |
163 | if (result.IsCompleted)
164 | {
165 | ReaderCompleted = true;
166 |
167 | if (buffer.IsEmpty)
168 | {
169 | bytesRead = 0;
170 | return true;
171 | }
172 |
173 | // End of pipe reached, less bytes available than requested
174 | // Copy available bytes and advance reader to the end
175 | buffer.CopyTo(bytes);
176 | Position += buffer.Length;
177 | PreviousChar = (char) buffer.Slice(buffer.Length - 1).First.Span[0];
178 | bytesRead = buffer.Length;
179 | Reader.AdvanceTo(buffer.End);
180 | return true;
181 | }
182 |
183 | // Not enough bytes read, advance reader
184 | Reader.AdvanceTo(buffer.Start, buffer.End);
185 |
186 | bytesRead = -1;
187 | return false; // Consume unsuccessful
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/BencodeNET/Objects/BNumber.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Pipelines;
4 | using System.Text;
5 |
6 | namespace BencodeNET.Objects
7 | {
8 | ///
9 | /// Represents a bencoded number (integer).
10 | ///
11 | ///
12 | /// The underlying value is a .
13 | ///
14 | public sealed class BNumber : BObject, IComparable
15 | {
16 | private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
17 |
18 | ///
19 | /// The string-length of long.MaxValue. Longer strings cannot be parsed.
20 | ///
21 | internal const int MaxDigits = 19;
22 |
23 | ///
24 | /// The underlying value.
25 | ///
26 | public override long Value { get; }
27 |
28 | ///
29 | /// Create a from a .
30 | ///
31 | public BNumber(long value)
32 | {
33 | Value = value;
34 | }
35 |
36 | ///
37 | /// Create a from a .
38 | ///
39 | ///
40 | /// Bencode dates are stored in unix format (seconds since epoch).
41 | ///
42 | public BNumber(DateTime? datetime)
43 | {
44 | Value = datetime?.Subtract(Epoch).Ticks / TimeSpan.TicksPerSecond ?? 0;
45 | }
46 |
47 | ///
48 | public override int GetSizeInBytes() => Value.DigitCount() + 2;
49 |
50 | ///
51 | protected override void EncodeObject(Stream stream)
52 | {
53 | stream.Write('i');
54 | stream.Write(Value);
55 | stream.Write('e');
56 | }
57 |
58 | ///
59 | protected override void EncodeObject(PipeWriter writer)
60 | {
61 | var size = GetSizeInBytes();
62 | var buffer = writer.GetSpan(size).Slice(0, size);
63 |
64 | buffer[0] = (byte) 'i';
65 | buffer = buffer.Slice(1);
66 |
67 | Encoding.ASCII.GetBytes(Value.ToString().AsSpan(), buffer);
68 |
69 | buffer[buffer.Length - 1] = (byte) 'e';
70 |
71 | writer.Advance(size);
72 | }
73 |
74 | #pragma warning disable 1591
75 | public static implicit operator int?(BNumber bint)
76 | {
77 | if (bint == null) return null;
78 | return (int)bint.Value;
79 | }
80 |
81 | public static implicit operator long?(BNumber bint)
82 | {
83 | if (bint == null) return null;
84 | return bint.Value;
85 | }
86 |
87 | public static implicit operator int(BNumber bint)
88 | {
89 | if (bint == null) throw new InvalidCastException();
90 | return (int)bint.Value;
91 | }
92 |
93 | public static implicit operator long(BNumber bint)
94 | {
95 | if (bint == null) throw new InvalidCastException();
96 | return bint.Value;
97 | }
98 |
99 | public static implicit operator bool(BNumber bint)
100 | {
101 | if (bint == null) throw new InvalidCastException();
102 | return bint.Value > 0;
103 | }
104 |
105 | public static implicit operator DateTime?(BNumber number)
106 | {
107 | if (number == null) return null;
108 |
109 | if (number.Value > int.MaxValue)
110 | {
111 | try
112 | {
113 | return Epoch.AddMilliseconds(number);
114 | }
115 | catch (ArgumentOutOfRangeException)
116 | {
117 | return Epoch;
118 | }
119 | }
120 |
121 | return Epoch.AddSeconds(number);
122 | }
123 |
124 | public static implicit operator BNumber(int value) => new BNumber(value);
125 |
126 | public static implicit operator BNumber(long value) => new BNumber(value);
127 |
128 | public static implicit operator BNumber(bool value) => new BNumber(value ? 1 : 0);
129 |
130 | public static implicit operator BNumber(DateTime? datetime) => new BNumber(datetime);
131 |
132 | public static bool operator ==(BNumber bnumber, BNumber other)
133 | {
134 | return bnumber?.Value == other?.Value;
135 | }
136 |
137 | public static bool operator !=(BNumber bnumber, BNumber other) => !(bnumber == other);
138 |
139 | public override bool Equals(object other)
140 | {
141 | var bnumber = other as BNumber;
142 | return Value == bnumber?.Value;
143 | }
144 |
145 | ///
146 | /// Returns the hash code for this instance.
147 | ///
148 | public override int GetHashCode() => Value.GetHashCode();
149 |
150 | public int CompareTo(BNumber other)
151 | {
152 | if (other == null)
153 | return 1;
154 |
155 | return Value.CompareTo(other.Value);
156 | }
157 |
158 | public override string ToString() => Value.ToString();
159 |
160 | public string ToString(string format) => Value.ToString(format);
161 |
162 | public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);
163 |
164 | public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider);
165 | #pragma warning restore 1591
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/BencodeNET/Objects/BObject.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Pipelines;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace BencodeNET.Objects
7 | {
8 | ///
9 | /// Abstract base class with default implementation of most methods of .
10 | ///
11 | public abstract class BObject : IBObject
12 | {
13 | internal BObject()
14 | { }
15 |
16 | ///
17 | /// Calculates the (encoded) size of the object in bytes.
18 | ///
19 | public abstract int GetSizeInBytes();
20 |
21 | ///
22 | /// Writes the object as bencode to the specified stream.
23 | ///
24 | /// The type of stream.
25 | /// The stream to write to.
26 | /// The used stream.
27 | public TStream EncodeTo(TStream stream) where TStream : Stream
28 | {
29 | var size = GetSizeInBytes();
30 | stream.TrySetLength(size);
31 | EncodeObject(stream);
32 | return stream;
33 | }
34 |
35 | ///
36 | /// Writes the object as bencode to the specified without flushing the writer,
37 | /// you should do that manually.
38 | ///
39 | /// The writer to write to.
40 | public void EncodeTo(PipeWriter writer)
41 | {
42 | EncodeObject(writer);
43 | }
44 |
45 | ///
46 | /// Writes the object as bencode to the specified and flushes the writer afterwards.
47 | ///
48 | /// The writer to write to.
49 | ///
50 | public ValueTask EncodeToAsync(PipeWriter writer, CancellationToken cancellationToken = default)
51 | {
52 | return EncodeObjectAsync(writer, cancellationToken);
53 | }
54 |
55 | ///
56 | /// Writes the object asynchronously as bencode to the specified using a .
57 | ///
58 | /// The stream to write to.
59 | /// The options for the .
60 | ///
61 | public ValueTask EncodeToAsync(Stream stream, StreamPipeWriterOptions writerOptions = null, CancellationToken cancellationToken = default)
62 | {
63 | return EncodeObjectAsync(PipeWriter.Create(stream, writerOptions), cancellationToken);
64 | }
65 |
66 | ///
67 | /// Implementations of this method should encode their
68 | /// underlying value to bencode and write it to the stream.
69 | ///
70 | /// The stream to encode to.
71 | protected abstract void EncodeObject(Stream stream);
72 |
73 | ///
74 | /// Implementations of this method should encode their underlying value to bencode and write it to the .
75 | ///
76 | /// The writer to encode to.
77 | protected abstract void EncodeObject(PipeWriter writer);
78 |
79 | ///
80 | /// Encodes and writes the underlying value to the and flushes the writer afterwards.
81 | ///
82 | /// The writer to encode to.
83 | ///
84 | protected virtual ValueTask EncodeObjectAsync(PipeWriter writer, CancellationToken cancellationToken)
85 | {
86 | EncodeObject(writer);
87 | return writer.FlushAsync(cancellationToken);
88 | }
89 | }
90 |
91 | ///
92 | /// Base class of bencode objects with a specific underlying value type.
93 | ///
94 | /// Type of the underlying value.
95 | public abstract class BObject : BObject
96 | {
97 | internal BObject()
98 | { }
99 |
100 | ///
101 | /// The underlying value of the .
102 | ///
103 | public abstract T Value { get; }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/BencodeNET/Objects/BObjectExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.IO;
4 | using System.Text;
5 |
6 | namespace BencodeNET.Objects
7 | {
8 | ///
9 | /// Extensions to simplify encoding directly as a string or byte array.
10 | ///
11 | public static class BObjectExtensions
12 | {
13 | ///
14 | /// Encodes the object and returns the result as a string using .
15 | ///
16 | /// The object bencoded and converted to a string using .
17 | public static string EncodeAsString(this IBObject bobject) => EncodeAsString(bobject, Encoding.UTF8);
18 |
19 | ///
20 | /// Encodes the byte-string as bencode and returns the encoded string.
21 | /// Uses the current value of the property.
22 | ///
23 | /// The byte-string as a bencoded string.
24 | public static string EncodeAsString(this BString bstring) => EncodeAsString(bstring, bstring.Encoding);
25 |
26 | ///
27 | /// Encodes the object and returns the result as a string using the specified encoding.
28 | ///
29 | ///
30 | /// The encoding used to convert the encoded bytes to a string.
31 | /// The object bencoded and converted to a string using the specified encoding.
32 | public static string EncodeAsString(this IBObject bobject, Encoding encoding)
33 | {
34 | var size = bobject.GetSizeInBytes();
35 | var buffer = ArrayPool.Shared.Rent(size);
36 | try
37 | {
38 | using (var stream = new MemoryStream(buffer))
39 | {
40 | bobject.EncodeTo(stream);
41 | return encoding.GetString(buffer.AsSpan().Slice(0, size));
42 | }
43 | }
44 | finally { ArrayPool.Shared.Return(buffer); }
45 | }
46 |
47 | ///
48 | /// Encodes the object and returns the raw bytes.
49 | ///
50 | /// The raw bytes of the bencoded object.
51 | public static byte[] EncodeAsBytes(this IBObject bobject)
52 | {
53 | var size = bobject.GetSizeInBytes();
54 | var bytes = new byte[size];
55 | using (var stream = new MemoryStream(bytes))
56 | {
57 | bobject.EncodeTo(stream);
58 | return bytes;
59 | }
60 | }
61 |
62 | ///
63 | /// Writes the object as bencode to the specified file path.
64 | ///
65 | ///
66 | /// The file path to write the encoded object to.
67 | public static void EncodeTo(this IBObject bobject, string filePath)
68 | {
69 | using (var stream = File.OpenWrite(filePath))
70 | {
71 | bobject.EncodeTo(stream);
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/BencodeNET/Objects/BString.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Pipelines;
4 | using System.Text;
5 |
6 | namespace BencodeNET.Objects
7 | {
8 | ///
9 | /// Represents a bencoded string, i.e. a byte-string.
10 | /// It isn't necessarily human-readable.
11 | ///
12 | ///
13 | /// The underlying value is a array.
14 | ///
15 | public sealed class BString : BObject>, IComparable
16 | {
17 | ///
18 | /// The maximum number of digits that can be handled as the length part of a bencoded string.
19 | ///
20 | internal const int LengthMaxDigits = 10;
21 |
22 | ///
23 | /// The underlying bytes of the string.
24 | ///
25 | public override ReadOnlyMemory Value { get; }
26 |
27 | ///
28 | /// Gets the length of the string in bytes.
29 | ///
30 | public int Length => Value.Length;
31 |
32 | private static readonly Encoding DefaultEncoding = Encoding.UTF8;
33 |
34 | ///
35 | /// Gets or sets the encoding used as the default with ToString().
36 | ///
37 | ///
38 | public Encoding Encoding
39 | {
40 | get => _encoding;
41 | set => _encoding = value ?? DefaultEncoding;
42 | }
43 | private Encoding _encoding;
44 |
45 | ///
46 | /// Creates an empty ('0:').
47 | ///
48 | public BString()
49 | : this((string)null)
50 | {
51 | }
52 |
53 | ///
54 | /// Creates a from bytes with the specified encoding.
55 | ///
56 | /// The bytes representing the data.
57 | /// The encoding of the bytes. Defaults to .
58 | public BString(byte[] bytes, Encoding encoding = null)
59 | {
60 | Value = bytes ?? throw new ArgumentNullException(nameof(bytes));
61 | _encoding = encoding ?? DefaultEncoding;
62 | }
63 |
64 | ///
65 | /// Creates a using the specified encoding to convert the string to bytes.
66 | ///
67 | /// The string.
68 | /// The encoding used to convert the string to bytes.
69 | ///
70 | public BString(string str, Encoding encoding = null)
71 | {
72 | _encoding = encoding ?? DefaultEncoding;
73 |
74 | if (string.IsNullOrEmpty(str))
75 | {
76 | Value = Array.Empty();
77 | }
78 | else
79 | {
80 | var maxByteCount = _encoding.GetMaxByteCount(str.Length);
81 | var span = new byte[maxByteCount].AsSpan();
82 |
83 | var length = _encoding.GetBytes(str.AsSpan(), span);
84 |
85 | Value = span.Slice(0, length).ToArray();
86 | }
87 | }
88 |
89 | ///
90 | public override int GetSizeInBytes() => Value.Length + 1 + Value.Length.DigitCount();
91 |
92 | ///
93 | protected override void EncodeObject(Stream stream)
94 | {
95 | stream.Write(Value.Length);
96 | stream.Write(':');
97 | stream.Write(Value.Span);
98 | }
99 |
100 | ///
101 | protected override void EncodeObject(PipeWriter writer)
102 | {
103 | // Init
104 | var size = GetSizeInBytes();
105 | var buffer = writer.GetSpan(size);
106 |
107 | // Write length
108 | var writtenBytes = Encoding.GetBytes(Value.Length.ToString().AsSpan(), buffer);
109 |
110 | // Write ':'
111 | buffer[writtenBytes] = (byte) ':';
112 |
113 | // Write value
114 | Value.Span.CopyTo(buffer.Slice(writtenBytes + 1));
115 |
116 | // Commit
117 | writer.Advance(size);
118 | }
119 |
120 | #pragma warning disable 1591
121 | public static implicit operator BString(string value) => new BString(value);
122 |
123 | public static bool operator ==(BString first, BString second)
124 | {
125 | return first?.Equals(second) ?? second is null;
126 | }
127 |
128 | public static bool operator !=(BString first, BString second) => !(first == second);
129 |
130 | public override bool Equals(object other) => other is BString bstring && Value.Span.SequenceEqual(bstring.Value.Span);
131 |
132 | public bool Equals(BString bstring) => bstring != null && Value.Span.SequenceEqual(bstring.Value.Span);
133 |
134 | public override int GetHashCode()
135 | {
136 | var bytesToHash = Math.Min(Value.Length, 32);
137 |
138 | long hashValue = 0;
139 | for (var i = 0; i < bytesToHash; i++)
140 | {
141 | hashValue = (37 * hashValue + Value.Span[i]) % int.MaxValue;
142 | }
143 |
144 | return (int)hashValue;
145 | }
146 |
147 | public int CompareTo(BString other)
148 | {
149 | return Value.Span.SequenceCompareTo(other.Value.Span);
150 | }
151 | #pragma warning restore 1591
152 |
153 | ///
154 | /// Converts the underlying bytes to a string representation using the current value of the property.
155 | ///
156 | ///
157 | /// A that represents this instance.
158 | ///
159 | public override string ToString()
160 | {
161 | return _encoding.GetString(Value.Span);
162 | }
163 |
164 | ///
165 | /// Converts the underlying bytes to a string representation using the specified encoding.
166 | ///
167 | /// The encoding to use to convert the underlying byte array to a .
168 | ///
169 | /// A that represents this instance.
170 | ///
171 | public string ToString(Encoding encoding)
172 | {
173 | encoding ??= _encoding;
174 | return encoding.GetString(Value.Span);
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/BencodeNET/Objects/IBObject.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Pipelines;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace BencodeNET.Objects
7 | {
8 | ///
9 | /// Represent a bencode value that can be encoded to bencode.
10 | ///
11 | public interface IBObject
12 | {
13 | ///
14 | /// Calculates the (encoded) size of the object in bytes.
15 | ///
16 | int GetSizeInBytes();
17 |
18 | ///
19 | /// Writes the object as bencode to the specified stream.
20 | ///
21 | /// The type of stream.
22 | /// The stream to write to.
23 | /// The used stream.
24 | TStream EncodeTo(TStream stream) where TStream : Stream;
25 |
26 | ///
27 | /// Writes the object as bencode to the specified without flushing the writer,
28 | /// you should do that manually.
29 | ///
30 | /// The writer to write to.
31 | void EncodeTo(PipeWriter writer);
32 |
33 | ///
34 | /// Writes the object as bencode to the specified and flushes the writer afterwards.
35 | ///
36 | /// The writer to write to.
37 | ///
38 | ValueTask EncodeToAsync(PipeWriter writer, CancellationToken cancellationToken = default);
39 |
40 | ///
41 | /// Writes the object asynchronously as bencode to the specified using a .
42 | ///
43 | /// The stream to write to.
44 | /// The options for the .
45 | ///
46 | ValueTask EncodeToAsync(Stream stream, StreamPipeWriterOptions writerOptions = null, CancellationToken cancellationToken = default);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/BencodeNET/Parsing/BDictionaryParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using BencodeNET.Exceptions;
6 | using BencodeNET.IO;
7 | using BencodeNET.Objects;
8 |
9 | namespace BencodeNET.Parsing
10 | {
11 | ///
12 | /// A parser for bencoded dictionaries.
13 | ///
14 | public class BDictionaryParser : BObjectParser
15 | {
16 | ///
17 | /// The minimum stream length in bytes for a valid dictionary ('de').
18 | ///
19 | protected const int MinimumLength = 2;
20 |
21 | ///
22 | /// Creates an instance using the specified for parsing contained keys and values.
23 | ///
24 | /// The parser used for contained keys and values.
25 | public BDictionaryParser(IBencodeParser bencodeParser)
26 | {
27 | BencodeParser = bencodeParser ?? throw new ArgumentNullException(nameof(bencodeParser));
28 | }
29 |
30 | ///
31 | /// The parser used for parsing contained keys and values.
32 | ///
33 | protected IBencodeParser BencodeParser { get; set; }
34 |
35 | ///
36 | /// The encoding used for parsing.
37 | ///
38 | public override Encoding Encoding => BencodeParser.Encoding;
39 |
40 | ///
41 | /// Parses the next and its contained keys and values from the reader.
42 | ///
43 | /// The reader to parse from.
44 | /// The parsed .
45 | /// Invalid bencode.
46 | public override BDictionary Parse(BencodeReader reader)
47 | {
48 | if (reader == null) throw new ArgumentNullException(nameof(reader));
49 |
50 | if (reader.Length < MinimumLength)
51 | throw InvalidBencodeException.BelowMinimumLength(MinimumLength, reader.Length.Value, reader.Position);
52 |
53 | var startPosition = reader.Position;
54 |
55 | // Dictionaries must start with 'd'
56 | if (reader.ReadChar() != 'd')
57 | throw InvalidBencodeException.UnexpectedChar('d', reader.PreviousChar, startPosition);
58 |
59 | var dictionary = new BDictionary();
60 | // Loop until next character is the end character 'e' or end of stream
61 | while (reader.PeekChar() != 'e' && reader.PeekChar() != default)
62 | {
63 | BString key;
64 | try
65 | {
66 | // Decode next string in stream as the key
67 | key = BencodeParser.Parse(reader);
68 | }
69 | catch (BencodeException ex)
70 | {
71 | throw InvalidException("Could not parse dictionary key. Keys must be strings.", ex, startPosition);
72 | }
73 |
74 | IBObject value;
75 | try
76 | {
77 | // Decode next object in stream as the value
78 | value = BencodeParser.Parse(reader);
79 | }
80 | catch (BencodeException ex)
81 | {
82 | throw InvalidException($"Could not parse dictionary value for the key '{key}'. There needs to be a value for each key.", ex, startPosition);
83 | }
84 |
85 | if (dictionary.ContainsKey(key))
86 | {
87 | throw InvalidException($"The dictionary already contains the key '{key}'. Duplicate keys are not supported.", startPosition);
88 | }
89 |
90 | dictionary.Add(key, value);
91 | }
92 |
93 | if (reader.ReadChar() != 'e')
94 | throw InvalidBencodeException.MissingEndChar(startPosition);
95 |
96 | return dictionary;
97 | }
98 |
99 | ///
100 | /// Parses the next and its contained keys and values from the reader.
101 | ///
102 | /// The reader to parse from.
103 | ///
104 | /// The parsed .
105 | /// Invalid bencode.
106 | public override async ValueTask ParseAsync(PipeBencodeReader reader, CancellationToken cancellationToken = default)
107 | {
108 | if (reader == null) throw new ArgumentNullException(nameof(reader));
109 |
110 | var startPosition = reader.Position;
111 |
112 | // Dictionaries must start with 'd'
113 | if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'd')
114 | throw InvalidBencodeException.UnexpectedChar('d', reader.PreviousChar, startPosition);
115 |
116 | var dictionary = new BDictionary();
117 | // Loop until next character is the end character 'e' or end of stream
118 | while (await reader.PeekCharAsync(cancellationToken).ConfigureAwait(false) != 'e' &&
119 | await reader.PeekCharAsync(cancellationToken).ConfigureAwait(false) != default)
120 | {
121 | BString key;
122 | try
123 | {
124 | // Decode next string in stream as the key
125 | key = await BencodeParser.ParseAsync(reader, cancellationToken).ConfigureAwait(false);
126 | }
127 | catch (BencodeException ex)
128 | {
129 | throw InvalidException("Could not parse dictionary key. Keys must be strings.", ex, startPosition);
130 | }
131 |
132 | IBObject value;
133 | try
134 | {
135 | // Decode next object in stream as the value
136 | value = await BencodeParser.ParseAsync(reader, cancellationToken).ConfigureAwait(false);
137 | }
138 | catch (BencodeException ex)
139 | {
140 | throw InvalidException($"Could not parse dictionary value for the key '{key}'. There needs to be a value for each key.", ex, startPosition);
141 | }
142 |
143 | if (dictionary.ContainsKey(key))
144 | {
145 | throw InvalidException($"The dictionary already contains the key '{key}'. Duplicate keys are not supported.", startPosition);
146 | }
147 |
148 | dictionary.Add(key, value);
149 | }
150 |
151 | if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'e')
152 | throw InvalidBencodeException.MissingEndChar(startPosition);
153 |
154 | return dictionary;
155 | }
156 |
157 | private static InvalidBencodeException InvalidException(string message, long startPosition)
158 | {
159 | return new InvalidBencodeException(
160 | $"{message} The dictionary starts at position {startPosition}.", startPosition);
161 | }
162 |
163 | private static InvalidBencodeException InvalidException(string message, Exception inner, long startPosition)
164 | {
165 | return new InvalidBencodeException(
166 | $"{message} The dictionary starts at position {startPosition}.",
167 | inner, startPosition);
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/BencodeNET/Parsing/BListParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using BencodeNET.Exceptions;
6 | using BencodeNET.IO;
7 | using BencodeNET.Objects;
8 |
9 | namespace BencodeNET.Parsing
10 | {
11 | ///
12 | /// A parser for bencoded lists.
13 | ///
14 | public class BListParser : BObjectParser
15 | {
16 | ///
17 | /// The minimum stream length in bytes for a valid list ('le').
18 | ///
19 | protected const int MinimumLength = 2;
20 |
21 | ///
22 | /// Creates an instance using the specified for parsing contained objects.
23 | ///
24 | /// The parser used for parsing contained objects.
25 | public BListParser(IBencodeParser bencodeParser)
26 | {
27 | BencodeParser = bencodeParser ?? throw new ArgumentNullException(nameof(bencodeParser));
28 | }
29 |
30 | ///
31 | /// The parser used for parsing contained objects.
32 | ///
33 | protected IBencodeParser BencodeParser { get; set; }
34 |
35 | ///
36 | /// The encoding used for parsing.
37 | ///
38 | public override Encoding Encoding => BencodeParser.Encoding;
39 |
40 | ///
41 | /// Parses the next from the reader.
42 | ///
43 | /// The reader to parse from.
44 | /// The parsed .
45 | /// Invalid bencode.
46 | public override BList Parse(BencodeReader reader)
47 | {
48 | if (reader == null) throw new ArgumentNullException(nameof(reader));
49 |
50 | if (reader.Length < MinimumLength)
51 | throw InvalidBencodeException.BelowMinimumLength(MinimumLength, reader.Length.Value, reader.Position);
52 |
53 | var startPosition = reader.Position;
54 |
55 | // Lists must start with 'l'
56 | if (reader.ReadChar() != 'l')
57 | throw InvalidBencodeException.UnexpectedChar('l', reader.PreviousChar, startPosition);
58 |
59 | var list = new BList();
60 | // Loop until next character is the end character 'e' or end of stream
61 | while (reader.PeekChar() != 'e' && reader.PeekChar() != default)
62 | {
63 | // Decode next object in stream
64 | var bObject = BencodeParser.Parse(reader);
65 | list.Add(bObject);
66 | }
67 |
68 | if (reader.ReadChar() != 'e')
69 | throw InvalidBencodeException.MissingEndChar(startPosition);
70 |
71 | return list;
72 | }
73 |
74 | ///
75 | /// Parses the next from the reader.
76 | ///
77 | /// The reader to parse from.
78 | ///
79 | /// The parsed .
80 | /// Invalid bencode.
81 | public override async ValueTask ParseAsync(PipeBencodeReader reader, CancellationToken cancellationToken = default)
82 | {
83 | if (reader == null) throw new ArgumentNullException(nameof(reader));
84 |
85 | var startPosition = reader.Position;
86 |
87 | // Lists must start with 'l'
88 | if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'l')
89 | throw InvalidBencodeException.UnexpectedChar('l', reader.PreviousChar, startPosition);
90 |
91 | var list = new BList();
92 | // Loop until next character is the end character 'e' or end of stream
93 | while (await reader.PeekCharAsync(cancellationToken).ConfigureAwait(false) != 'e' &&
94 | await reader.PeekCharAsync(cancellationToken).ConfigureAwait(false) != default)
95 | {
96 | // Decode next object in stream
97 | var bObject = await BencodeParser.ParseAsync(reader, cancellationToken).ConfigureAwait(false);
98 | list.Add(bObject);
99 | }
100 |
101 | if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'e')
102 | throw InvalidBencodeException.MissingEndChar(startPosition);
103 |
104 | return list;
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/BencodeNET/Parsing/BNumberParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using BencodeNET.Exceptions;
8 | using BencodeNET.IO;
9 | using BencodeNET.Objects;
10 |
11 | namespace BencodeNET.Parsing
12 | {
13 | ///
14 | /// A parser for bencoded numbers.
15 | ///
16 | public class BNumberParser : BObjectParser
17 | {
18 | ///
19 | /// The minimum stream length in bytes for a valid number ('i0e').
20 | ///
21 | protected const int MinimumLength = 3;
22 |
23 | ///
24 | /// The encoding used for parsing.
25 | ///
26 | public override Encoding Encoding => Encoding.UTF8;
27 |
28 | ///
29 | /// Parses the next from the reader.
30 | ///
31 | /// The reader to parse from.
32 | /// The parsed .
33 | /// Invalid bencode.
34 | /// The bencode is unsupported by this library.
35 | public override BNumber Parse(BencodeReader reader)
36 | {
37 | if (reader == null) throw new ArgumentNullException(nameof(reader));
38 |
39 | if (reader.Length < MinimumLength)
40 | throw InvalidBencodeException.BelowMinimumLength(MinimumLength, reader.Length.Value, reader.Position);
41 |
42 | var startPosition = reader.Position;
43 |
44 | // Numbers must start with 'i'
45 | if (reader.ReadChar() != 'i')
46 | throw InvalidBencodeException.UnexpectedChar('i', reader.PreviousChar, startPosition);
47 |
48 | var digits = ArrayPool.Shared.Rent(BNumber.MaxDigits);
49 | try
50 | {
51 | var digitCount = 0;
52 | for (var c = reader.ReadChar(); c != default && c != 'e'; c = reader.ReadChar())
53 | {
54 | digits[digitCount++] = c;
55 | }
56 |
57 | if (digitCount == 0)
58 | throw NoDigitsException(startPosition);
59 |
60 | // Last read character should be 'e'
61 | if (reader.PreviousChar != 'e')
62 | throw InvalidBencodeException.MissingEndChar(startPosition);
63 |
64 | return ParseNumber(digits[..digitCount], startPosition);
65 | }
66 | finally
67 | {
68 | ArrayPool.Shared.Return(digits);
69 | }
70 | }
71 |
72 | ///
73 | /// Parses the next from the reader.
74 | ///
75 | /// The reader to parse from.
76 | ///
77 | /// The parsed .
78 | /// Invalid bencode.
79 | /// The bencode is unsupported by this library.
80 | public override async ValueTask ParseAsync(PipeBencodeReader reader, CancellationToken cancellationToken = default)
81 | {
82 | if (reader == null) throw new ArgumentNullException(nameof(reader));
83 |
84 | var startPosition = reader.Position;
85 |
86 | // Numbers must start with 'i'
87 | if (await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false) != 'i')
88 | throw InvalidBencodeException.UnexpectedChar('i', reader.PreviousChar, startPosition);
89 |
90 | var digits = ArrayPool.Shared.Rent(BNumber.MaxDigits);
91 | try
92 | {
93 | var digitCount = 0;
94 | for (var c = await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false);
95 | c != default && c != 'e';
96 | c = await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false))
97 | {
98 | digits[digitCount++] = c;
99 | }
100 |
101 | if (digitCount == 0)
102 | throw NoDigitsException(startPosition);
103 |
104 | // Last read character should be 'e'
105 | if (reader.PreviousChar != 'e')
106 | throw InvalidBencodeException.MissingEndChar(startPosition);
107 |
108 | return ParseNumber(digits.AsSpan()[..digitCount], startPosition);
109 | }
110 | finally
111 | {
112 | ArrayPool.Shared.Return(digits);
113 | }
114 | }
115 |
116 | private BNumber ParseNumber(in ReadOnlySpan digits, long startPosition)
117 | {
118 | var isNegative = digits[0] == '-';
119 | var numberOfDigits = isNegative ? digits.Length - 1 : digits.Length;
120 |
121 | // We do not support numbers that cannot be stored as a long (Int64)
122 | if (numberOfDigits > BNumber.MaxDigits)
123 | {
124 | throw UnsupportedException(
125 | $"The number '{digits.AsString()}' has more than 19 digits and cannot be stored as a long (Int64) and therefore is not supported.",
126 | startPosition);
127 | }
128 |
129 | // We need at least one digit
130 | if (numberOfDigits < 1)
131 | throw NoDigitsException(startPosition);
132 |
133 | var firstDigit = isNegative ? digits[1] : digits[0];
134 |
135 | // Leading zeros are not valid
136 | if (firstDigit == '0' && numberOfDigits > 1)
137 | throw InvalidException($"Leading '0's are not valid. Found value '{digits.AsString()}'.", startPosition);
138 |
139 | // '-0' is not valid either
140 | if (firstDigit == '0' && numberOfDigits == 1 && isNegative)
141 | throw InvalidException("'-0' is not a valid number.", startPosition);
142 |
143 | if (!ParseUtil.TryParseLongFast(digits, out var number))
144 | {
145 | var nonSignChars = isNegative ? digits.Slice(1) : digits;
146 | if (nonSignChars.AsString().Any(x => !x.IsDigit()))
147 | throw InvalidException($"The value '{digits.AsString()}' is not a valid number.", startPosition);
148 |
149 | throw UnsupportedException(
150 | $"The value '{digits.AsString()}' is not a valid long (Int64). Supported values range from '{long.MinValue:N0}' to '{long.MaxValue:N0}'.",
151 | startPosition);
152 | }
153 |
154 | return new BNumber(number);
155 | }
156 |
157 | private static InvalidBencodeException NoDigitsException(long startPosition)
158 | {
159 | return new InvalidBencodeException(
160 | $"It contains no digits. The number starts at position {startPosition}.",
161 | startPosition);
162 | }
163 |
164 | private static InvalidBencodeException InvalidException(string message, long startPosition)
165 | {
166 | return new InvalidBencodeException(
167 | $"{message} The number starts at position {startPosition}.",
168 | startPosition);
169 | }
170 |
171 | private static UnsupportedBencodeException UnsupportedException(string message, long startPosition)
172 | {
173 | return new UnsupportedBencodeException(
174 | $"{message} The number starts at position {startPosition}.",
175 | startPosition);
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/BencodeNET/Parsing/BObjectParser.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Pipelines;
3 | using System.Text;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using BencodeNET.IO;
7 | using BencodeNET.Objects;
8 |
9 | namespace BencodeNET.Parsing
10 | {
11 | ///
12 | /// Abstract base parser for parsing bencode of specific types.
13 | ///
14 | /// The type of bencode object the parser returns.
15 | public abstract class BObjectParser : IBObjectParser where T : IBObject
16 | {
17 | ///
18 | /// The encoding used for parsing.
19 | ///
20 | public abstract Encoding Encoding { get; }
21 |
22 | IBObject IBObjectParser.Parse(Stream stream)
23 | {
24 | return Parse(stream);
25 | }
26 |
27 | IBObject IBObjectParser.Parse(BencodeReader reader)
28 | {
29 | return Parse(reader);
30 | }
31 |
32 | async ValueTask IBObjectParser.ParseAsync(PipeReader pipeReader, CancellationToken cancellationToken)
33 | {
34 | return await ParseAsync(new PipeBencodeReader(pipeReader), cancellationToken).ConfigureAwait(false);
35 | }
36 |
37 | async ValueTask IBObjectParser.ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken)
38 | {
39 | return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false);
40 | }
41 |
42 | ///
43 | /// Parses a stream into an of type .
44 | ///
45 | /// The stream to parse.
46 | /// The parsed object.
47 | public virtual T Parse(Stream stream) => Parse(new BencodeReader(stream, leaveOpen: true));
48 |
49 | ///
50 | /// Parses an of type from a .
51 | ///
52 | /// The reader to read from.
53 | /// The parsed object.
54 | public abstract T Parse(BencodeReader reader);
55 |
56 | ///
57 | /// Parses an of type from a .
58 | ///
59 | /// The pipe reader to read from.
60 | ///
61 | /// The parsed object.
62 | public ValueTask ParseAsync(PipeReader pipeReader, CancellationToken cancellationToken = default)
63 | => ParseAsync(new PipeBencodeReader(pipeReader), cancellationToken);
64 |
65 | ///
66 | /// Parses an of type from a .
67 | ///
68 | /// The pipe reader to read from.
69 | ///
70 | /// The parsed object.
71 | public abstract ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default);
72 | }
73 | }
--------------------------------------------------------------------------------
/BencodeNET/Parsing/BObjectParserExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using BencodeNET.Objects;
3 |
4 | namespace BencodeNET.Parsing
5 | {
6 | ///
7 | /// Extensions to simplify parsing strings and byte arrays.
8 | ///
9 | public static class BObjectParserExtensions
10 | {
11 | ///
12 | /// Parses a bencoded string into an .
13 | ///
14 | ///
15 | /// The bencoded string to parse.
16 | /// The parsed object.
17 | public static IBObject ParseString(this IBObjectParser parser, string bencodedString)
18 | {
19 | using (var stream = bencodedString.AsStream(parser.Encoding))
20 | {
21 | return parser.Parse(stream);
22 | }
23 | }
24 |
25 | ///
26 | /// Parses a byte array into an .
27 | ///
28 | ///
29 | /// The bytes to parse.
30 | /// The parsed object.
31 | public static IBObject Parse(this IBObjectParser parser, byte[] bytes)
32 | {
33 | using (var stream = new MemoryStream(bytes))
34 | {
35 | return parser.Parse(stream);
36 | }
37 | }
38 |
39 | ///
40 | /// Parses a bencoded string into an of type .
41 | ///
42 | ///
43 | /// The bencoded string to parse.
44 | /// The parsed object.
45 | public static T ParseString(this IBObjectParser parser, string bencodedString) where T : IBObject
46 | {
47 | using (var stream = bencodedString.AsStream(parser.Encoding))
48 | {
49 | return parser.Parse(stream);
50 | }
51 | }
52 |
53 | ///
54 | /// Parses a byte array into an of type .
55 | ///
56 | ///
57 | /// The bytes to parse.
58 | /// The parsed object.
59 | public static T Parse(this IBObjectParser parser, byte[] bytes) where T : IBObject
60 | {
61 | using (var stream = new MemoryStream(bytes))
62 | {
63 | return parser.Parse(stream);
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/BencodeNET/Parsing/BObjectParserList.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using BencodeNET.Objects;
6 |
7 | namespace BencodeNET.Parsing
8 | {
9 | ///
10 | /// A special collection for that has some extra methods
11 | /// for efficiently adding and accessing parsers by the type they can parse.
12 | ///
13 | public class BObjectParserList : IEnumerable>
14 | {
15 | private IDictionary Parsers { get; } = new Dictionary();
16 |
17 | ///
18 | /// Adds a parser for the specified type.
19 | /// Existing parsers for this type will be replaced.
20 | ///
21 | /// The type this parser can parse.
22 | /// The parser to add.
23 | public void Add(Type type, IBObjectParser parser)
24 | {
25 | AddOrReplace(type, parser);
26 | }
27 |
28 | ///
29 | /// Adds a parser for the specified type.
30 | /// Existing parsers for this type will be replaced.
31 | ///
32 | /// The types this parser can parse.
33 | /// The parser to add.
34 | public void Add(IEnumerable types, IBObjectParser parser)
35 | {
36 | AddOrReplace(types, parser);
37 | }
38 |
39 | ///
40 | /// Adds a specific parser.
41 | /// Existing parsers for the type will be replaced.
42 | ///
43 | /// The type this parser can parse.
44 | /// The parser to add.
45 | public void Add(IBObjectParser parser) where T : IBObject
46 | {
47 | AddOrReplace(typeof(T), parser);
48 | }
49 |
50 | ///
51 | /// Adds a parser for the specified type.
52 | /// Existing parsers for this type will be replaced.
53 | ///
54 | /// The type this parser can parse.
55 | /// The parser to add.
56 | public void AddOrReplace(Type type, IBObjectParser parser)
57 | {
58 | if (!typeof(IBObject).IsAssignableFrom(type))
59 | throw new ArgumentException($"The '{nameof(type)}' parameter must be assignable to '{typeof(IBObject).FullName}'");
60 |
61 | if (Parsers.ContainsKey(type))
62 | Parsers.Remove(type);
63 |
64 | Parsers.Add(type, parser);
65 | }
66 |
67 | ///
68 | /// Adds a parser for the specified type.
69 | /// Existing parsers for this type will be replaced.
70 | ///
71 | /// The types this parser can parse.
72 | /// The parser to add.
73 | public void AddOrReplace(IEnumerable types, IBObjectParser parser)
74 | {
75 | foreach (var type in types)
76 | {
77 | AddOrReplace(type, parser);
78 | }
79 | }
80 |
81 | ///
82 | /// Adds a specific parser.
83 | /// Existing parsers for the type will be replaced.
84 | ///
85 | /// The type this parser can parse.
86 | /// The parser to add.
87 | public void AddOrReplace(IBObjectParser parser) where T : IBObject
88 | {
89 | AddOrReplace(typeof(T), parser);
90 | }
91 |
92 | ///
93 | /// Gets the parser, if any, for the specified type.
94 | ///
95 | /// The type to get a parser for.
96 | /// The parser for the specified type or null if there isn't one.
97 | public IBObjectParser Get(Type type)
98 | {
99 | return Parsers.GetValueOrDefault(type);
100 | }
101 |
102 | ///
103 | /// Gets the parser, if any, for the specified type.
104 | ///
105 | /// The type to get a parser for.
106 | /// The parser for the specified type or null if there isn't one.
107 | public IBObjectParser this[Type type]
108 | {
109 | get => Get(type);
110 | set => AddOrReplace(type, value);
111 | }
112 |
113 | ///
114 | /// Gets the parser, if any, for the specified type.
115 | ///
116 | /// The type to get a parser for.
117 | /// The parser for the specified type or null if there isn't one.
118 | public IBObjectParser Get() where T : IBObject
119 | {
120 | return Get(typeof(T)) as IBObjectParser;
121 | }
122 |
123 | ///
124 | /// Gets the specific parser of the type specified or null if not found.
125 | ///
126 | /// The parser type to get.
127 | /// The parser of the specified type or null if there isn't one.
128 | public T GetSpecific() where T : class, IBObjectParser
129 | {
130 | return Parsers.FirstOrDefault(x => x.Value is T).Value as T;
131 | }
132 |
133 | ///
134 | /// Removes the parser for the specified type.
135 | ///
136 | /// The type to remove the parser for.
137 | /// True if successful, false otherwise.
138 | public bool Remove(Type type) => Parsers.Remove(type);
139 |
140 | ///
141 | /// Removes the parser for the specified type.
142 | ///
143 | /// The type to remove the parser for.
144 | /// True if successful, false otherwise.
145 | public bool Remove() => Remove(typeof (T));
146 |
147 | ///
148 | /// Empties the collection.
149 | ///
150 | public void Clear() => Parsers.Clear();
151 |
152 | ///
153 | /// Returns an enumerator that iterates through the collection.
154 | ///
155 | /// An enumerator that can be used to iterate through the collection.
156 | public IEnumerator> GetEnumerator()
157 | {
158 | return Parsers.GetEnumerator();
159 | }
160 |
161 | IEnumerator IEnumerable.GetEnumerator()
162 | {
163 | return GetEnumerator();
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/BencodeNET/Parsing/BencodeParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using BencodeNET.Exceptions;
6 | using BencodeNET.IO;
7 | using BencodeNET.Objects;
8 | using BencodeNET.Torrents;
9 |
10 | namespace BencodeNET.Parsing
11 | {
12 | ///
13 | /// Main class used for parsing bencode.
14 | ///
15 | public class BencodeParser : IBencodeParser
16 | {
17 | ///
18 | /// List of parsers used by the .
19 | ///
20 | public BObjectParserList Parsers { get; }
21 |
22 | ///
23 | /// The encoding use for parsing.
24 | ///
25 | public Encoding Encoding
26 | {
27 | get => _encoding;
28 | set
29 | {
30 | _encoding = value ?? throw new ArgumentNullException(nameof(value));
31 | Parsers.GetSpecific()?.ChangeEncoding(value);
32 | }
33 | }
34 | private Encoding _encoding;
35 |
36 | ///
37 | /// Creates an instance using the specified encoding and the default parsers.
38 | /// Encoding defaults to if not specified.
39 | ///
40 | /// The encoding to use when parsing.
41 | public BencodeParser(Encoding encoding = null)
42 | {
43 | _encoding = encoding ?? Encoding.UTF8;
44 |
45 | Parsers = new BObjectParserList
46 | {
47 | new BNumberParser(),
48 | new BStringParser(_encoding),
49 | new BListParser(this),
50 | new BDictionaryParser(this),
51 | new TorrentParser(this)
52 | };
53 | }
54 |
55 | ///
56 | /// Parses an from the reader.
57 | ///
58 | public virtual IBObject Parse(BencodeReader reader)
59 | {
60 | if (reader == null) throw new ArgumentNullException(nameof(reader));
61 |
62 | switch (reader.PeekChar())
63 | {
64 | case '0':
65 | case '1':
66 | case '2':
67 | case '3':
68 | case '4':
69 | case '5':
70 | case '6':
71 | case '7':
72 | case '8':
73 | case '9': return Parse(reader);
74 | case 'i': return Parse(reader);
75 | case 'l': return Parse(reader);
76 | case 'd': return Parse(reader);
77 | case default(char): return null;
78 | }
79 |
80 | throw InvalidBencodeException.InvalidBeginningChar(reader.PeekChar(), reader.Position);
81 | }
82 |
83 | ///
84 | /// Parse an of type from the reader.
85 | ///
86 | /// The type of to parse as.
87 | public virtual T Parse(BencodeReader reader) where T : class, IBObject
88 | {
89 | var parser = Parsers.Get();
90 |
91 | if (parser == null)
92 | throw new BencodeException($"Missing parser for the type '{typeof(T).FullName}'. Stream position: {reader.Position}");
93 |
94 | return parser.Parse(reader);
95 | }
96 |
97 | ///
98 | /// Parse an