├── .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 from the . 99 | /// 100 | public virtual async ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default) 101 | { 102 | if (pipeReader == null) throw new ArgumentNullException(nameof(pipeReader)); 103 | 104 | switch (await pipeReader.PeekCharAsync(cancellationToken).ConfigureAwait(false)) 105 | { 106 | case '0': 107 | case '1': 108 | case '2': 109 | case '3': 110 | case '4': 111 | case '5': 112 | case '6': 113 | case '7': 114 | case '8': 115 | case '9': return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); 116 | case 'i': return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); 117 | case 'l': return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); 118 | case 'd': return await ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); 119 | case default(char): return null; 120 | } 121 | 122 | throw InvalidBencodeException.InvalidBeginningChar( 123 | await pipeReader.PeekCharAsync(cancellationToken).ConfigureAwait(false), 124 | pipeReader.Position); 125 | } 126 | 127 | /// 128 | /// Parse an of type from the . 129 | /// 130 | public virtual async ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default) where T : class, IBObject 131 | { 132 | var parser = Parsers.Get(); 133 | 134 | if (parser == null) 135 | throw new BencodeException($"Missing parser for the type '{typeof(T).FullName}'. Stream position: {pipeReader.Position}"); 136 | 137 | return await parser.ParseAsync(pipeReader, cancellationToken).ConfigureAwait(false); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /BencodeNET/Parsing/BencodeParserExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Pipelines; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using BencodeNET.IO; 6 | using BencodeNET.Objects; 7 | 8 | namespace BencodeNET.Parsing 9 | { 10 | /// 11 | /// Extensions to simplify parsing strings, byte arrays or files directly. 12 | /// 13 | public static class BencodeParserExtensions 14 | { 15 | /// 16 | /// Parses a bencoded string into an . 17 | /// 18 | /// 19 | /// The bencoded string to parse. 20 | /// The parsed object. 21 | public static IBObject ParseString(this IBencodeParser parser, string bencodedString) 22 | { 23 | using var stream = bencodedString.AsStream(parser.Encoding); 24 | return parser.Parse(stream); 25 | } 26 | 27 | /// 28 | /// Parses a bencoded array of bytes into an . 29 | /// 30 | /// 31 | /// The bencoded bytes to parse. 32 | /// The parsed object. 33 | public static IBObject Parse(this IBencodeParser parser, byte[] bytes) 34 | { 35 | using var stream = new MemoryStream(bytes); 36 | return parser.Parse(stream); 37 | } 38 | 39 | /// 40 | /// Parses a bencoded file into an . 41 | /// 42 | /// 43 | /// The path to the file to parse. 44 | /// The parsed object. 45 | public static IBObject Parse(this IBencodeParser parser, string filePath) 46 | { 47 | using var stream = File.OpenRead(filePath); 48 | return parser.Parse(stream); 49 | } 50 | 51 | /// 52 | /// Parses a bencoded string into an of type . 53 | /// 54 | /// The type of to parse as. 55 | /// 56 | /// The bencoded string to parse. 57 | /// The parsed object. 58 | public static T ParseString(this IBencodeParser parser, string bencodedString) where T : class, IBObject 59 | { 60 | using var stream = bencodedString.AsStream(parser.Encoding); 61 | return parser.Parse(stream); 62 | } 63 | 64 | /// 65 | /// Parses a bencoded array of bytes into an of type . 66 | /// 67 | /// The type of to parse as. 68 | /// 69 | /// The bencoded bytes to parse. 70 | /// The parsed object. 71 | public static T Parse(this IBencodeParser parser, byte[] bytes) where T : class, IBObject 72 | { 73 | using var stream = new MemoryStream(bytes); 74 | return parser.Parse(stream); 75 | } 76 | 77 | /// 78 | /// Parses a bencoded file into an of type . 79 | /// 80 | /// 81 | /// The path to the file to parse. 82 | /// The parsed object. 83 | public static T Parse(this IBencodeParser parser, string filePath) where T : class, IBObject 84 | { 85 | using var stream = File.OpenRead(filePath); 86 | return parser.Parse(stream); 87 | } 88 | 89 | /// 90 | /// Parses a stream into an . 91 | /// 92 | /// 93 | /// The stream to parse. 94 | /// The parsed object. 95 | public static IBObject Parse(this IBencodeParser parser, Stream stream) 96 | { 97 | using var reader = new BencodeReader(stream, leaveOpen: true); 98 | return parser.Parse(reader); 99 | } 100 | 101 | /// 102 | /// Parses a stream into an of type . 103 | /// 104 | /// The type of to parse as. 105 | /// 106 | /// The stream to parse. 107 | /// The parsed object. 108 | public static T Parse(this IBencodeParser parser, Stream stream) where T : class, IBObject 109 | { 110 | using var reader = new BencodeReader(stream, leaveOpen: true); 111 | return parser.Parse(reader); 112 | } 113 | 114 | /// 115 | /// Parses an from the . 116 | /// 117 | public static ValueTask ParseAsync(this IBencodeParser parser, PipeReader pipeReader, CancellationToken cancellationToken = default) 118 | { 119 | var reader = new PipeBencodeReader(pipeReader); 120 | return parser.ParseAsync(reader, cancellationToken); 121 | } 122 | 123 | /// 124 | /// Parses an of type from the . 125 | /// 126 | /// The type of to parse as. 127 | public static ValueTask ParseAsync(this IBencodeParser parser, PipeReader pipeReader, CancellationToken cancellationToken = default) where T : class, IBObject 128 | { 129 | var reader = new PipeBencodeReader(pipeReader); 130 | return parser.ParseAsync(reader, cancellationToken); 131 | } 132 | 133 | /// 134 | /// Parses an from the asynchronously using a . 135 | /// 136 | public static ValueTask ParseAsync(this IBencodeParser parser, Stream stream, StreamPipeReaderOptions readerOptions = null, CancellationToken cancellationToken = default) 137 | { 138 | var reader = PipeReader.Create(stream, readerOptions); 139 | return parser.ParseAsync(reader, cancellationToken); 140 | } 141 | 142 | /// 143 | /// Parses an of type from the asynchronously using a . 144 | /// 145 | /// The type of to parse as. 146 | public static ValueTask ParseAsync(this IBencodeParser parser, Stream stream, StreamPipeReaderOptions readerOptions = null, CancellationToken cancellationToken = default) where T : class, IBObject 147 | { 148 | var reader = PipeReader.Create(stream, readerOptions); 149 | return parser.ParseAsync(reader, cancellationToken); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /BencodeNET/Parsing/IBObjectParser.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 | /// A contract for parsing bencode from different sources as an . 13 | /// 14 | public interface IBObjectParser 15 | { 16 | /// 17 | /// The encoding used for parsing. 18 | /// 19 | Encoding Encoding { get; } 20 | 21 | /// 22 | /// Parses a stream into an . 23 | /// 24 | /// The stream to parse. 25 | /// The parsed object. 26 | IBObject Parse(Stream stream); 27 | 28 | /// 29 | /// Parses an from a . 30 | /// 31 | IBObject Parse(BencodeReader reader); 32 | 33 | /// 34 | /// Parses an from a . 35 | /// 36 | /// The pipe reader to read from. 37 | /// 38 | /// The parsed object. 39 | ValueTask ParseAsync(PipeReader pipeReader, CancellationToken cancellationToken = default); 40 | 41 | /// 42 | /// Parses an from a . 43 | /// 44 | /// The pipe reader to read from. 45 | /// 46 | /// The parsed object. 47 | ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default); 48 | } 49 | 50 | /// 51 | /// A contract for parsing bencode from different sources as type inheriting . 52 | /// 53 | public interface IBObjectParser : IBObjectParser where T : IBObject 54 | { 55 | /// 56 | /// Parses a stream into an of type . 57 | /// 58 | /// The stream to parse. 59 | /// The parsed object. 60 | new T Parse(Stream stream); 61 | 62 | /// 63 | /// Parses an of type from a . 64 | /// 65 | new T Parse(BencodeReader reader); 66 | 67 | /// 68 | /// Parses an of type from a . 69 | /// 70 | /// The pipe reader to read from. 71 | /// 72 | /// The parsed object. 73 | new ValueTask ParseAsync(PipeReader pipeReader, CancellationToken cancellationToken = default); 74 | 75 | /// 76 | /// Parses an of type from a . 77 | /// 78 | /// The pipe reader to read from. 79 | /// 80 | /// The parsed object. 81 | new ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /BencodeNET/Parsing/IBencodeParser.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using System.Text; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using BencodeNET.IO; 6 | using BencodeNET.Objects; 7 | 8 | namespace BencodeNET.Parsing 9 | { 10 | /// 11 | /// Represents a parser capable of parsing bencode. 12 | /// 13 | public interface IBencodeParser 14 | { 15 | /// 16 | /// List of parsers used by the . 17 | /// 18 | BObjectParserList Parsers { get; } 19 | 20 | /// 21 | /// The encoding use for parsing. 22 | /// 23 | Encoding Encoding { get; } 24 | 25 | /// 26 | /// Parses an from the reader. 27 | /// 28 | /// 29 | IBObject Parse(BencodeReader reader); 30 | 31 | /// 32 | /// Parse an of type from the reader. 33 | /// 34 | /// The type of to parse as. 35 | /// 36 | T Parse(BencodeReader reader) where T : class, IBObject; 37 | 38 | /// 39 | /// Parse an from the . 40 | /// 41 | ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default); 42 | 43 | /// 44 | /// Parse an of type from the . 45 | /// 46 | ValueTask ParseAsync(PipeBencodeReader pipeReader, CancellationToken cancellationToken = default) where T : class, IBObject; 47 | } 48 | } -------------------------------------------------------------------------------- /BencodeNET/Parsing/ParseUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BencodeNET.Parsing 4 | { 5 | /// 6 | /// A collection of helper methods for parsing bencode. 7 | /// 8 | public static class ParseUtil 9 | { 10 | private const int Int64MaxDigits = 19; 11 | 12 | /// 13 | /// A faster implementation than 14 | /// because we skip some checks that are not needed. 15 | /// 16 | public static bool TryParseLongFast(string value, out long result) 17 | => TryParseLongFast(value.AsSpan(), out result); 18 | 19 | /// 20 | /// A faster implementation than 21 | /// because we skip some checks that are not needed. 22 | /// 23 | public static bool TryParseLongFast(ReadOnlySpan value, out long result) 24 | { 25 | result = 0; 26 | 27 | if (value == null) 28 | return false; 29 | 30 | var length = value.Length; 31 | 32 | // Cannot parse empty string 33 | if (length == 0) 34 | return false; 35 | 36 | var isNegative = value[0] == '-'; 37 | var startIndex = isNegative ? 1 : 0; 38 | 39 | // Cannot parse just '-' 40 | if (isNegative && length == 1) 41 | return false; 42 | 43 | // Cannot parse string longer than long.MaxValue 44 | if (length - startIndex > Int64MaxDigits) 45 | return false; 46 | 47 | long parsedLong = 0; 48 | for (var i = startIndex; i < length; i++) 49 | { 50 | var character = value[i]; 51 | if (!character.IsDigit()) 52 | return false; 53 | 54 | var digit = character - '0'; 55 | 56 | if (isNegative) 57 | parsedLong = 10 * parsedLong - digit; 58 | else 59 | parsedLong = 10 * parsedLong + digit; 60 | } 61 | 62 | // Negative - should be less than zero (Int64.MinValue overflow) 63 | if (isNegative && parsedLong >= 0) 64 | return false; 65 | 66 | // Positive - should be equal to or greater than zero (Int64.MaxValue overflow) 67 | if (!isNegative && parsedLong < 0) 68 | return false; 69 | 70 | result = parsedLong; 71 | return true; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /BencodeNET/Torrents/InvalidTorrentException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BencodeNET.Exceptions; 3 | 4 | #pragma warning disable 1591 5 | namespace BencodeNET.Torrents 6 | { 7 | /// 8 | /// Represents parse errors when parsing torrents. 9 | /// 10 | public class InvalidTorrentException : BencodeException 11 | { 12 | public string InvalidField { get; set; } 13 | 14 | public InvalidTorrentException() 15 | { } 16 | 17 | public InvalidTorrentException(string message) 18 | : base(message) 19 | { } 20 | 21 | public InvalidTorrentException(string message, string invalidField) 22 | : base(message) 23 | { 24 | InvalidField = invalidField; 25 | } 26 | 27 | public InvalidTorrentException(string message, Exception inner) 28 | : base(message, inner) 29 | { } 30 | } 31 | } 32 | #pragma warning restore 1591 -------------------------------------------------------------------------------- /BencodeNET/Torrents/MagnetLinkOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BencodeNET.Torrents 4 | { 5 | /// 6 | /// Possible options for controlling magnet link generation. 7 | /// 8 | [Flags] 9 | public enum MagnetLinkOptions 10 | { 11 | /// 12 | /// Results in the bare minimum magnet link containing only info hash and display name. 13 | /// 14 | None = 0, 15 | 16 | /// 17 | /// Includes trackers in the magnet link. 18 | /// 19 | IncludeTrackers = 1 << 0, 20 | } 21 | } -------------------------------------------------------------------------------- /BencodeNET/Torrents/MultiFileInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace BencodeNET.Torrents 6 | { 7 | /// 8 | /// File info for files in a multi-file torrents. 9 | /// This 10 | /// 11 | /// 12 | /// Corresponds to an entry in the 'info.files' list field in a torrent. 13 | /// 14 | public class MultiFileInfo 15 | { 16 | /// 17 | /// The file name. It just returns the last part of . 18 | /// 19 | public string FileName => Path?.LastOrDefault(); 20 | 21 | /// 22 | /// The UTF-8 encoded file name. It just returns the last part of . 23 | /// 24 | public string FileNameUtf8 => PathUtf8?.LastOrDefault(); 25 | 26 | /// 27 | /// The file size in bytes. 28 | /// 29 | /// 30 | /// Corresponds to the 'length' field. 31 | /// 32 | public long FileSize { get; set; } 33 | 34 | /// 35 | /// [optional] 32-character hexadecimal string corresponding to the MD5 sum of the file. Rarely used. 36 | /// 37 | public string Md5Sum { get; set; } 38 | 39 | /// 40 | /// A list of file path elements. 41 | /// 42 | public IList Path { get; set; } = new List(); 43 | 44 | /// 45 | /// A list of UTF-8 encoded file path elements. 46 | /// 47 | public IList PathUtf8 { get; set; } = new List(); 48 | 49 | /// 50 | /// The full path of the file including file name. 51 | /// 52 | public string FullPath 53 | { 54 | get => Path != null ? string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), Path) : null; 55 | set => Path = value.Split(new[] { System.IO.Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); 56 | } 57 | 58 | /// 59 | /// The full UTF-8 encoded path of the file including file name. 60 | /// 61 | public string FullPathUtf8 62 | { 63 | get => PathUtf8 != null ? string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), PathUtf8) : null; 64 | set => PathUtf8 = value.Split(new[] { System.IO.Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /BencodeNET/Torrents/MultiFileInfoList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BencodeNET.Torrents 4 | { 5 | /// 6 | /// A list of file info for the files included in a multi-file torrent. 7 | /// 8 | /// 9 | /// Corresponds to the 'info' field in a multi-file torrent. 10 | /// 11 | public class MultiFileInfoList : List 12 | { 13 | /// 14 | public MultiFileInfoList() 15 | { } 16 | 17 | /// 18 | /// Name of directory to store files in. 19 | public MultiFileInfoList(string directoryName) 20 | { 21 | DirectoryName = directoryName; 22 | } 23 | 24 | /// 25 | /// Name of directory to store files in. 26 | /// Name of directory to store files in. 27 | public MultiFileInfoList(string directoryName, string directoryNameUtf8) 28 | { 29 | DirectoryName = directoryName; 30 | DirectoryNameUtf8 = directoryNameUtf8; 31 | } 32 | 33 | /// 34 | /// The name of the directory in which to store all the files. This is purely advisory. 35 | /// 36 | /// 37 | /// Corresponds to the 'name' field. 38 | /// 39 | public string DirectoryName { get; set; } 40 | 41 | /// 42 | /// The UTF-8 encoded name of the directory in which to store all the files. This is purely advisory. 43 | /// 44 | /// 45 | /// Corresponds to the 'name.utf-8' field. 46 | /// 47 | public string DirectoryNameUtf8 { get; set; } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BencodeNET/Torrents/SingleFileInfo.cs: -------------------------------------------------------------------------------- 1 | namespace BencodeNET.Torrents 2 | { 3 | /// 4 | /// File info for a file in a single-file torrent. 5 | /// 6 | /// 7 | /// Corresponds to the 'info' field in a single-file torrent. 8 | /// 9 | public class SingleFileInfo 10 | { 11 | /// 12 | /// The file name. This is purely advisory. 13 | /// 14 | /// 15 | /// Corresponds to the 'name' field. 16 | /// 17 | public string FileName { get; set; } 18 | 19 | /// 20 | /// The UTF-8 encoded file name. This is purely advisory. 21 | /// 22 | /// 23 | /// Corresponds to the 'name.utf-8' field. 24 | /// 25 | public string FileNameUtf8 { get; set; } 26 | 27 | /// 28 | /// The file size in bytes. 29 | /// 30 | /// 31 | /// Corresponds to the 'length' field. 32 | /// 33 | public long FileSize { get; set; } 34 | 35 | /// 36 | /// [optional] 32-character hexadecimal string corresponding to the MD5 sum of the file. Rarely used. 37 | /// 38 | public string Md5Sum { get; set; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BencodeNET/Torrents/TorrentFields.cs: -------------------------------------------------------------------------------- 1 | using BencodeNET.Objects; 2 | 3 | #pragma warning disable 1591 4 | namespace BencodeNET.Torrents 5 | { 6 | /// 7 | /// A reference of default torrent field names. 8 | /// 9 | public static class TorrentFields 10 | { 11 | public const string Announce = "announce"; 12 | public const string AnnounceList = "announce-list"; 13 | public const string CreatedBy = "created by"; 14 | public const string CreationDate = "creation date"; 15 | public const string Comment = "comment"; 16 | public const string Encoding = "encoding"; 17 | public const string Info = "info"; 18 | 19 | public static readonly BString[] Keys = 20 | { 21 | Announce, 22 | AnnounceList, 23 | Comment, 24 | CreatedBy, 25 | CreationDate, 26 | Encoding, 27 | Info 28 | }; 29 | } 30 | 31 | /// 32 | /// A reference of default torrent fields names in the 'info'-dictionary. 33 | /// 34 | public static class TorrentInfoFields 35 | { 36 | public const string Name = "name"; 37 | public const string NameUtf8 = "name.utf-8"; 38 | public const string Private = "private"; 39 | public const string PieceLength = "piece length"; 40 | public const string Pieces = "pieces"; 41 | public const string Length = "length"; 42 | public const string Md5Sum = "md5sum"; 43 | public const string Files = "files"; 44 | 45 | public static readonly BString[] Keys = 46 | { 47 | Name, 48 | NameUtf8, 49 | Private, 50 | PieceLength, 51 | Pieces, 52 | Length, 53 | Md5Sum, 54 | Files 55 | }; 56 | } 57 | 58 | /// 59 | /// A reference of default torrent fields in the dictionaries in the 'files'-list in the 'info'-dictionary.s 60 | /// 61 | public static class TorrentFilesFields 62 | { 63 | public const string Length = "length"; 64 | public const string Path = "path"; 65 | public const string PathUtf8 = "path.utf-8"; 66 | public const string Md5Sum = "md5sum"; 67 | 68 | public static readonly BString[] Keys = 69 | { 70 | Length, 71 | Path, 72 | PathUtf8, 73 | Md5Sum 74 | }; 75 | } 76 | } 77 | #pragma warning restore 1591 -------------------------------------------------------------------------------- /BencodeNET/Torrents/TorrentFileMode.cs: -------------------------------------------------------------------------------- 1 | namespace BencodeNET.Torrents 2 | { 3 | /// 4 | /// Indicates the torrent file mode. 5 | /// Torrents are structured differently if it is either single-file or multi-file. 6 | /// 7 | public enum TorrentFileMode 8 | { 9 | /// 10 | /// Torrent file mode could not be determined and is most likely invalid. 11 | /// 12 | Unknown, 13 | 14 | /// 15 | /// Single-file torrent. Contains only a single file. 16 | /// 17 | Single, 18 | 19 | /// 20 | /// Multi-file torrent. Can contain multiple files and a parent directory name for all included files. 21 | /// 22 | Multi 23 | } 24 | } -------------------------------------------------------------------------------- /BencodeNET/Torrents/TorrentParserMode.cs: -------------------------------------------------------------------------------- 1 | namespace BencodeNET.Torrents 2 | { 3 | /// 4 | /// Determines how strict to be when parsing torrent files. 5 | /// 6 | public enum TorrentParserMode 7 | { 8 | /// 9 | /// The parser will throw an exception if some parts of the torrent specification is not followed. 10 | /// 11 | Strict, 12 | 13 | /// 14 | /// The parser will ignore stuff that doesn't follow the torrent specifications. 15 | /// 16 | Tolerant 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BencodeNET/Torrents/TorrentUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using BencodeNET.Objects; 7 | 8 | namespace BencodeNET.Torrents 9 | { 10 | /// 11 | /// Utility class for doing torrent-related work like calculating info hash and creating magnet links. 12 | /// 13 | public static class TorrentUtil 14 | { 15 | /// 16 | /// Calculates the info hash of the torrent. 17 | /// The info hash is a 20-byte SHA1 hash of the 'info'-dictionary of the torrent 18 | /// used to uniquely identify it and it's contents. 19 | /// 20 | /// Example: 6D60711ECF005C1147D8973A67F31A11454AB3F5 21 | /// 22 | /// The torrent to calculate the info hash for. 23 | /// A string representation of the 20-byte SHA1 hash without dashes. 24 | public static string CalculateInfoHash(Torrent torrent) 25 | { 26 | var info = torrent.ToBDictionary().Get("info"); 27 | return CalculateInfoHash(info); 28 | } 29 | 30 | /// 31 | /// Calculates the info hash of the torrent. 32 | /// The info hash is a 20-byte SHA1 hash of the 'info'-dictionary of the torrent 33 | /// used to uniquely identify it and it's contents. 34 | /// 35 | /// Example: 6D60711ECF005C1147D8973A67F31A11454AB3F5 36 | /// 37 | /// The torrent to calculate the info hash for. 38 | /// A byte-array of the 20-byte SHA1 hash. 39 | public static byte[] CalculateInfoHashBytes(Torrent torrent) 40 | { 41 | var info = torrent.ToBDictionary().Get("info"); 42 | return CalculateInfoHashBytes(info); 43 | } 44 | 45 | /// 46 | /// Calculates the hash of the 'info'-dictionary. 47 | /// The info hash is a 20-byte SHA1 hash of the 'info'-dictionary of the torrent 48 | /// used to uniquely identify it and it's contents. 49 | /// 50 | /// Example: 6D60711ECF005C1147D8973A67F31A11454AB3F5 51 | /// 52 | /// The 'info'-dictionary of a torrent. 53 | /// A string representation of the 20-byte SHA1 hash without dashes. 54 | public static string CalculateInfoHash(BDictionary info) 55 | { 56 | var hashBytes = CalculateInfoHashBytes(info); 57 | return BytesToHexString(hashBytes); 58 | } 59 | 60 | /// 61 | /// Calculates the hash of the 'info'-dictionary. 62 | /// The info hash is a 20-byte SHA1 hash of the 'info'-dictionary of the torrent 63 | /// used to uniquely identify it and it's contents. 64 | /// 65 | /// Example: 6D60711ECF005C1147D8973A67F31A11454AB3F5 66 | /// 67 | /// The 'info'-dictionary of a torrent. 68 | /// A byte-array of the 20-byte SHA1 hash. 69 | public static byte[] CalculateInfoHashBytes(BDictionary info) 70 | { 71 | using (var sha1 = SHA1.Create()) 72 | using (var stream = new MemoryStream()) 73 | { 74 | info.EncodeTo(stream); 75 | stream.Position = 0; 76 | 77 | return sha1.ComputeHash(stream); 78 | } 79 | } 80 | 81 | /// 82 | /// Converts the byte array to a hexadecimal string representation without hyphens. 83 | /// 84 | /// 85 | public static string BytesToHexString(byte[] bytes) 86 | { 87 | return BitConverter.ToString(bytes).Replace("-", ""); 88 | } 89 | 90 | /// 91 | /// Creates a Magnet link in the BTIH (BitTorrent Info Hash) format: xt=urn:btih:{info hash} 92 | /// 93 | /// Torrent to create Magnet link for. 94 | /// Controls how the Magnet link is constructed. 95 | /// 96 | public static string CreateMagnetLink(Torrent torrent, MagnetLinkOptions options = MagnetLinkOptions.IncludeTrackers) 97 | { 98 | var infoHash = torrent.GetInfoHash().ToLower(); 99 | var displayName = torrent.DisplayName; 100 | var trackers = torrent.Trackers.Flatten(); 101 | 102 | return CreateMagnetLink(infoHash, displayName, trackers, options); 103 | } 104 | 105 | /// 106 | /// Creates a Magnet link in the BTIH (BitTorrent Info Hash) format: xt=urn:btih:{info hash} 107 | /// 108 | /// The info has of the torrent. 109 | /// The display name of the torrent. Usually the file name or directory name for multi-file torrents 110 | /// A list of trackers if any. 111 | /// Controls how the Magnet link is constructed. 112 | /// 113 | public static string CreateMagnetLink(string infoHash, string displayName, IEnumerable trackers, MagnetLinkOptions options) 114 | { 115 | if (string.IsNullOrEmpty(infoHash)) 116 | throw new ArgumentException("Info hash cannot be null or empty.", nameof(infoHash)); 117 | 118 | var magnet = $"magnet:?xt=urn:btih:{infoHash}"; 119 | 120 | if (!string.IsNullOrWhiteSpace(displayName)) 121 | magnet += $"&dn={displayName}"; 122 | 123 | var validEscapedTrackers = 124 | trackers?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(Uri.EscapeDataString).ToList() ?? 125 | new List(); 126 | 127 | if (options.HasFlag(MagnetLinkOptions.IncludeTrackers) && validEscapedTrackers.Any()) 128 | { 129 | var trackersString = string.Join("&", validEscapedTrackers.Select(x => $"tr={x}")); 130 | magnet += $"&{trackersString}"; 131 | } 132 | 133 | return magnet; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /BencodeNET/UtilityExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Pipelines; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace BencodeNET 9 | { 10 | internal static class UtilityExtensions 11 | { 12 | public static bool IsDigit(this char c) 13 | { 14 | return c >= '0' && c <= '9'; 15 | } 16 | 17 | public static MemoryStream AsStream(this string str, Encoding encoding) 18 | { 19 | return new MemoryStream(encoding.GetBytes(str)); 20 | } 21 | 22 | public static TValue GetValueOrDefault(this IDictionary dictionary, TKey key) 23 | { 24 | return dictionary.TryGetValue(key, out var value) ? value : default; 25 | } 26 | 27 | public static IEnumerable Flatten(this IEnumerable> source) 28 | { 29 | return source.SelectMany(x => x); 30 | } 31 | 32 | public static int DigitCount(this int value) => DigitCount((long) value); 33 | 34 | public static int DigitCount(this long value) 35 | { 36 | var sign = value < 0 ? 1 : 0; 37 | 38 | if (value == long.MinValue) 39 | return 20; 40 | 41 | value = Math.Abs(value); 42 | 43 | if (value < 10) 44 | return sign + 1; 45 | if (value < 100) 46 | return sign + 2; 47 | if (value < 1000) 48 | return sign + 3; 49 | if (value < 10000) 50 | return sign + 4; 51 | if (value < 100000) 52 | return sign + 5; 53 | if (value < 1000000) 54 | return sign + 6; 55 | if (value < 10000000) 56 | return sign + 7; 57 | if (value < 100000000) 58 | return sign + 8; 59 | if (value < 1000000000) 60 | return sign + 9; 61 | if (value < 10000000000) 62 | return sign + 10; 63 | if (value < 100000000000) 64 | return sign + 11; 65 | if (value < 1000000000000) 66 | return sign + 12; 67 | if (value < 10000000000000) 68 | return sign + 13; 69 | if (value < 100000000000000) 70 | return sign + 14; 71 | if (value < 1000000000000000) 72 | return sign + 15; 73 | if (value < 10000000000000000) 74 | return sign + 16; 75 | if (value < 100000000000000000) 76 | return sign + 17; 77 | if (value < 1000000000000000000) 78 | return sign + 18; 79 | 80 | return sign + 19; 81 | } 82 | 83 | public static bool TrySetLength(this Stream stream, long length) 84 | { 85 | if (!stream.CanWrite || !stream.CanSeek) 86 | return false; 87 | 88 | try 89 | { 90 | if (stream.Length >= length) 91 | return false; 92 | 93 | stream.SetLength(length); 94 | return true; 95 | } 96 | catch 97 | { 98 | return false; 99 | } 100 | } 101 | 102 | public static void Write(this Stream stream, int number) 103 | { 104 | Span buffer = stackalloc byte[11]; 105 | var bytesRead = Encoding.ASCII.GetBytes(number.ToString().AsSpan(), buffer); 106 | stream.Write(buffer.Slice(0, bytesRead)); 107 | } 108 | 109 | public static void Write(this Stream stream, long number) 110 | { 111 | Span buffer = stackalloc byte[20]; 112 | var bytesRead = Encoding.ASCII.GetBytes(number.ToString().AsSpan(), buffer); 113 | stream.Write(buffer.Slice(0, bytesRead)); 114 | } 115 | 116 | public static void Write(this Stream stream, char c) 117 | { 118 | stream.WriteByte((byte) c); 119 | } 120 | 121 | public static void WriteChar(this PipeWriter writer, char c) 122 | { 123 | writer.GetSpan(1)[0] = (byte) c; 124 | writer.Advance(1); 125 | } 126 | 127 | public static void WriteCharAt(this Span bytes, char c, int index) 128 | { 129 | bytes[index] = (byte) c; 130 | } 131 | 132 | public static string AsString(this ReadOnlySpan chars) 133 | { 134 | return new string(chars); 135 | } 136 | 137 | public static string AsString(this Span chars) 138 | { 139 | return new string(chars); 140 | } 141 | 142 | public static string AsString(this Memory chars) 143 | { 144 | return new string(chars.Span); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ... 10 | 11 | ## [5.0.0] - 2023-03-30 12 | ### Changed 13 | - Added target framework .NET 6 14 | - Removed the following target frameworks and any associated conditional code: 15 | - .NET Standard 2.0 16 | - .NET Standard 2.1 17 | - .NET Core App 2.1 18 | - .NET 5.0 19 | - Upgraded dependency System.IO.Pipelines from 5.0.1 to 6.0.3 20 | - Switched a use of MemoryPool to ArrayPool 21 | - Merged PR #56 22 | - Always set `private` field in torrent. If `IsPrivate` is false then `0` is output instead of `1` 23 | - Merged PR #60 24 | - Escape tracker URLs in magnet links 25 | 26 | ## [4.0.0] - 2021-01-23 27 | ### Changed 28 | - Changed supported frameworks to: 29 | - .NET Standard 2.0 30 | - .NET Standard 2.1 31 | - .NET Core App 2.1 32 | - .NET 5.0 33 | - `Torrent.Pieces` can now only be set to an array with a length which is a multiple of 20. 34 | 35 | ## [3.1.4] - 2020-03-06 36 | ### Fixed 37 | - Issue parsing torrents without both `name` and `name.utf-8` field ([#47]) 38 | - Exception when accessing properties `FullPath` and `FullPathUtf8` on `MultiFileInfo` if `Path`/`PathUtf8` is null ([#47]) 39 | 40 | ## [3.1.3] - 2020-03-03 41 | ### Added 42 | - Added `Torrent.DisplayNameUtf8` and `MultiFileInfoList.DirectoryNameUtf8`, both mapped to the `name.utf-8` field 43 | 44 | ### Changed 45 | - New UTF-8 fields are now also added to `BDictionary` created by `Torrent.ToBDictionary` (and used by encode methods) 46 | 47 | ### Fixed 48 | - `Torrent.NumberOfPieces` is now correctly calculated by dividing by 20 instead of `Pieces.Length` (introduced in 3.1.0) ([#48]) 49 | 50 | ## [3.1.0] - 2020-02-28 51 | ### Added 52 | - Added `FileNameUtf8` and `PathUtf8` and `FullPathUtf8` properties to `SingleFileInfo`/`MultiFileInfo` ([#47]) 53 | - These properties reads from the `name.utf-8` and `path.utf-8` fields. 54 | 55 | ### Changed 56 | - `Torrent.NumberOfPieces` now uses `Pieces.Length` instead of `TotalSize` for the calculation ([#48]) 57 | 58 | ## [3.0.1] - 2019-10-17 59 | ### Fixed 60 | - Fixed missing parser for `Torrent` ([#44]) 61 | 62 | 63 | ## [3.0.0] - 2019-10-13 64 | There is a few changes to the public API, but most people shouldn't be affected by this unless they have extended/overriden functionality. 65 | Basic usage should not see any or only minor changes compared to v2.3.0. 66 | 67 | Implemented async support for parsing/encoding. 68 | 69 | Added build targets for .NET Core 2.1 and .NET Core 3.0 to take advantage of performance improvements 70 | in `Stream` and `Encoding` APIs for .NET Core 2.1 or later. 71 | 72 | Rewrite of internal parsing for better performance, taking advantage of new `Span`/`Memory` 73 | types - faster parsing and less memory allocation. 74 | 75 | Removed support for .NET Framework 4.5 and .NET Standard 1.3. 76 | Lowest supported versions are now .NET Framework 4.6.1 (4.7.2 highly recommended) and .NET Standard 2.0. 77 | 78 | 79 | ### Added 80 | - Implemented parsing/encoding using `PipeReader`/`PipeWriter` 81 | - Added `BencodeReader` as replacement for `BencodeStream` 82 | - Added `IBObject.GetSizeInBytes()` method, returning the size of the object in number of bytes. 83 | 84 | ### Changed 85 | - Improved parsing/encoding performance 86 | - Reduced memory allocation on parsing/encoding 87 | - Made `BString`, `BNumber`, `BList` and `BDictionary` classes `sealed` 88 | - Made parse methods of `BencodeParser` virtual so it can be overriden if needed by anyone 89 | - Constructor `BString(IEnumerable bytes, Encoding encoding = null)` changed to `BString(byte[] bytes, Encoding encoding = null)` 90 | - Exposed value type of `BString` changed from `IReadOnlyList` (`byte[]` internally) to `ReadOnlyMemory` 91 | - Removed parse method overloads on `IBencodeParser` and added matching extension methods instead 92 | - Removed encode method overloads on `IBObject` and added matching extension methods instead 93 | - Torrent parse mode now default to `TorrentParserMode.Tolerant` instead of `TorrentParserMode.Strict` 94 | - Torrent related classes moved to `BencodeNET.Torrents` namespace 95 | 96 | ### Removed 97 | - Removed `BencodeStream` and replaced with `BencodeReader` 98 | - Dropped .NET Standard 1.3 support; .NET Standard 2.0 is now lowest supported version 99 | - Dropped .NET Framework 4.5 support; .NET Framework 4.6.1 is now lowest supported version (but 4.7.2 is highly recommended) 100 | - Removed most constructors on `BencodeParser` leaving only `BencodeParser(Encoding encoding = null)` and 101 | added `BencodeParser.Encoding` property to enable changing encoding. Parsers can still be added/replaced/removed 102 | through `BencodeParser.Parsers` property. 103 | 104 | ### Fixed 105 | - Parsing from non-seekable `Stream`s is now possible 106 | - Fixed issue parsing torrent files with non-standard 'announce-list' ([#39]) 107 | 108 | 109 | ## [2.3.0] - 2019-02-11 110 | ### Added 111 | - Added `BNumber` casting operators to `int?` and `long?` 112 | 113 | 114 | ## [2.2.9] - 2017-08-05 115 | ### Added 116 | - Added tolerant parse mode for torrents, which skips validation 117 | - Save original info hash when parsing torrent 118 | 119 | ### Changed 120 | - Try to guess and handle timestamps in milliseconds in 'created' field 121 | 122 | ### Fixed 123 | - Handle invalid unix timestamps in 'created' field 124 | 125 | 126 | ## [2.2.2] - 2017-04-03 127 | ### Added 128 | - `BList.AsNumbers()` method 129 | - `Torrent.PiecesAsHexString` property 130 | - Attempt to use .torrent file encoding when parsing torrent itself 131 | 132 | ### Changed 133 | - `Torrent.Pieces` type changed to `byte[]` 134 | 135 | ### Fixed 136 | - `Torrent.Pieces` property 137 | 138 | 139 | ## [2.1.0] - 2016-10-07 140 | API has been more or less completely rewritten for better use with dependency injectiom 141 | and generally better usability; albeit a bit more complex. 142 | 143 | ### Added 144 | - .NET Standard support 145 | 146 | 147 | ## [1.3.1] - 2016-06-27 148 | ### Added 149 | - Some XML documentation (intellisense) 150 | 151 | ### Changed 152 | - Better handling of `CreationDate` in torrent files 153 | 154 | 155 | ## [1.2.1] - 2015-09-26 156 | ### Changed 157 | - Further performance improvements when decoding strings and numbers (up to ~30% for a standard torrent file) 158 | - XML documentation now included in nuget package 159 | 160 | 161 | ## [1.2.0] - 2015-09-21 162 | ### Changed 163 | - Big performance improvements when decoding 164 | 165 | ### Removed 166 | - BencodeStream.BaseStream property has been removed 167 | 168 | 169 | ## [1.1.0] - 2015-09-21 170 | ### Added 171 | - Torrent file abstractions including method to calculate info hash of a torrent file 172 | 173 | 174 | ## [1.0.0] - 2015-09-19 175 | 176 | 177 | [Unreleased]: ../../compare/v4.0.0...HEAD 178 | [4.0.0]: ../../compare/v3.1.4...v4.0.0 179 | [3.1.4]: ../../compare/v3.1.3...v3.1.4 180 | [3.1.3]: ../../compare/v3.1.0...v3.1.3 181 | [3.1.0]: ../../compare/v3.0.1...v3.1.0 182 | [3.0.1]: ../../compare/v3.0.0...v3.0.1 183 | [3.0.0]: ../../compare/v2.3.0...v3.0.0 184 | [2.3.0]: ../../compare/v2.2.9...v2.3.0 185 | [2.2.9]: ../../compare/v2.2.0...v2.2.9 186 | [2.2.2]: ../../compare/v2.1.0...v2.2.2 187 | [2.1.0]: ../../compare/v1.3.1...v2.1.0 188 | [1.3.1]: ../../compare/v1.3.0...v1.3.1 189 | [1.3.0]: ../../compare/v1.2.1...v1.3.0 190 | [1.2.1]: ../../compare/v1.2.0...v1.2.1 191 | [1.2.0]: ../../compare/v1.1.0...v1.2.0 192 | [1.1.0]: ../../compare/v1.0.0...v1.1.0 193 | [1.0.0]: ../../releases/tag/v1.0.0 194 | 195 | [#48]: https://github.com/Krusen/BencodeNET/issues/48 196 | [#47]: https://github.com/Krusen/BencodeNET/issues/47 197 | [#44]: https://github.com/Krusen/BencodeNET/issues/44 198 | [#39]: https://github.com/Krusen/BencodeNET/issues/39 -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: Mainline 2 | branches: {} 3 | ignore: 4 | sha: [] 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | # Windows image required for code coverage -> https://github.com/Microsoft/vstest-docs/blob/master/docs/analyze.md#coverage 11 | vmImage: 'windows-latest' 12 | 13 | steps: 14 | 15 | - task: PowerShell@2 16 | name: GitVersion 17 | displayName: run gitversion 18 | inputs: 19 | targetType: 'inline' 20 | script: | 21 | Write-Host "Installing GitVersion..." 22 | choco install gitversion.portable --no-progress 23 | 24 | Write-Host "Executing GitVersion..." 25 | $Env:SemVer = gitversion /showvariable SemVer 26 | 27 | Write-Host "SemVer: $Env:Semver" 28 | echo "##vso[task.setvariable variable=SemVer;isOutput=true]$Env:SemVer" 29 | echo "##vso[build.updatebuildnumber]$Env:SemVer+$(Build.BuildId)" 30 | 31 | - task: DotNetCoreCLI@2 32 | displayName: dotnet test 33 | inputs: 34 | command: 'test' 35 | projects: 'BencodeNET.sln' 36 | arguments: '--configuration Release --collect "Code coverage"' 37 | testRunTitle: 'BencodeNET.Tests' 38 | 39 | - task: DotNetCoreCLI@2 40 | displayName: dotnet pack 41 | inputs: 42 | command: 'pack' 43 | packagesToPack: 'BencodeNET/*.csproj' 44 | configuration: 'Release' 45 | packDirectory: '$(Pipeline.Workspace)/Packages' 46 | nobuild: true 47 | versioningScheme: 'off' 48 | buildProperties: 'SemVer=$(GitVersion.SemVer)' 49 | verbosityPack: 'Normal' 50 | 51 | - task: PublishPipelineArtifact@1 52 | displayName: Publish artifacts 53 | inputs: 54 | targetPath: '$(Pipeline.Workspace)\Packages' 55 | artifact: 'Packages' 56 | publishLocation: 'pipeline' --------------------------------------------------------------------------------