├── .gitignore
├── .gitmodules
├── CodeMaid.config
├── LICENSE-GPL.txt
├── QuickLook.Plugin.Metadata.Base.config
├── QuickLook.Plugin.TorrentViewer.sln
├── QuickLook.Plugin.TorrentViewer
├── App.config
├── ArchiveFileListView.xaml
├── ArchiveFileListView.xaml.cs
├── Bencode
│ ├── Exceptions
│ │ ├── BencodeException.cs
│ │ ├── InvalidBencodeException.cs
│ │ └── UnsupportedBencodeException.cs
│ ├── IO
│ │ ├── BencodeReader.cs
│ │ └── PipeBencodeReader.cs
│ ├── LICENSE.md
│ ├── 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
│ ├── README.md
│ ├── 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
├── Converters.cs
├── Data
│ ├── Block.cs
│ ├── BlockComparer.cs
│ ├── BlockDataHandler.cs
│ ├── ContainedFile.cs
│ ├── DiskFileHandler.cs
│ ├── IBlockDataHandler.cs
│ ├── IFileHandler.cs
│ ├── IPieceCalculator.cs
│ ├── MemoryFileHandler.cs
│ ├── Metainfo.cs
│ ├── MetainfoBuilder.cs
│ ├── Piece.cs
│ ├── PieceCalculator.cs
│ └── Sha1Hash.cs
├── IconManager.cs
├── Plugin.cs
├── QuickLook.Plugin.Metadata.config
├── QuickLook.Plugin.TorrentViewer.csproj
├── Torrent2Magnet.cs
├── TorrentFile.cs
├── TorrentParser.cs
└── Translations.config
├── README.md
├── Scripts
├── pack-zip.cmd
├── pack-zip.ps1
└── update-version.ps1
└── Settings.XamlStyler
/.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/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.sln.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
331 | /*.qlplugin
332 | /GitVersion.cs
333 | /QuickLook.Plugin.Metadata.config
334 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "QuickLook.Common"]
2 | path = QuickLook.Common
3 | url = https://github.com/QL-Win/QuickLook.Common
4 |
--------------------------------------------------------------------------------
/CodeMaid.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 1
12 |
13 |
14 | True
15 |
16 |
17 | False
18 |
19 |
20 | True
21 |
22 |
23 | False
24 |
25 |
26 | False
27 |
28 |
29 | False
30 |
31 |
32 | False
33 |
34 |
35 | False
36 |
37 |
38 | False
39 |
40 |
41 | False
42 |
43 |
44 | False
45 |
46 |
47 | False
48 |
49 |
50 | False
51 |
52 |
53 | False
54 |
55 |
56 | False
57 |
58 |
59 | False
60 |
61 |
62 | True
63 |
64 |
66 | False
67 |
68 |
70 | False
71 |
72 |
74 | False
75 |
76 |
78 | False
79 |
80 |
82 | False
83 |
84 |
86 | False
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.Metadata.Base.config:
--------------------------------------------------------------------------------
1 |
2 |
3 | QuickLook.Plugin.TorrentViewer
4 | 0
5 | View the Torrent files.
6 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.9.34728.123
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickLook.Common", "QuickLook.Common\QuickLook.Common.csproj", "{85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickLook.Plugin.TorrentViewer", "QuickLook.Plugin.TorrentViewer\QuickLook.Plugin.TorrentViewer.csproj", "{2C4C2B2F-A67B-4644-BCBF-E696962DD26D}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Debug|x86 = Debug|x86
14 | Release|Any CPU = Release|Any CPU
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}.Debug|x86.ActiveCfg = Debug|Any CPU
21 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}.Debug|x86.Build.0 = Debug|Any CPU
22 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}.Release|x86.ActiveCfg = Release|Any CPU
25 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}.Release|x86.Build.0 = Release|Any CPU
26 | {2C4C2B2F-A67B-4644-BCBF-E696962DD26D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {2C4C2B2F-A67B-4644-BCBF-E696962DD26D}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {2C4C2B2F-A67B-4644-BCBF-E696962DD26D}.Debug|x86.ActiveCfg = Debug|Any CPU
29 | {2C4C2B2F-A67B-4644-BCBF-E696962DD26D}.Debug|x86.Build.0 = Debug|Any CPU
30 | {2C4C2B2F-A67B-4644-BCBF-E696962DD26D}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {2C4C2B2F-A67B-4644-BCBF-E696962DD26D}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {2C4C2B2F-A67B-4644-BCBF-E696962DD26D}.Release|x86.ActiveCfg = Release|Any CPU
33 | {2C4C2B2F-A67B-4644-BCBF-E696962DD26D}.Release|x86.Build.0 = Release|Any CPU
34 | EndGlobalSection
35 | GlobalSection(SolutionProperties) = preSolution
36 | HideSolutionNode = FALSE
37 | EndGlobalSection
38 | GlobalSection(ExtensibilityGlobals) = postSolution
39 | SolutionGuid = {D3761C32-8C5F-498A-892B-3B0882994B62}
40 | EndGlobalSection
41 | EndGlobal
42 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/ArchiveFileListView.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
43 |
51 |
52 |
57 |
63 |
64 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
89 |
94 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
110 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/ArchiveFileListView.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Paddy Xu
2 | //
3 | // This file is part of QuickLook program.
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with this program. If not, see .
17 |
18 | using QuickLook.Common.ExtensionMethods;
19 | using QuickLook.Common.Helpers;
20 | using System;
21 | using System.Collections.Generic;
22 | using System.Globalization;
23 | using System.IO;
24 | using System.Linq;
25 | using System.Reflection;
26 | using System.Windows;
27 | using System.Windows.Controls;
28 | using System.Xml.XPath;
29 |
30 | namespace QuickLook.Plugin.TorrentViewer;
31 |
32 | ///
33 | /// Interaction logic for ArchiveFileListView.xaml
34 | ///
35 | public partial class ArchiveFileListView : UserControl, IDisposable
36 | {
37 | public ArchiveFileListView()
38 | {
39 | InitializeComponent();
40 | }
41 |
42 | public void Dispose()
43 | {
44 | GC.SuppressFinalize(this);
45 |
46 | IconManager.ClearCache();
47 | }
48 |
49 | public void SetDataContext(TorrentFiles context)
50 | {
51 | string translationFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations.config");
52 |
53 | treeTitle.Text = context.Name;
54 | treeTitle.ToolTip = context.Name;
55 | totalCount.Text = string.Format(Common.Helpers.TranslationHelper.Get("TOTAL_COUNT", translationFile), context.Files.Count().ToString());
56 | totalSize.Text = string.Format(Common.Helpers.TranslationHelper.Get("TOTAL_SIZE", translationFile), context.Files.Sum(x => x.Size).ToPrettySize(2));
57 |
58 | copyButton.ToolTip = context.Magnet;
59 | copyButton.Click += (_, _) =>
60 | {
61 | try
62 | {
63 | Clipboard.SetText(context.Magnet);
64 | }
65 | catch
66 | {
67 | ///
68 | }
69 | };
70 |
71 | treeGrid.DataContext = context.Files;
72 |
73 | treeView.LayoutUpdated += (sender, e) =>
74 | {
75 | // return when empty
76 | if (treeView.Items.Count == 0)
77 | return;
78 |
79 | // return when there are more than one root nodes
80 | if (treeView.Items.Count > 1)
81 | return;
82 |
83 | var root = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(treeView.Items[0]);
84 | if (root == null)
85 | return;
86 |
87 | root.IsExpanded = true;
88 | };
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Exceptions/BencodeException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
4 |
5 | ///
6 | /// Represents generic errors in this bencode library.
7 | ///
8 | public class BencodeException : Exception
9 | {
10 | public BencodeException()
11 | {
12 | }
13 |
14 | public BencodeException(string message)
15 | : base(message)
16 | {
17 | }
18 |
19 | public BencodeException(string message, Exception inner)
20 | : base(message, inner)
21 | {
22 | }
23 | }
24 |
25 | ///
26 | /// Represents generic errors in this bencode library related to a specific .
27 | ///
28 | /// The related type.
29 | public class BencodeException : BencodeException
30 | {
31 | ///
32 | /// The type related to this error. Usually the type being parsed.
33 | ///
34 | public Type RelatedType { get; } = typeof(T);
35 |
36 | public BencodeException()
37 | {
38 | }
39 |
40 | public BencodeException(string message)
41 | : base(message)
42 |
43 | {
44 | }
45 |
46 | public BencodeException(string message, Exception inner)
47 | : base(message, inner)
48 | {
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Exceptions/InvalidBencodeException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
4 |
5 | ///
6 | /// Represents parse errors when encountering invalid bencode of some sort.
7 | ///
8 | /// The type being parsed.
9 | public class InvalidBencodeException : BencodeException
10 | {
11 | ///
12 | /// The position in the stream where the error happened or
13 | /// the starting position of the parsed object that caused the error.
14 | ///
15 | public long StreamPosition { get; set; }
16 |
17 | public InvalidBencodeException()
18 | {
19 | }
20 |
21 | public InvalidBencodeException(string message)
22 | : base(message)
23 | {
24 | }
25 |
26 | public InvalidBencodeException(string message, Exception inner)
27 | : base(message, inner)
28 | {
29 | }
30 |
31 | public InvalidBencodeException(string message, Exception inner, long streamPosition)
32 | : base($"Failed to parse {typeof(T).Name}. {message}", inner)
33 | {
34 | StreamPosition = Math.Max(0, streamPosition);
35 | }
36 |
37 | public InvalidBencodeException(string message, long streamPosition)
38 | : base($"Failed to parse {typeof(T).Name}. {message}")
39 | {
40 | StreamPosition = Math.Max(0, streamPosition);
41 | }
42 |
43 | internal static InvalidBencodeException InvalidBeginningChar(char invalidChar, long streamPosition)
44 | {
45 | var message =
46 | $"Invalid beginning character of object. Found '{invalidChar}' at position {streamPosition}. Valid characters are: 0-9, 'i', 'l' and 'd'";
47 | return new InvalidBencodeException(message, streamPosition);
48 | }
49 |
50 | internal static InvalidBencodeException MissingEndChar(long streamPosition)
51 | {
52 | var message = "Missing end character of object. Expected 'e' but reached end of stream.";
53 | return new InvalidBencodeException(message, streamPosition);
54 | }
55 |
56 | internal static InvalidBencodeException BelowMinimumLength(int minimumLength, long actualLength, long streamPosition)
57 | {
58 | var message =
59 | $"Invalid length. Minimum valid stream length for parsing '{typeof(T).FullName}' is {minimumLength} but the actual length was only {actualLength}.";
60 | return new InvalidBencodeException(message, streamPosition);
61 | }
62 |
63 | internal static InvalidBencodeException UnexpectedChar(char expected, char unexpected, long streamPosition)
64 | {
65 | var message = unexpected == default
66 | ? $"Unexpected character. Expected '{expected}' but reached end of stream."
67 | : $"Unexpected character. Expected '{expected}' but found '{unexpected}' at position {streamPosition}.";
68 | return new InvalidBencodeException(message, streamPosition);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Exceptions/UnsupportedBencodeException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
4 |
5 | ///
6 | /// Represents parse errors for when encountering bencode that is potentially valid but not supported by this library.
7 | /// Usually numbers larger than or strings longer than that.
8 | ///
9 | ///
10 | public class UnsupportedBencodeException : BencodeException
11 | {
12 | public long StreamPosition { get; set; }
13 |
14 | public UnsupportedBencodeException()
15 | {
16 | }
17 |
18 | public UnsupportedBencodeException(string message)
19 | : base(message)
20 | {
21 | }
22 |
23 | public UnsupportedBencodeException(string message, Exception inner)
24 | : base(message, inner)
25 | {
26 | }
27 |
28 | public UnsupportedBencodeException(string message, long streamPosition)
29 | : base(message)
30 | {
31 | StreamPosition = streamPosition;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/IO/BencodeReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/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 QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Objects/BDictionary.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.IO.Pipelines;
6 | using System.Linq;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace QuickLook.Plugin.TorrentViewer.Bencode.Objects;
11 |
12 | ///
13 | /// Represents a bencoded dictionary of keys and values.
14 | ///
15 | ///
16 | /// The underlying value is a .
17 | ///
18 | public sealed class BDictionary : BObject>, IDictionary
19 | {
20 | ///
21 | /// The underlying dictionary.
22 | ///
23 | public override IDictionary Value { get; } = new SortedDictionary();
24 |
25 | ///
26 | /// Creates an empty dictionary.
27 | ///
28 | public BDictionary()
29 | {
30 | }
31 |
32 | ///
33 | /// Creates a dictionary from key-value pairs.
34 | ///
35 | ///
36 | public BDictionary(IEnumerable> keyValuePairs)
37 | {
38 | Value = new SortedDictionary(keyValuePairs.ToDictionary(x => x.Key, x => x.Value));
39 | }
40 |
41 | ///
42 | /// Creates a dictionary with an initial value of the supplied dictionary.
43 | ///
44 | ///
45 | public BDictionary(IDictionary dictionary)
46 | {
47 | Value = dictionary;
48 | }
49 |
50 | ///
51 | /// Adds the specified key and value to the dictionary as .
52 | ///
53 | ///
54 | ///
55 | public void Add(string key, string value) => Add(new BString(key), new BString(value));
56 |
57 | ///
58 | /// Adds the specified key and value to the dictionary as .
59 | ///
60 | ///
61 | ///
62 | public void Add(string key, long value) => Add(new BString(key), new BNumber(value));
63 |
64 | ///
65 | /// Gets the value associated with the specified key and casts it as .
66 | /// If the key does not exist or the value is not of the specified type null is returned.
67 | ///
68 | /// The type to cast the value to.
69 | /// The key to get the associated value of.
70 | /// The associated value of the specified key or null if the key does not exist.
71 | /// If the value is not of the specified type null is returned as well.
72 | public T Get(BString key) where T : class, IBObject
73 | {
74 | return this[key] as T;
75 | }
76 |
77 | ///
78 | /// Merges this instance with another .
79 | ///
80 | ///
81 | /// By default existing keys are either overwritten ( and ) or merged if possible ( and ).
82 | /// This behavior can be changed with the parameter.
83 | ///
84 | /// The dictionary to merge into this instance.
85 | /// Decides how to handle the values of existing keys.
86 | public void MergeWith(BDictionary dictionary, ExistingKeyAction existingKeyAction = ExistingKeyAction.Merge)
87 | {
88 | foreach (var field in dictionary)
89 | {
90 | // Add non-existing key
91 | if (!ContainsKey(field.Key))
92 | {
93 | Add(field);
94 | continue;
95 | }
96 |
97 | if (existingKeyAction == ExistingKeyAction.Skip)
98 | continue;
99 |
100 | switch (field.Value)
101 | {
102 | // Replace strings and numbers
103 | case BString _:
104 | case BNumber _:
105 | this[field.Key] = field.Value;
106 | continue;
107 |
108 | // Append list to existing list or replace other types
109 | case BList newList:
110 | {
111 | var existingList = Get(field.Key);
112 | if (existingList == null || existingKeyAction == ExistingKeyAction.Replace)
113 | {
114 | this[field.Key] = field.Value;
115 | continue;
116 | }
117 | existingList.AddRange(newList);
118 | continue;
119 | }
120 |
121 | // Merge dictionary with existing or replace other types
122 | case BDictionary newDictionary:
123 | {
124 | var existingDictionary = Get(field.Key);
125 | if (existingDictionary == null || existingKeyAction == ExistingKeyAction.Replace)
126 | {
127 | this[field.Key] = field.Value;
128 | continue;
129 | }
130 | existingDictionary.MergeWith(newDictionary);
131 | break;
132 | }
133 | }
134 | }
135 | }
136 |
137 | ///
138 | public override int GetSizeInBytes()
139 | {
140 | var size = 2;
141 | foreach (var entry in this)
142 | {
143 | size += entry.Key.GetSizeInBytes() + entry.Value.GetSizeInBytes();
144 | }
145 | return size;
146 | }
147 |
148 | ///
149 | protected override void EncodeObject(Stream stream)
150 | {
151 | stream.Write('d');
152 | foreach (var entry in this)
153 | {
154 | entry.Key.EncodeTo(stream);
155 | entry.Value.EncodeTo(stream);
156 | }
157 | stream.Write('e');
158 | }
159 |
160 | ///
161 | protected override void EncodeObject(PipeWriter writer)
162 | {
163 | writer.WriteChar('d');
164 | foreach (var entry in this)
165 | {
166 | entry.Key.EncodeTo(writer);
167 | entry.Value.EncodeTo(writer);
168 | }
169 |
170 | writer.WriteChar('e');
171 | }
172 |
173 | ///
174 | protected override async ValueTask EncodeObjectAsync(PipeWriter writer, CancellationToken cancellationToken)
175 | {
176 | writer.WriteChar('d');
177 | foreach (var entry in this)
178 | {
179 | cancellationToken.ThrowIfCancellationRequested();
180 | await entry.Key.EncodeToAsync(writer, cancellationToken).ConfigureAwait(false);
181 | await entry.Value.EncodeToAsync(writer, cancellationToken).ConfigureAwait(false);
182 | }
183 | writer.WriteChar('e');
184 |
185 | return await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
186 | }
187 |
188 | #region IDictionary Members
189 |
190 | #pragma warning disable 1591
191 |
192 | public ICollection Keys => Value.Keys;
193 |
194 | public ICollection Values => Value.Values;
195 |
196 | public int Count => Value.Count;
197 |
198 | public bool IsReadOnly => Value.IsReadOnly;
199 |
200 | ///
201 | /// Returns the value associated with the key or null if the key doesn't exist.
202 | ///
203 | public IBObject this[BString key]
204 | {
205 | get => ContainsKey(key) ? Value[key] : null;
206 | set => Value[key] = value ?? throw new ArgumentNullException(nameof(value), "A null value cannot be added to a BDictionary");
207 | }
208 |
209 | public void Add(KeyValuePair item)
210 | {
211 | if (item.Value == null) throw new ArgumentException("Must not contain a null value", nameof(item));
212 | Value.Add(item);
213 | }
214 |
215 | public void Add(BString key, IBObject value)
216 | {
217 | if (value == null) throw new ArgumentNullException(nameof(value));
218 | Value.Add(key, value);
219 | }
220 |
221 | public void Clear() => Value.Clear();
222 |
223 | public bool Contains(KeyValuePair item) => Value.Contains(item);
224 |
225 | public bool ContainsKey(BString key) => Value.ContainsKey(key);
226 |
227 | public void CopyTo(KeyValuePair[] array, int arrayIndex) => Value.CopyTo(array, arrayIndex);
228 |
229 | public IEnumerator> GetEnumerator() => Value.GetEnumerator();
230 |
231 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
232 |
233 | public bool Remove(KeyValuePair item) => Value.Remove(item);
234 |
235 | public bool Remove(BString key) => Value.Remove(key);
236 |
237 | public bool TryGetValue(BString key, out IBObject value) => Value.TryGetValue(key, out value);
238 |
239 | #pragma warning restore 1591
240 |
241 | #endregion IDictionary Members
242 | }
243 |
244 | ///
245 | /// Specifies the action to take when encountering an already existing key when merging two .
246 | ///
247 | public enum ExistingKeyAction
248 | {
249 | ///
250 | /// Merges the values of existing keys for and .
251 | /// Overwrites existing keys for and .
252 | ///
253 | Merge,
254 |
255 | ///
256 | /// Replaces the values of all existing keys.
257 | ///
258 | Replace,
259 |
260 | ///
261 | /// Leaves all existing keys as they were.
262 | ///
263 | Skip
264 | }
265 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Objects/BNumber.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Pipelines;
4 | using System.Text;
5 |
6 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
76 | public static implicit operator int?(BNumber bint)
77 | {
78 | if (bint == null) return null;
79 | return (int)bint.Value;
80 | }
81 |
82 | public static implicit operator long?(BNumber bint)
83 | {
84 | if (bint == null) return null;
85 | return bint.Value;
86 | }
87 |
88 | public static implicit operator int(BNumber bint)
89 | {
90 | if (bint == null) throw new InvalidCastException();
91 | return (int)bint.Value;
92 | }
93 |
94 | public static implicit operator long(BNumber bint)
95 | {
96 | if (bint == null) throw new InvalidCastException();
97 | return bint.Value;
98 | }
99 |
100 | public static implicit operator bool(BNumber bint)
101 | {
102 | if (bint == null) throw new InvalidCastException();
103 | return bint.Value > 0;
104 | }
105 |
106 | public static implicit operator DateTime?(BNumber number)
107 | {
108 | if (number == null) return null;
109 |
110 | if (number.Value > int.MaxValue)
111 | {
112 | try
113 | {
114 | return Epoch.AddMilliseconds(number);
115 | }
116 | catch (ArgumentOutOfRangeException)
117 | {
118 | return Epoch;
119 | }
120 | }
121 |
122 | return Epoch.AddSeconds(number);
123 | }
124 |
125 | public static implicit operator BNumber(int value) => new BNumber(value);
126 |
127 | public static implicit operator BNumber(long value) => new BNumber(value);
128 |
129 | public static implicit operator BNumber(bool value) => new BNumber(value ? 1 : 0);
130 |
131 | public static implicit operator BNumber(DateTime? datetime) => new BNumber(datetime);
132 |
133 | public static bool operator ==(BNumber bnumber, BNumber other)
134 | {
135 | return bnumber?.Value == other?.Value;
136 | }
137 |
138 | public static bool operator !=(BNumber bnumber, BNumber other) => !(bnumber == other);
139 |
140 | public override bool Equals(object other)
141 | {
142 | var bnumber = other as BNumber;
143 | return Value == bnumber?.Value;
144 | }
145 |
146 | ///
147 | /// Returns the hash code for this instance.
148 | ///
149 | public override int GetHashCode() => Value.GetHashCode();
150 |
151 | public int CompareTo(BNumber other)
152 | {
153 | if (other == null)
154 | return 1;
155 |
156 | return Value.CompareTo(other.Value);
157 | }
158 |
159 | public override string ToString() => Value.ToString();
160 |
161 | public string ToString(string format) => Value.ToString(format);
162 |
163 | public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);
164 |
165 | public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider);
166 |
167 | #pragma warning restore 1591
168 | }
169 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Objects/BObject.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Pipelines;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 | ///
18 | /// Calculates the (encoded) size of the object in bytes.
19 | ///
20 | public abstract int GetSizeInBytes();
21 |
22 | ///
23 | /// Writes the object as bencode to the specified stream.
24 | ///
25 | /// The type of stream.
26 | /// The stream to write to.
27 | /// The used stream.
28 | public TStream EncodeTo(TStream stream) where TStream : Stream
29 | {
30 | var size = GetSizeInBytes();
31 | stream.TrySetLength(size);
32 | EncodeObject(stream);
33 | return stream;
34 | }
35 |
36 | ///
37 | /// Writes the object as bencode to the specified without flushing the writer,
38 | /// you should do that manually.
39 | ///
40 | /// The writer to write to.
41 | public void EncodeTo(PipeWriter writer)
42 | {
43 | EncodeObject(writer);
44 | }
45 |
46 | ///
47 | /// Writes the object as bencode to the specified and flushes the writer afterwards.
48 | ///
49 | /// The writer to write to.
50 | ///
51 | public ValueTask EncodeToAsync(PipeWriter writer, CancellationToken cancellationToken = default)
52 | {
53 | return EncodeObjectAsync(writer, cancellationToken);
54 | }
55 |
56 | ///
57 | /// Writes the object asynchronously as bencode to the specified using a .
58 | ///
59 | /// The stream to write to.
60 | /// The options for the .
61 | ///
62 | public ValueTask EncodeToAsync(Stream stream, StreamPipeWriterOptions writerOptions = null, CancellationToken cancellationToken = default)
63 | {
64 | return EncodeObjectAsync(PipeWriter.Create(stream, writerOptions), cancellationToken);
65 | }
66 |
67 | ///
68 | /// Implementations of this method should encode their
69 | /// underlying value to bencode and write it to the stream.
70 | ///
71 | /// The stream to encode to.
72 | protected abstract void EncodeObject(Stream stream);
73 |
74 | ///
75 | /// Implementations of this method should encode their underlying value to bencode and write it to the .
76 | ///
77 | /// The writer to encode to.
78 | protected abstract void EncodeObject(PipeWriter writer);
79 |
80 | ///
81 | /// Encodes and writes the underlying value to the and flushes the writer afterwards.
82 | ///
83 | /// The writer to encode to.
84 | ///
85 | protected virtual ValueTask EncodeObjectAsync(PipeWriter writer, CancellationToken cancellationToken)
86 | {
87 | EncodeObject(writer);
88 | return writer.FlushAsync(cancellationToken);
89 | }
90 | }
91 |
92 | ///
93 | /// Base class of bencode objects with a specific underlying value type.
94 | ///
95 | /// Type of the underlying value.
96 | public abstract class BObject : BObject
97 | {
98 | internal BObject()
99 | {
100 | }
101 |
102 | ///
103 | /// The underlying value of the .
104 | ///
105 | public abstract T Value { get; }
106 | }
107 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Objects/BObjectExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.IO;
4 | using System.Text;
5 |
6 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Objects/BString.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Pipelines;
4 | using System.Text;
5 |
6 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
44 | private Encoding _encoding;
45 |
46 | ///
47 | /// Creates an empty ('0:').
48 | ///
49 | public BString()
50 | : this((string)null)
51 | {
52 | }
53 |
54 | ///
55 | /// Creates a from bytes with the specified encoding.
56 | ///
57 | /// The bytes representing the data.
58 | /// The encoding of the bytes. Defaults to .
59 | public BString(byte[] bytes, Encoding encoding = null)
60 | {
61 | Value = bytes ?? throw new ArgumentNullException(nameof(bytes));
62 | _encoding = encoding ?? DefaultEncoding;
63 | }
64 |
65 | ///
66 | /// Creates a using the specified encoding to convert the string to bytes.
67 | ///
68 | /// The string.
69 | /// The encoding used to convert the string to bytes.
70 | ///
71 | public BString(string str, Encoding encoding = null)
72 | {
73 | _encoding = encoding ?? DefaultEncoding;
74 |
75 | if (string.IsNullOrEmpty(str))
76 | {
77 | Value = Array.Empty();
78 | }
79 | else
80 | {
81 | var maxByteCount = _encoding.GetMaxByteCount(str.Length);
82 | var span = new byte[maxByteCount].AsSpan();
83 |
84 | var length = _encoding.GetBytes(str.AsSpan(), span);
85 |
86 | Value = span.Slice(0, length).ToArray();
87 | }
88 | }
89 |
90 | ///
91 | public override int GetSizeInBytes() => Value.Length + 1 + Value.Length.DigitCount();
92 |
93 | ///
94 | protected override void EncodeObject(Stream stream)
95 | {
96 | stream.Write(Value.Length);
97 | stream.Write(':');
98 | stream.Write(Value.Span);
99 | }
100 |
101 | ///
102 | protected override void EncodeObject(PipeWriter writer)
103 | {
104 | // Init
105 | var size = GetSizeInBytes();
106 | var buffer = writer.GetSpan(size);
107 |
108 | // Write length
109 | var writtenBytes = Encoding.GetBytes(Value.Length.ToString().AsSpan(), buffer);
110 |
111 | // Write ':'
112 | buffer[writtenBytes] = (byte)':';
113 |
114 | // Write value
115 | Value.Span.CopyTo(buffer.Slice(writtenBytes + 1));
116 |
117 | // Commit
118 | writer.Advance(size);
119 | }
120 |
121 | #pragma warning disable 1591
122 |
123 | public static implicit operator BString(string value) => new BString(value);
124 |
125 | public static bool operator ==(BString first, BString second)
126 | {
127 | return first?.Equals(second) ?? second is null;
128 | }
129 |
130 | public static bool operator !=(BString first, BString second) => !(first == second);
131 |
132 | public override bool Equals(object other) => other is BString bstring && Value.Span.SequenceEqual(bstring.Value.Span);
133 |
134 | public bool Equals(BString bstring) => bstring != null && Value.Span.SequenceEqual(bstring.Value.Span);
135 |
136 | public override int GetHashCode()
137 | {
138 | var bytesToHash = Math.Min(Value.Length, 32);
139 |
140 | long hashValue = 0;
141 | for (var i = 0; i < bytesToHash; i++)
142 | {
143 | hashValue = (37 * hashValue + Value.Span[i]) % int.MaxValue;
144 | }
145 |
146 | return (int)hashValue;
147 | }
148 |
149 | public int CompareTo(BString other)
150 | {
151 | return Value.Span.SequenceCompareTo(other.Value.Span);
152 | }
153 |
154 | #pragma warning restore 1591
155 |
156 | ///
157 | /// Converts the underlying bytes to a string representation using the current value of the property.
158 | ///
159 | ///
160 | /// A that represents this instance.
161 | ///
162 | public override string ToString()
163 | {
164 | return _encoding.GetString(Value.Span);
165 | }
166 |
167 | ///
168 | /// Converts the underlying bytes to a string representation using the specified encoding.
169 | ///
170 | /// The encoding to use to convert the underlying byte array to a .
171 | ///
172 | /// A that represents this instance.
173 | ///
174 | public string ToString(Encoding encoding)
175 | {
176 | encoding ??= _encoding;
177 | return encoding.GetString(Value.Span);
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Objects/IBObject.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.IO.Pipelines;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BDictionaryParser.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
3 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
4 | using System;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BListParser.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
3 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
4 | using System;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BNumberParser.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
3 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
4 | using System;
5 | using System.Buffers;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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.AsSpan(0, digitCount).ToArray(), 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(0, 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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BObjectParser.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
3 | using System.IO;
4 | using System.IO.Pipelines;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BObjectParserExtensions.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
2 | using System.IO;
3 |
4 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BObjectParserList.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
2 | using System;
3 | using System.Collections;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BStringParser.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
3 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
4 | using System;
5 | using System.Buffers;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace QuickLook.Plugin.TorrentViewer.Bencode.Parsing;
11 |
12 | ///
13 | /// A parser for bencoded byte strings.
14 | ///
15 | public class BStringParser : BObjectParser
16 | {
17 | ///
18 | /// The minimum stream length in bytes for a valid string ('0:').
19 | ///
20 | protected const int MinimumLength = 2;
21 |
22 | ///
23 | /// Creates an instance using for parsing.
24 | ///
25 | public BStringParser()
26 | : this(Encoding.UTF8)
27 | {
28 | }
29 |
30 | ///
31 | /// Creates an instance using the specified encoding for parsing.
32 | ///
33 | ///
34 | public BStringParser(Encoding encoding)
35 | {
36 | _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding));
37 | }
38 |
39 | ///
40 | /// The encoding used when creating the when parsing.
41 | ///
42 | public override Encoding Encoding => _encoding;
43 |
44 | private Encoding _encoding;
45 |
46 | ///
47 | /// Changes the encoding used for parsing.
48 | ///
49 | /// The new encoding to use.
50 | public void ChangeEncoding(Encoding encoding)
51 | {
52 | _encoding = encoding;
53 | }
54 |
55 | ///
56 | /// Parses the next from the reader.
57 | ///
58 | /// The reader to parse from.
59 | /// The parsed .
60 | /// Invalid bencode.
61 | /// The bencode is unsupported by this library.
62 | public override BString Parse(BencodeReader reader)
63 | {
64 | if (reader == null) throw new ArgumentNullException(nameof(reader));
65 |
66 | // Minimum valid bencode string is '0:' meaning an empty string
67 | if (reader.Length < MinimumLength)
68 | throw InvalidBencodeException.BelowMinimumLength(MinimumLength, reader.Length.Value, reader.Position);
69 |
70 | var startPosition = reader.Position;
71 |
72 | var buffer = ArrayPool.Shared.Rent(BString.LengthMaxDigits);
73 | try
74 | {
75 | var lengthString = buffer.AsSpan();
76 | var lengthStringCount = 0;
77 | for (var c = reader.ReadChar(); c != default && c.IsDigit(); c = reader.ReadChar())
78 | {
79 | EnsureLengthStringBelowMaxLength(lengthStringCount, startPosition);
80 |
81 | lengthString[lengthStringCount++] = c;
82 | }
83 |
84 | EnsurePreviousCharIsColon(reader.PreviousChar, reader.Position);
85 |
86 | var stringLength = ParseStringLength(lengthString, lengthStringCount, startPosition);
87 | var bytes = new byte[stringLength];
88 | var bytesRead = reader.Read(bytes);
89 |
90 | EnsureExpectedBytesRead(bytesRead, stringLength, startPosition);
91 |
92 | return new BString(bytes, Encoding);
93 | }
94 | finally
95 | {
96 | ArrayPool.Shared.Return(buffer);
97 | }
98 | }
99 |
100 | ///
101 | /// Parses the next from the reader.
102 | ///
103 | /// The reader to parse from.
104 | ///
105 | /// The parsed .
106 | /// Invalid bencode.
107 | /// The bencode is unsupported by this library.
108 | public override async ValueTask ParseAsync(PipeBencodeReader reader, CancellationToken cancellationToken = default)
109 | {
110 | if (reader == null) throw new ArgumentNullException(nameof(reader));
111 |
112 | var startPosition = reader.Position;
113 |
114 | using var memoryOwner = MemoryPool.Shared.Rent(BString.LengthMaxDigits);
115 | var lengthString = memoryOwner.Memory;
116 | var lengthStringCount = 0;
117 | for (var c = await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false);
118 | c != default && c.IsDigit();
119 | c = await reader.ReadCharAsync(cancellationToken).ConfigureAwait(false))
120 | {
121 | EnsureLengthStringBelowMaxLength(lengthStringCount, startPosition);
122 |
123 | lengthString.Span[lengthStringCount++] = c;
124 | }
125 |
126 | EnsurePreviousCharIsColon(reader.PreviousChar, reader.Position);
127 |
128 | var stringLength = ParseStringLength(lengthString.Span, lengthStringCount, startPosition);
129 | var bytes = new byte[stringLength];
130 | var bytesRead = await reader.ReadAsync(bytes, cancellationToken).ConfigureAwait(false);
131 |
132 | EnsureExpectedBytesRead(bytesRead, stringLength, startPosition);
133 |
134 | return new BString(bytes, Encoding);
135 | }
136 |
137 | ///
138 | /// Ensures that the length (number of digits) of the string-length part is not above
139 | /// as that would equal 10 GB of data, which we cannot handle.
140 | ///
141 | private void EnsureLengthStringBelowMaxLength(int lengthStringCount, long startPosition)
142 | {
143 | // Because of memory limitations (~1-2 GB) we know for certain we cannot handle more than 10 digits (10GB)
144 | if (lengthStringCount >= BString.LengthMaxDigits)
145 | {
146 | throw UnsupportedException(
147 | $"Length of string is more than {BString.LengthMaxDigits} digits (>10GB) and is not supported (max is ~1-2GB).",
148 | startPosition);
149 | }
150 | }
151 |
152 | ///
153 | /// Ensure that the previously read char is a colon (:),
154 | /// separating the string-length part and the actual string value.
155 | ///
156 | private void EnsurePreviousCharIsColon(char previousChar, long position)
157 | {
158 | if (previousChar != ':') throw InvalidBencodeException.UnexpectedChar(':', previousChar, position - 1);
159 | }
160 |
161 | ///
162 | /// Parses the string-length into a .
163 | ///
164 | private long ParseStringLength(Span lengthString, int lengthStringCount, long startPosition)
165 | {
166 | lengthString = lengthString.Slice(0, lengthStringCount);
167 |
168 | if (!ParseUtil.TryParseLongFast(lengthString, out var stringLength))
169 | throw InvalidException($"Invalid length '{lengthString.AsString()}' of string.", startPosition);
170 |
171 | // Int32.MaxValue is ~2GB and is the absolute maximum that can be handled in memory
172 | if (stringLength > int.MaxValue)
173 | {
174 | throw UnsupportedException(
175 | $"Length of string is {stringLength:N0} but maximum supported length is {int.MaxValue:N0}.",
176 | startPosition);
177 | }
178 |
179 | return stringLength;
180 | }
181 |
182 | ///
183 | /// Ensures that number of bytes read matches the expected number parsed from the string-length part.
184 | ///
185 | private void EnsureExpectedBytesRead(long bytesRead, long stringLength, long startPosition)
186 | {
187 | // If the two don't match we've reached the end of the stream before reading the expected number of chars
188 | if (bytesRead == stringLength) return;
189 |
190 | throw InvalidException(
191 | $"Expected string to be {stringLength:N0} bytes long but could only read {bytesRead:N0} bytes.",
192 | startPosition);
193 | }
194 |
195 | private static InvalidBencodeException InvalidException(string message, long startPosition)
196 | {
197 | return new InvalidBencodeException(
198 | $"{message} The string starts at position {startPosition}.",
199 | startPosition);
200 | }
201 |
202 | private static UnsupportedBencodeException UnsupportedException(string message, long startPosition)
203 | {
204 | return new UnsupportedBencodeException(
205 | $"{message} The string starts at position {startPosition}.",
206 | startPosition);
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BencodeParser.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
3 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
4 | using System;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace QuickLook.Plugin.TorrentViewer.Bencode.Parsing;
10 |
11 | ///
12 | /// Main class used for parsing bencode.
13 | ///
14 | public class BencodeParser : IBencodeParser
15 | {
16 | ///
17 | /// List of parsers used by the .
18 | ///
19 | public BObjectParserList Parsers { get; }
20 |
21 | ///
22 | /// The encoding use for parsing.
23 | ///
24 | public Encoding Encoding
25 | {
26 | get => _encoding;
27 | set
28 | {
29 | _encoding = value ?? throw new ArgumentNullException(nameof(value));
30 | Parsers.GetSpecific()?.ChangeEncoding(value);
31 | }
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 Torrents.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/BencodeParserExtensions.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
3 | using System.IO;
4 | using System.IO.Pipelines;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/IBObjectParser.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
3 | using System.IO;
4 | using System.IO.Pipelines;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/IBencodeParser.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.IO;
2 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
3 | using System.IO.Pipelines;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Parsing/ParseUtil.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/InvalidTorrentException.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Exceptions;
2 | using System;
3 |
4 | namespace QuickLook.Plugin.TorrentViewer.Bencode.Torrents;
5 |
6 | ///
7 | /// Represents parse errors when parsing torrents.
8 | ///
9 | public class InvalidTorrentException : BencodeException
10 | {
11 | public string InvalidField { get; set; }
12 |
13 | public InvalidTorrentException()
14 | {
15 | }
16 |
17 | public InvalidTorrentException(string message)
18 | : base(message)
19 | {
20 | }
21 |
22 | public InvalidTorrentException(string message, string invalidField)
23 | : base(message)
24 | {
25 | InvalidField = invalidField;
26 | }
27 |
28 | public InvalidTorrentException(string message, Exception inner)
29 | : base(message, inner)
30 | {
31 | }
32 | }
33 |
34 | #pragma warning restore 1591
35 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/MagnetLinkOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/MultiFileInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/MultiFileInfoList.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 | ///
19 | /// Name of directory to store files in.
20 | public MultiFileInfoList(string directoryName)
21 | {
22 | DirectoryName = directoryName;
23 | }
24 |
25 | ///
26 | /// Name of directory to store files in.
27 | /// Name of directory to store files in.
28 | public MultiFileInfoList(string directoryName, string directoryNameUtf8)
29 | {
30 | DirectoryName = directoryName;
31 | DirectoryNameUtf8 = directoryNameUtf8;
32 | }
33 |
34 | ///
35 | /// The name of the directory in which to store all the files. This is purely advisory.
36 | ///
37 | ///
38 | /// Corresponds to the 'name' field.
39 | ///
40 | public string DirectoryName { get; set; }
41 |
42 | ///
43 | /// The UTF-8 encoded name of the directory in which to store all the files. This is purely advisory.
44 | ///
45 | ///
46 | /// Corresponds to the 'name.utf-8' field.
47 | ///
48 | public string DirectoryNameUtf8 { get; set; }
49 | }
50 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/SingleFileInfo.cs:
--------------------------------------------------------------------------------
1 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/TorrentFields.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
2 |
3 | namespace QuickLook.Plugin.TorrentViewer.Bencode.Torrents;
4 |
5 | ///
6 | /// A reference of default torrent field names.
7 | ///
8 | public static class TorrentFields
9 | {
10 | public const string Announce = "announce";
11 | public const string AnnounceList = "announce-list";
12 | public const string CreatedBy = "created by";
13 | public const string CreationDate = "creation date";
14 | public const string Comment = "comment";
15 | public const string Encoding = "encoding";
16 | public const string Info = "info";
17 |
18 | public static readonly BString[] Keys =
19 | {
20 | Announce,
21 | AnnounceList,
22 | Comment,
23 | CreatedBy,
24 | CreationDate,
25 | Encoding,
26 | Info
27 | };
28 | }
29 |
30 | ///
31 | /// A reference of default torrent fields names in the 'info'-dictionary.
32 | ///
33 | public static class TorrentInfoFields
34 | {
35 | public const string Name = "name";
36 | public const string NameUtf8 = "name.utf-8";
37 | public const string Private = "private";
38 | public const string PieceLength = "piece length";
39 | public const string Pieces = "pieces";
40 | public const string Length = "length";
41 | public const string Md5Sum = "md5sum";
42 | public const string Files = "files";
43 |
44 | public static readonly BString[] Keys =
45 | {
46 | Name,
47 | NameUtf8,
48 | Private,
49 | PieceLength,
50 | Pieces,
51 | Length,
52 | Md5Sum,
53 | Files
54 | };
55 | }
56 |
57 | ///
58 | /// A reference of default torrent fields in the dictionaries in the 'files'-list in the 'info'-dictionary.s
59 | ///
60 | public static class TorrentFilesFields
61 | {
62 | public const string Length = "length";
63 | public const string Path = "path";
64 | public const string PathUtf8 = "path.utf-8";
65 | public const string Md5Sum = "md5sum";
66 |
67 | public static readonly BString[] Keys =
68 | {
69 | Length,
70 | Path,
71 | PathUtf8,
72 | Md5Sum
73 | };
74 | }
75 |
76 | #pragma warning restore 1591
77 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/TorrentFileMode.cs:
--------------------------------------------------------------------------------
1 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/TorrentParserMode.cs:
--------------------------------------------------------------------------------
1 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/Torrents/TorrentUtil.cs:
--------------------------------------------------------------------------------
1 | using QuickLook.Plugin.TorrentViewer.Bencode.Objects;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Security.Cryptography;
7 |
8 | namespace QuickLook.Plugin.TorrentViewer.Bencode.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 | [Obsolete]
40 | public static byte[] CalculateInfoHashBytes(Torrent torrent)
41 | {
42 | var info = torrent.ToBDictionary().Get("info");
43 | return CalculateInfoHashBytes(info);
44 | }
45 |
46 | ///
47 | /// Calculates the hash of the 'info'-dictionary.
48 | /// The info hash is a 20-byte SHA1 hash of the 'info'-dictionary of the torrent
49 | /// used to uniquely identify it and it's contents.
50 | ///
51 | /// Example: 6D60711ECF005C1147D8973A67F31A11454AB3F5
52 | ///
53 | /// The 'info'-dictionary of a torrent.
54 | /// A string representation of the 20-byte SHA1 hash without dashes.
55 | public static string CalculateInfoHash(BDictionary info)
56 | {
57 | var hashBytes = CalculateInfoHashBytes(info);
58 | return BytesToHexString(hashBytes);
59 | }
60 |
61 | ///
62 | /// Calculates the hash of the 'info'-dictionary.
63 | /// The info hash is a 20-byte SHA1 hash of the 'info'-dictionary of the torrent
64 | /// used to uniquely identify it and it's contents.
65 | ///
66 | /// Example: 6D60711ECF005C1147D8973A67F31A11454AB3F5
67 | ///
68 | /// The 'info'-dictionary of a torrent.
69 | /// A byte-array of the 20-byte SHA1 hash.
70 | public static byte[] CalculateInfoHashBytes(BDictionary info)
71 | {
72 | using (var sha1 = SHA1.Create())
73 | using (var stream = new MemoryStream())
74 | {
75 | info.EncodeTo(stream);
76 | stream.Position = 0;
77 |
78 | return sha1.ComputeHash(stream);
79 | }
80 | }
81 |
82 | ///
83 | /// Converts the byte array to a hexadecimal string representation without hyphens.
84 | ///
85 | ///
86 | public static string BytesToHexString(byte[] bytes)
87 | {
88 | return BitConverter.ToString(bytes).Replace("-", "");
89 | }
90 |
91 | ///
92 | /// Creates a Magnet link in the BTIH (BitTorrent Info Hash) format: xt=urn:btih:{info hash}
93 | ///
94 | /// Torrent to create Magnet link for.
95 | /// Controls how the Magnet link is constructed.
96 | ///
97 | public static string CreateMagnetLink(Torrent torrent, MagnetLinkOptions options = MagnetLinkOptions.IncludeTrackers)
98 | {
99 | var infoHash = torrent.GetInfoHash().ToLower();
100 | var displayName = torrent.DisplayName;
101 | var trackers = torrent.Trackers.Flatten();
102 |
103 | return CreateMagnetLink(infoHash, displayName, trackers, options);
104 | }
105 |
106 | ///
107 | /// Creates a Magnet link in the BTIH (BitTorrent Info Hash) format: xt=urn:btih:{info hash}
108 | ///
109 | /// The info has of the torrent.
110 | /// The display name of the torrent. Usually the file name or directory name for multi-file torrents
111 | /// A list of trackers if any.
112 | /// Controls how the Magnet link is constructed.
113 | ///
114 | public static string CreateMagnetLink(string infoHash, string displayName, IEnumerable trackers, MagnetLinkOptions options)
115 | {
116 | if (string.IsNullOrEmpty(infoHash))
117 | throw new ArgumentException("Info hash cannot be null or empty.", nameof(infoHash));
118 |
119 | var magnet = $"magnet:?xt=urn:btih:{infoHash}";
120 |
121 | if (!string.IsNullOrWhiteSpace(displayName))
122 | magnet += $"&dn={displayName}";
123 |
124 | var validEscapedTrackers =
125 | trackers?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(Uri.EscapeDataString).ToList() ??
126 | new List();
127 |
128 | if (options.HasFlag(MagnetLinkOptions.IncludeTrackers) && validEscapedTrackers.Any())
129 | {
130 | var trackersString = string.Join("&", validEscapedTrackers.Select(x => $"tr={x}"));
131 | magnet += $"&{trackersString}";
132 | }
133 |
134 | return magnet;
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Bencode/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 QuickLook.Plugin.TorrentViewer.Bencode;
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.ToArray());
135 | }
136 |
137 | public static string AsString(this Span chars)
138 | {
139 | return new string(chars.ToArray());
140 | }
141 |
142 | public static string AsString(this Memory chars)
143 | {
144 | return new string(chars.Span.ToArray());
145 | }
146 |
147 | public static int GetBytes(this Encoding encoding, ReadOnlySpan chars, Span bytes)
148 | {
149 | char[] charArray = chars.ToArray();
150 | byte[] byteArray = encoding.GetBytes(charArray);
151 |
152 | if (byteArray.Length > bytes.Length)
153 | {
154 | throw new ArgumentException("The byte span is too small to hold the result.");
155 | }
156 |
157 | byteArray.CopyTo(bytes);
158 | return byteArray.Length;
159 | }
160 |
161 | public static string GetString(this Encoding encoding, ReadOnlySpan bytes)
162 | {
163 | byte[] byteArray = bytes.ToArray();
164 | return encoding.GetString(byteArray);
165 | }
166 |
167 | public static void Write(this Stream stream, ReadOnlySpan buffer)
168 | {
169 | byte[] byteArray = buffer.ToArray();
170 | stream.Write(byteArray, 0, byteArray.Length);
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Converters.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Paddy Xu
2 | //
3 | // This file is part of QuickLook program.
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with this program. If not, see .
17 |
18 | using QuickLook.Common.ExtensionMethods;
19 | using System;
20 | using System.Globalization;
21 | using System.Windows;
22 | using System.Windows.Data;
23 |
24 | namespace QuickLook.Plugin.TorrentViewer;
25 |
26 | public class SizePrettyPrintConverter : DependencyObject, IValueConverter
27 | {
28 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
29 | {
30 | var size = (long)value;
31 |
32 | return size.ToPrettySize(2);
33 | }
34 |
35 | public object ConvertBack(object value, Type targetTypes, object parameter, CultureInfo culture)
36 | {
37 | throw new NotImplementedException();
38 | }
39 | }
40 |
41 | public class FileExtToIconConverter : DependencyObject, IValueConverter
42 | {
43 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
44 | {
45 | var name = (string)value;
46 |
47 | return IconManager.FindIconForFilename(name, false);
48 | }
49 |
50 | public object ConvertBack(object value, Type targetTypes, object parameter, CultureInfo culture)
51 | {
52 | throw new NotImplementedException();
53 | }
54 | }
55 |
56 | public class AddConverter : DependencyObject, IValueConverter
57 | {
58 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
59 | {
60 | if (value is double p1 && double.TryParse(parameter?.ToString(), out double p2))
61 | {
62 | return p1 + p2;
63 | }
64 | return value;
65 | }
66 |
67 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
68 | {
69 | throw new NotImplementedException();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/Block.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | namespace QuickLook.Plugin.TorrentViewer.Data;
9 |
10 | ///
11 | /// Represents a region of a piece with a specified offset and length.
12 | ///
13 | public class Block
14 | {
15 | ///
16 | /// Initializes a new instance of the class,
17 | /// with the specified piece index, offset and data.
18 | ///
19 | /// Index of the piece the block belongs to.
20 | /// Offset into the piece at which the data starts.
21 | /// Data for the block.
22 | public Block(int pieceIndex, int offset, byte[] data)
23 | {
24 | PieceIndex = pieceIndex;
25 | Offset = offset;
26 | Data = data;
27 | }
28 |
29 | ///
30 | /// Gets a value indicating the index of the piece to which the block belongs.
31 | ///
32 | public int PieceIndex { get; }
33 |
34 | ///
35 | /// Gets a value indicating the offset into the piece the data contained within the block represents.
36 | ///
37 | public int Offset { get; }
38 |
39 | ///
40 | /// Gets the data contained within the block.
41 | ///
42 | public byte[] Data { get; }
43 |
44 | ///
45 | /// Gets a value indicating the number of bytes contained within the block.
46 | ///
47 | public int Length => Data.Length;
48 | }
49 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/BlockComparer.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System.Collections.Generic;
9 |
10 | namespace QuickLook.Plugin.TorrentViewer.Data;
11 |
12 | internal class BlockComparer : IComparer
13 | {
14 | public int Compare(Block x, Block y)
15 | {
16 | if (x.Offset == y.Offset)
17 | return 0;
18 | else if (x.Offset < y.Offset)
19 | return -1;
20 | else
21 | return 1;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/BlockDataHandler.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System;
9 | using System.IO;
10 |
11 | namespace QuickLook.Plugin.TorrentViewer.Data;
12 |
13 | ///
14 | /// Provides access to a collection of files as a single block of data.
15 | ///
16 | internal class BlockDataHandler : IBlockDataHandler
17 | {
18 | ///
19 | /// Initializes a new instance of the class,
20 | /// using the specified file handler as the data source.
21 | ///
22 | /// File handler to use as the data source.
23 | /// Metainfo file description.
24 | public BlockDataHandler(IFileHandler fileHandler, Metainfo metainfo)
25 | {
26 | FileHandler = fileHandler;
27 | Metainfo = metainfo;
28 | }
29 |
30 | ///
31 | /// Gets the file handler used internally for data access.
32 | ///
33 | public IFileHandler FileHandler { get; }
34 |
35 | ///
36 | /// Gets the metainfo for this piece checker.
37 | ///
38 | public Metainfo Metainfo { get; }
39 |
40 | public void Flush()
41 | {
42 | FileHandler.Flush();
43 | }
44 |
45 | ///
46 | /// Returns a copy of the contiguous file data starting at the specified offset.
47 | ///
48 | /// Offset to read from.
49 | /// Number of bytes to read.
50 | /// Block data from specified region.
51 | public byte[] ReadBlockData(long offset, long length)
52 | {
53 | byte[] data;
54 | TryReadBlockData(offset, length, out data);
55 | return data;
56 | }
57 |
58 | ///
59 | /// Returns a copy of the contiguous file data starting at the specified offset.
60 | ///
61 | /// Offset to read from.
62 | /// Number of bytes to read.
63 | /// The returned data.
64 | /// Block data from specified region.
65 | public bool TryReadBlockData(long offset, long length, out byte[] data)
66 | {
67 | // Open the first file for reading
68 | long remainder;
69 | int currentFile = Metainfo.FileIndex(offset, out remainder);
70 | Stream fileStream = FileHandler.GetFileStream(Metainfo.Files[currentFile].Name);
71 | fileStream.Seek(remainder, SeekOrigin.Begin);
72 |
73 | // Fill the current piece with file data
74 | int copied = 0;
75 | data = new byte[length];
76 | while (copied < length)
77 | {
78 | // Move to next file if necessary
79 | if (fileStream.Position == Metainfo.Files[currentFile].Size)
80 | {
81 | currentFile++;
82 | fileStream = FileHandler.GetFileStream(Metainfo.Files[currentFile].Name);
83 | fileStream.Seek(0, SeekOrigin.Begin);
84 | }
85 |
86 | // Copy to end of file, or end of piece
87 | int toRead = (int)Math.Min(length, Metainfo.Files[currentFile].Size - fileStream.Position);
88 |
89 | // Check if going beyond end of file
90 | if (fileStream.Length - fileStream.Position < toRead)
91 | {
92 | data = null;
93 | return false;
94 | }
95 |
96 | fileStream.Read(data, copied, toRead);
97 | copied += toRead;
98 | }
99 |
100 | return true;
101 | }
102 |
103 | ///
104 | /// Writes the specified contiguous data from the given offset position.
105 | ///
106 | /// Offset at which to start writing.
107 | /// Block data to write.
108 | public void WriteBlockData(long offset, byte[] data)
109 | {
110 | long remainder;
111 | int fileIndex = Metainfo.FileIndex(offset, out remainder);
112 | Stream fileStream = FileHandler.GetFileStream(Metainfo.Files[fileIndex].Name);
113 | if (fileStream.Length < remainder)
114 | {
115 | fileStream.Seek(fileStream.Length, SeekOrigin.Begin);
116 | long extra = remainder - fileStream.Length;
117 | byte[] padding = new byte[extra];
118 | fileStream.Write(padding, 0, padding.Length);
119 | }
120 | fileStream.Seek(remainder, SeekOrigin.Begin);
121 |
122 | long written = 0;
123 |
124 | // Change to LongLength: dotnet/corefx#9998
125 | while (written < data.Length)
126 | {
127 | // Move to next file if necessary
128 | if (fileStream.Position == Metainfo.Files[fileIndex].Size)
129 | {
130 | fileIndex++;
131 | fileStream = FileHandler.GetFileStream(Metainfo.Files[fileIndex].Name);
132 | }
133 |
134 | // Write to end of file, or end of data
135 | // Change to LongLength: dotnet/corefx#9998
136 | int toWrite = (int)Math.Min(data.Length - written, Metainfo.Files[fileIndex].Size - fileStream.Position);
137 | fileStream.Write(data, (int)written, toWrite);
138 | written += toWrite;
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/ContainedFile.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | namespace QuickLook.Plugin.TorrentViewer.Data;
9 |
10 | ///
11 | /// Describes a single file within a download collection.
12 | ///
13 | public class ContainedFile
14 | {
15 | public ContainedFile(string name, long size)
16 | {
17 | Name = name;
18 | Size = size;
19 | }
20 |
21 | ///
22 | /// Gets the name of this file, including directories.
23 | ///
24 | public string Name { get; }
25 |
26 | ///
27 | /// Gets the size of the file, in bytes.
28 | ///
29 | public long Size { get; }
30 | }
31 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/DiskFileHandler.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System;
9 | using System.Collections.Generic;
10 | using System.IO;
11 |
12 | namespace QuickLook.Plugin.TorrentViewer.Data;
13 |
14 | ///
15 | /// Provides a disk-based implementation of an IFileHandler.
16 | ///
17 | public class DiskFileHandler : IFileHandler
18 | {
19 | private readonly Dictionary _openFiles;
20 |
21 | ///
22 | /// Initializes a new instance of the class using the specified directory.
23 | ///
24 | /// Base directory for all files.
25 | public DiskFileHandler(string directory)
26 | {
27 | Directory = directory;
28 | _openFiles = new Dictionary();
29 | }
30 |
31 | ///
32 | /// Gets the base directory for files.
33 | ///
34 | public string Directory { get; }
35 |
36 | public Stream GetFileStream(string fileName)
37 | {
38 | string path = FullName(fileName);
39 | if (!File.Exists(path))
40 | File.WriteAllText(path, string.Empty);
41 |
42 | FileStream stream;
43 | if (!_openFiles.TryGetValue(fileName, out stream))
44 | {
45 | stream = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
46 | _openFiles.Add(fileName, stream);
47 | }
48 |
49 | return stream;
50 | }
51 |
52 | public void CloseFileStream(Stream file)
53 | {
54 | if (!_openFiles.ContainsValue((FileStream)file))
55 | throw new InvalidOperationException("Cannot close stream. File is not open.");
56 |
57 | // Remove from collection of open files
58 | string fileName = null;
59 | foreach (var stream in _openFiles)
60 | {
61 | if (stream.Value == file)
62 | {
63 | fileName = stream.Key;
64 | break;
65 | }
66 | }
67 | _openFiles.Remove(fileName);
68 |
69 | file.Dispose();
70 | }
71 |
72 | public void Flush()
73 | {
74 | foreach (var file in _openFiles)
75 | {
76 | file.Value.Flush();
77 | }
78 | }
79 |
80 | public void Dispose()
81 | {
82 | foreach (var file in _openFiles)
83 | {
84 | file.Value.Flush();
85 | file.Value.Dispose();
86 | }
87 | }
88 |
89 | string FullName(string fileName)
90 | {
91 | return Path.Combine(Directory, fileName);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/IBlockDataHandler.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | namespace QuickLook.Plugin.TorrentViewer.Data;
9 |
10 | ///
11 | /// Provides access to a collection of files as a single block of data.
12 | ///
13 | public interface IBlockDataHandler
14 | {
15 | ///
16 | /// Gets the metainfo describing the layout of the collection of files.
17 | ///
18 | Metainfo Metainfo { get; }
19 |
20 | ///
21 | /// Returns a copy of the contiguous file data starting at the specified offset.
22 | ///
23 | /// Offset to read from.
24 | /// Number of bytes to read.
25 | /// Block data from specified region.
26 | byte[] ReadBlockData(long offset, long length);
27 |
28 | ///
29 | /// Attempts to read a copy of the contiguous file data starting at the specified offset.
30 | ///
31 | /// Offset to read from.
32 | /// Number of bytes to read.
33 | /// Block data from specified region.
34 | /// Value indicating whether the operation was successful.
35 | bool TryReadBlockData(long offset, long length, out byte[] data);
36 |
37 | ///
38 | /// Writes the specified contiguous data from the given offset position.
39 | ///
40 | /// Offset at which to start writing.
41 | /// Block data to write.
42 | void WriteBlockData(long offset, byte[] data);
43 |
44 | ///
45 | /// Flushes any underlying streams to ensure all data has been written.
46 | ///
47 | void Flush();
48 | }
49 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/IFileHandler.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System;
9 | using System.IO;
10 |
11 | namespace QuickLook.Plugin.TorrentViewer.Data;
12 |
13 | ///
14 | /// Provides access to files as streams.
15 | ///
16 | public interface IFileHandler : IDisposable
17 | {
18 | Stream GetFileStream(string fileName);
19 |
20 | void CloseFileStream(Stream file);
21 |
22 | void Flush();
23 | }
24 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/IPieceCalculator.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System.Collections.Generic;
9 |
10 | namespace QuickLook.Plugin.TorrentViewer.Data;
11 |
12 | internal interface IPieceCalculator
13 | {
14 | void ComputePieces(List files, int pieceSize, IFileHandler fileHandler, List pieces);
15 | }
16 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/MemoryFileHandler.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System.Collections.Generic;
9 | using System.IO;
10 |
11 | namespace QuickLook.Plugin.TorrentViewer.Data;
12 |
13 | ///
14 | /// An IFileHandler that stores data in memory.
15 | ///
16 | public class MemoryFileHandler : IFileHandler
17 | {
18 | private readonly Dictionary _files;
19 |
20 | public MemoryFileHandler()
21 | {
22 | _files = new Dictionary();
23 | }
24 |
25 | public MemoryFileHandler(Dictionary existingFileData)
26 | {
27 | _files = new Dictionary();
28 | foreach (var existing in existingFileData)
29 | _files.Add(existing.Key, new MemoryStream(existing.Value));
30 | }
31 |
32 | public MemoryFileHandler(string existingFile, byte[] existingData)
33 | {
34 | _files = new Dictionary();
35 | _files.Add(existingFile, new MemoryStream(existingData));
36 | }
37 |
38 | public Stream GetFileStream(string fileName)
39 | {
40 | MemoryStream stream;
41 | if (!_files.TryGetValue(fileName, out stream))
42 | {
43 | stream = new MemoryStream();
44 | _files.Add(fileName, stream);
45 | }
46 |
47 | return stream;
48 | }
49 |
50 | public void CloseFileStream(Stream file)
51 | {
52 | // Don't close the stream or the data will be lost
53 | }
54 |
55 | public void Flush()
56 | {
57 | // Don't need to do anything
58 | }
59 |
60 | public void Dispose()
61 | {
62 | foreach (var file in _files)
63 | file.Value.Dispose();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/Metainfo.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System;
9 | using System.Collections.Generic;
10 | using System.Collections.ObjectModel;
11 | using System.Linq;
12 |
13 | namespace QuickLook.Plugin.TorrentViewer.Data;
14 |
15 | ///
16 | /// Describes a set of files.
17 | ///
18 | public class Metainfo
19 | {
20 | private readonly ReadOnlyCollection _pieces;
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | /// The name of the torrent.
26 | /// SHA-1 hash of the metadata.
27 | /// List of files to include.
28 | /// List of pieces to include.
29 | /// URLs of the trackers.
30 | /// Info section of the metadata file.
31 | public Metainfo(string name,
32 | Sha1Hash infoHash,
33 | IEnumerable files,
34 | IEnumerable pieces,
35 | IEnumerable> trackers,
36 | IReadOnlyList metadata)
37 | {
38 | Name = name;
39 | _pieces = new ReadOnlyCollection(pieces.ToList());
40 | InfoHash = infoHash;
41 | Files = [.. files];
42 | TotalSize = Files.Any() ? Files.Sum(f => f.Size) : 0;
43 | Trackers = trackers.Select(x => (IReadOnlyList)new ReadOnlyCollection(x.ToList())).ToList().AsReadOnly();
44 | PieceSize = _pieces.First().Size;
45 | Metadata = metadata;
46 | }
47 |
48 | ///
49 | /// Gets the name of the torrent.
50 | ///
51 | public string Name { get; }
52 |
53 | ///
54 | /// Gets a hash of the data for this set of files.
55 | ///
56 | public Sha1Hash InfoHash { get; }
57 |
58 | ///
59 | /// Gets the 'info' section of the metainfo file.
60 | ///
61 | public IReadOnlyList Metadata { get; }
62 |
63 | ///
64 | /// Gets the list of files contained within this collection.
65 | ///
66 | public List Files { get; }
67 |
68 | ///
69 | /// Gets the total size in bytes of this set of files.
70 | ///
71 | public long TotalSize { get; }
72 |
73 | ///
74 | /// Gets the uris of the trackers managing downloads for this set of files.
75 | ///
76 | public IReadOnlyList> Trackers { get; }
77 |
78 | ///
79 | /// Gets a value indicating the number of bytes in each piece.
80 | ///
81 | public int PieceSize { get; }
82 |
83 | ///
84 | /// Gets a read-only list of pieces for this set of files.
85 | ///
86 | public IReadOnlyList Pieces => _pieces;
87 |
88 | ///
89 | /// Returns the offset in bytes to the start of the specified piece.
90 | ///
91 | /// The piece to calculate the offset for.
92 | /// Offset in bytes.
93 | public long PieceOffset(Piece piece)
94 | {
95 | return (long)piece.Index * (long)PieceSize;
96 | }
97 |
98 | ///
99 | /// Returns the index of the first file containing the data at the specified offset.
100 | ///
101 | /// Offset into the file data, in bytes.
102 | /// Index of file for the specified offset.
103 | public int FileIndex(long offset)
104 | {
105 | return FileIndex(offset, out long _);
106 | }
107 |
108 | ///
109 | /// Returns the index of the first file containing the data at the specified offset.
110 | ///
111 | /// Offset into the file data, in bytes.
112 | /// Offset into file at which data begins.
113 | /// Index of file for the specified offset.
114 | public int FileIndex(long offset, out long remainder)
115 | {
116 | if (offset < 0)
117 | throw new IndexOutOfRangeException();
118 |
119 | if (Files.Count == 0)
120 | throw new IndexOutOfRangeException();
121 |
122 | int fileIndex = 0;
123 | while (offset > Files[fileIndex].Size)
124 | {
125 | ContainedFile result = Files[fileIndex];
126 | offset -= result.Size;
127 | fileIndex++;
128 |
129 | if (fileIndex > Files.Count)
130 | throw new IndexOutOfRangeException();
131 | }
132 |
133 | remainder = offset;
134 | return fileIndex;
135 | }
136 |
137 | ///
138 | /// Returns the offset in the file block data at which the data for the specified file begins.
139 | ///
140 | /// Index of file to find offset for.
141 | /// Offset to file in bytes.
142 | public long FileOffset(int fileIndex)
143 | {
144 | long offset = 0;
145 | for (int i = 0; i < fileIndex; i++)
146 | offset += Files[i].Size;
147 | return offset;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/MetainfoBuilder.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System;
9 | using System.Collections.Generic;
10 | using System.Linq;
11 |
12 | namespace QuickLook.Plugin.TorrentViewer.Data;
13 |
14 | public sealed class MetainfoBuilder
15 | {
16 | private readonly string _name;
17 | private readonly IReadOnlyCollection> _files;
18 | private readonly IReadOnlyCollection _trackers;
19 |
20 | public MetainfoBuilder(string torrentName)
21 | {
22 | _name = torrentName;
23 | _files = new List>();
24 | _trackers = new List();
25 | }
26 |
27 | private MetainfoBuilder(IEnumerable> files,
28 | IEnumerable trackers)
29 | {
30 | _files = files.ToList();
31 | _trackers = trackers.ToList();
32 | }
33 |
34 | public MetainfoBuilder AddFile(string fileName, byte[] data)
35 | {
36 | return new MetainfoBuilder(_files.Concat(new[] { Tuple.Create(fileName, data) }),
37 | _trackers);
38 | }
39 |
40 | public MetainfoBuilder WithTracker(Uri trackerUri)
41 | {
42 | return new MetainfoBuilder(_files,
43 | _trackers.Concat(new[] { trackerUri }));
44 | }
45 |
46 | public Metainfo Build()
47 | {
48 | var containedFiles = _files.Select(x => new ContainedFile(x.Item1, x.Item2.Length)).ToList();
49 | var fileHandler = new MemoryFileHandler(_files.ToDictionary(x => x.Item1, x => x.Item2));
50 |
51 | var pieces = PieceCalculator.ComputePieces(containedFiles, 256000, fileHandler);
52 | return new Metainfo(_name, Sha1Hash.Empty, containedFiles, pieces, new[] { _trackers }, null);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/Piece.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | namespace QuickLook.Plugin.TorrentViewer.Data;
9 |
10 | ///
11 | /// Represents a piece of data in a collection of files.
12 | ///
13 | public class Piece
14 | {
15 | ///
16 | /// Initializes a new instance of the class with the specified index.
17 | ///
18 | /// Zero-based index of the piece.
19 | public Piece(int index)
20 | {
21 | Index = index;
22 | }
23 |
24 | ///
25 | /// Initializes a new instance of the class
26 | /// with the specified index, size, files and hash.
27 | ///
28 | /// Zero-based index of the piece.
29 | /// Size of the piece, in bytes.
30 | /// Hash for the piece.
31 | public Piece(int index, int size, Sha1Hash hash)
32 | {
33 | Index = index;
34 | Size = size;
35 | Hash = hash;
36 | }
37 |
38 | ///
39 | /// Gets a value indicating the zero-based piece index.
40 | ///
41 | public int Index { get; }
42 |
43 | ///
44 | /// Gets the piece size, in bytes.
45 | ///
46 | public int Size { get; }
47 |
48 | ///
49 | /// Gets the SHA-1 hash for this piece.
50 | ///
51 | public Sha1Hash Hash { get; }
52 | }
53 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/PieceCalculator.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System;
9 | using System.Collections.Generic;
10 | using System.IO;
11 | using System.Linq;
12 | using System.Security.Cryptography;
13 |
14 | namespace QuickLook.Plugin.TorrentViewer.Data;
15 |
16 | internal class PieceCalculator
17 | {
18 | public static IList ComputePieces(IList files, int pieceSize, IFileHandler fileHandler)
19 | {
20 | var pieces = new List();
21 |
22 | if (files.Count == 0)
23 | return pieces;
24 |
25 | long totalLength = files.Sum(f => f.Size);
26 |
27 | int currentFile = 0;
28 | Stream fileStream = fileHandler.GetFileStream(files[currentFile].Name);
29 | long offset = 0;
30 | int pieceCounter = 0;
31 |
32 | using (var sha1 = SHA1.Create())
33 | {
34 | // Full pieces
35 | while (offset <= totalLength - pieceSize && totalLength - pieceSize > 0)
36 | {
37 | byte[] data = GetBlockData(fileHandler, files, pieceSize, ref currentFile, ref fileStream);
38 |
39 | Sha1Hash pieceHash = new Sha1Hash(sha1.ComputeHash(data));
40 | Piece piece = new Piece(pieceCounter, pieceSize, pieceHash);
41 | pieces.Add(piece);
42 | offset += pieceSize;
43 | pieceCounter++;
44 | }
45 |
46 | // Remaining smaller piece
47 | long remaining = totalLength - offset;
48 | if (remaining > 0)
49 | {
50 | byte[] remainingData = GetBlockData(fileHandler, files, remaining, ref currentFile, ref fileStream);
51 |
52 | Sha1Hash pieceHash = new Sha1Hash(sha1.ComputeHash(remainingData));
53 | Piece piece = new Piece(pieceCounter, (int)remaining, pieceHash);
54 | pieces.Add(piece);
55 | }
56 | }
57 |
58 | return pieces;
59 | }
60 |
61 | private static byte[] GetBlockData(IFileHandler fileHandler, IList files, long length, ref int currentFile, ref Stream fileStream)
62 | {
63 | // Fill the current piece with file data
64 | int copied = 0;
65 | var data = new byte[length];
66 | while (copied < length)
67 | {
68 | // Move to next file if necessary
69 | if (fileStream.Position == files[currentFile].Size)
70 | {
71 | currentFile++;
72 | fileStream = fileHandler.GetFileStream(files[currentFile].Name);
73 | }
74 |
75 | // Copy to end of file, or end of piece
76 | int toRead = (int)Math.Min(length, files[currentFile].Size - fileStream.Position);
77 |
78 | // Check if going beyond end of file
79 | if (fileStream.Length - fileStream.Position < toRead)
80 | throw new InvalidOperationException("Tried to read beyond the end of the file.");
81 |
82 | fileStream.Read(data, copied, toRead);
83 | copied += toRead;
84 | }
85 |
86 | return data;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Data/Sha1Hash.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using System;
9 | using System.Linq;
10 |
11 | namespace QuickLook.Plugin.TorrentViewer.Data;
12 |
13 | ///
14 | /// Represents a SHA-1 hash result.
15 | ///
16 | public sealed class Sha1Hash
17 | {
18 | ///
19 | /// Gets the length in bytes of a raw hash data.
20 | ///
21 | public const int Length = 20;
22 |
23 | ///
24 | /// Gets the empty hash.
25 | ///
26 | public static readonly Sha1Hash Empty;
27 |
28 | static Sha1Hash()
29 | {
30 | Empty = new Sha1Hash(new byte[Length]);
31 | }
32 |
33 | ///
34 | /// Initializes a new instance of the class with the specified value.
35 | ///
36 | /// 20-byte value of the hash.
37 | public Sha1Hash(byte[] value)
38 | {
39 | if (value == null || value.Length != Length)
40 | throw new ArgumentException(string.Format("Value must be {0} bytes.", Length));
41 |
42 | Value = value;
43 | }
44 |
45 | public byte[] Value { get; }
46 |
47 | public static implicit operator byte[](Sha1Hash hash)
48 | {
49 | return hash.Value;
50 | }
51 |
52 | public static bool operator ==(Sha1Hash x, Sha1Hash y)
53 | {
54 | if (ReferenceEquals(x, y))
55 | return true;
56 | else if ((object)x == null || ((object)y == null))
57 | return false;
58 | else
59 | return Enumerable.SequenceEqual(x.Value, y.Value);
60 | }
61 |
62 | public static bool operator !=(Sha1Hash x, Sha1Hash y)
63 | {
64 | return !(x == y);
65 | }
66 |
67 | public override bool Equals(object obj)
68 | {
69 | if (obj is Sha1Hash other)
70 | {
71 | return Enumerable.SequenceEqual(Value, other.Value);
72 | }
73 | else
74 | {
75 | return false;
76 | }
77 | }
78 |
79 | public override int GetHashCode()
80 | {
81 | unchecked
82 | {
83 | int hash = 17;
84 | foreach (byte el in Value)
85 | hash = hash * 31 + el.GetHashCode();
86 | return hash;
87 | }
88 | }
89 |
90 | public override string ToString()
91 | {
92 | string base64 = Convert.ToBase64String(Value);
93 | return base64.Substring(0, 8);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/IconManager.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2017 Paddy Xu
2 | //
3 | // This file is part of QuickLook program.
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with this program. If not, see .
17 |
18 | using System;
19 | using System.Collections.Generic;
20 | using System.Drawing;
21 | using System.IO;
22 | using System.Runtime.InteropServices;
23 | using System.Windows;
24 | using System.Windows.Interop;
25 | using System.Windows.Media;
26 | using System.Windows.Media.Imaging;
27 |
28 | namespace QuickLook.Plugin.TorrentViewer;
29 |
30 | ///
31 | /// Internals are mostly from here:
32 | /// http://www.codeproject.com/Articles/2532/Obtaining-and-managing-file-and-folder-icons-using
33 | /// Caches all results.
34 | ///
35 | public static class IconManager
36 | {
37 | private static ImageSource SmallDirIcon = null!;
38 | private static ImageSource LargeDirIcon = null!;
39 | private static readonly Dictionary SmallIconCache = [];
40 | private static readonly Dictionary LargeIconCache = [];
41 |
42 | public static void ClearCache()
43 | {
44 | SmallDirIcon = LargeDirIcon = null!;
45 |
46 | SmallIconCache.Clear();
47 | LargeIconCache.Clear();
48 | }
49 |
50 | ///
51 | /// Get the icon of a directory
52 | ///
53 | /// 16x16 or 32x32 icon
54 | /// an icon
55 | public static ImageSource FindIconForDir(bool large)
56 | {
57 | var icon = large ? LargeDirIcon : SmallDirIcon;
58 | if (icon != null)
59 | return icon;
60 | icon = IconReader.GetFolderIcon(large ? IconReader.IconSize.Large : IconReader.IconSize.Small,
61 | false)
62 | .ToImageSource();
63 | if (large)
64 | LargeDirIcon = icon;
65 | else
66 | SmallDirIcon = icon;
67 | return icon;
68 | }
69 |
70 | ///
71 | /// Get an icon for a given filename
72 | ///
73 | /// any filename
74 | /// 16x16 or 32x32 icon
75 | /// null if path is null, otherwise - an icon
76 | public static ImageSource FindIconForFilename(string fileName, bool large)
77 | {
78 | var extension = Path.GetExtension(fileName);
79 | if (extension == null)
80 | return null!;
81 | var cache = large ? LargeIconCache : SmallIconCache;
82 | if (cache.TryGetValue(extension, out ImageSource icon))
83 | return icon;
84 | icon = IconReader.GetFileIcon(fileName, large ? IconReader.IconSize.Large : IconReader.IconSize.Small,
85 | false)
86 | .ToImageSource();
87 | cache.Add(extension, icon);
88 | return icon;
89 | }
90 |
91 | ///
92 | /// http://stackoverflow.com/a/6580799/1943849
93 | ///
94 | private static ImageSource ToImageSource(this Icon icon)
95 | {
96 | var imageSource = Imaging.CreateBitmapSourceFromHIcon(
97 | icon.Handle,
98 | Int32Rect.Empty,
99 | BitmapSizeOptions.FromEmptyOptions());
100 | return imageSource;
101 | }
102 |
103 | ///
104 | /// Provides static methods to read system icons for both folders and files.
105 | ///
106 | ///
107 | /// IconReader.GetFileIcon("c:\\general.xls");
108 | ///
109 | private static class IconReader
110 | {
111 | ///
112 | /// Options to specify the size of icons to return.
113 | ///
114 | public enum IconSize
115 | {
116 | ///
117 | /// Specify large icon - 32 pixels by 32 pixels.
118 | ///
119 | Large = 0,
120 |
121 | ///
122 | /// Specify small icon - 16 pixels by 16 pixels.
123 | ///
124 | Small = 1
125 | }
126 |
127 | ///
128 | /// Returns the icon of a folder.
129 | ///
130 | /// Large or small
131 | /// Whether to include the link icon
132 | /// System.Drawing.Icon
133 | public static Icon GetFolderIcon(IconSize size, bool linkOverlay)
134 | {
135 | var shfi = new Shell32.Shfileinfo();
136 | var flags = Shell32.ShgfiIcon | Shell32.ShgfiUsefileattributes;
137 | if (linkOverlay) flags += Shell32.ShgfiLinkoverlay;
138 | /* Check the size specified for return. */
139 | if (IconSize.Small == size)
140 | flags += Shell32.ShgfiSmallicon;
141 | else
142 | flags += Shell32.ShgfiLargeicon;
143 | Shell32.SHGetFileInfo("placeholder",
144 | Shell32.FileAttributeDirectory,
145 | ref shfi,
146 | (uint)Marshal.SizeOf(shfi),
147 | flags);
148 | // Copy (clone) the returned icon to a new object, thus allowing us to clean-up properly
149 | var icon = (Icon)Icon.FromHandle(shfi.hIcon).Clone();
150 | User32.DestroyIcon(shfi.hIcon); // Cleanup
151 | return icon;
152 | }
153 |
154 | ///
155 | /// Returns an icon for a given file - indicated by the name parameter.
156 | ///
157 | /// Pathname for file.
158 | /// Large or small
159 | /// Whether to include the link icon
160 | /// System.Drawing.Icon
161 | public static Icon GetFileIcon(string name, IconSize size, bool linkOverlay)
162 | {
163 | var shfi = new Shell32.Shfileinfo();
164 | var flags = Shell32.ShgfiIcon | Shell32.ShgfiUsefileattributes;
165 | if (linkOverlay) flags += Shell32.ShgfiLinkoverlay;
166 | /* Check the size specified for return. */
167 | if (IconSize.Small == size)
168 | flags += Shell32.ShgfiSmallicon;
169 | else
170 | flags += Shell32.ShgfiLargeicon;
171 | Shell32.SHGetFileInfo(name,
172 | Shell32.FileAttributeNormal,
173 | ref shfi,
174 | (uint)Marshal.SizeOf(shfi),
175 | flags);
176 | // Copy (clone) the returned icon to a new object, thus allowing us to clean-up properly
177 | var icon = (Icon)Icon.FromHandle(shfi.hIcon).Clone();
178 | User32.DestroyIcon(shfi.hIcon); // Cleanup
179 | return icon;
180 | }
181 | }
182 |
183 | ///
184 | /// Wraps necessary Shell32.dll structures and functions required to retrieve Icon Handles using SHGetFileInfo. Code
185 | /// courtesy of MSDN Cold Rooster Consulting case study.
186 | ///
187 | private static class Shell32
188 | {
189 | private const int MaxPath = 256;
190 | public const uint ShgfiIcon = 0x000000100; // get icon
191 | public const uint ShgfiLinkoverlay = 0x000008000; // put a link overlay on icon
192 | public const uint ShgfiLargeicon = 0x000000000; // get large icon
193 | public const uint ShgfiSmallicon = 0x000000001; // get small icon
194 | public const uint ShgfiUsefileattributes = 0x000000010; // use passed dwFileAttribute
195 | public const uint FileAttributeNormal = 0x00000080;
196 | public const uint FileAttributeDirectory = 0x00000010;
197 |
198 | [DllImport("Shell32.dll")]
199 | public static extern IntPtr SHGetFileInfo(
200 | string pszPath,
201 | uint dwFileAttributes,
202 | ref Shfileinfo psfi,
203 | uint cbFileInfo,
204 | uint uFlags
205 | );
206 |
207 | [StructLayout(LayoutKind.Sequential)]
208 | public struct Shfileinfo
209 | {
210 | private const int Namesize = 80;
211 | public readonly IntPtr hIcon;
212 | private readonly int iIcon;
213 | private readonly uint dwAttributes;
214 |
215 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MaxPath)]
216 | private readonly string szDisplayName;
217 |
218 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = Namesize)]
219 | private readonly string szTypeName;
220 | }
221 | }
222 |
223 | ///
224 | /// Wraps necessary functions imported from User32.dll. Code courtesy of MSDN Cold Rooster Consulting example.
225 | ///
226 | private static class User32
227 | {
228 | ///
229 | /// Provides access to function required to delete handle. This method is used internally
230 | /// and is not required to be called separately.
231 | ///
232 | /// Pointer to icon handle.
233 | /// N/A
234 | [DllImport("User32.dll")]
235 | public static extern int DestroyIcon(IntPtr hIcon);
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Plugin.cs:
--------------------------------------------------------------------------------
1 | // Copyright © 2024 ema
2 | //
3 | // This file is part of QuickLook program.
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with this program. If not, see .
17 |
18 | using QuickLook.Common.Plugin;
19 | using QuickLook.Plugin.TorrentViewer.Data;
20 | using System;
21 | using System.Diagnostics;
22 | using System.IO;
23 | using System.Linq;
24 | using System.Windows;
25 |
26 | namespace QuickLook.Plugin.TorrentViewer;
27 |
28 | public class Plugin : IViewer
29 | {
30 | private ArchiveFileListView? _tvp;
31 | private string? _path;
32 |
33 | public int Priority => 0;
34 |
35 | public void Init()
36 | {
37 | }
38 |
39 | public bool CanHandle(string path)
40 | {
41 | if (File.Exists(path) && Path.GetExtension(path).Equals(".torrent", StringComparison.OrdinalIgnoreCase))
42 | {
43 | return true;
44 | }
45 | return false;
46 | }
47 |
48 | public void Prepare(string path, ContextObject context)
49 | {
50 | context.PreferredSize = new Size { Width = 840, Height = 600 };
51 | }
52 |
53 | public void View(string path, ContextObject context)
54 | {
55 | _path = path;
56 | _tvp = new ArchiveFileListView();
57 |
58 | Metainfo metainfo = LoadTorrent();
59 | TorrentFiles torrent = new()
60 | {
61 | Name = metainfo.Name,
62 | InfoHash = metainfo.InfoHash.Value.Select(v => $"{v:x2}").Join(),
63 | Files = metainfo.Files
64 | .Select(f => new TorrentFile()
65 | {
66 | Name = f.Name,
67 | Size = f.Size,
68 | }),
69 | Trackers = metainfo.Trackers.SelectMany(t => t).Select(u => u.ToString()),
70 | };
71 |
72 | torrent.Magnet = Torrent2Magnet.GenerateMagnetLink(torrent.InfoHash, torrent.Name, torrent.Trackers.ToArray());
73 |
74 | _tvp.SetDataContext(torrent);
75 | _tvp.Tag = context;
76 |
77 | context.ViewerContent = _tvp;
78 | context.Title = $"{Path.GetFileName(path)}";
79 | context.IsBusy = false;
80 | }
81 |
82 | public void Cleanup()
83 | {
84 | GC.SuppressFinalize(this);
85 |
86 | _tvp = null;
87 | }
88 |
89 | public Metainfo LoadTorrent()
90 | {
91 | if (!File.Exists(_path))
92 | {
93 | return null!;
94 | }
95 |
96 | try
97 | {
98 | byte[] buffer = File.ReadAllBytes(_path);
99 | using MemoryStream stream = new(buffer);
100 | Metainfo metainfo = TorrentParser.ReadFromStream(stream);
101 | return metainfo;
102 | }
103 | catch (Exception e)
104 | {
105 | Debug.WriteLine("Failed to load torrent file.");
106 | Debug.WriteLine(e);
107 | }
108 |
109 | return null!;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/QuickLook.Plugin.Metadata.config:
--------------------------------------------------------------------------------
1 |
2 |
3 | QuickLook.Plugin.TorrentViewer
4 | 1.0.3
5 | View the Torrent files.
6 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/QuickLook.Plugin.TorrentViewer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Library
5 | net462
6 | QuickLook.Plugin.TorrentViewer
7 | QuickLook.Plugin.TorrentViewer
8 | 512
9 | false
10 | true
11 | latest
12 | enable
13 | false
14 | false
15 | {A5B1DCF7-5C77-430A-A72F-326A3CE50FF4}
16 | MinimumRecommendedRules.ruleset
17 | $(NoWarn);CS8600;CS8601;CS8602;CS8603;CS8604;CS8618;CS8625
18 |
19 |
20 |
21 | true
22 | full
23 | false
24 | ..\..\QuickLook\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.TorrentViewer\
25 | DEBUG;TRACE
26 | prompt
27 |
28 |
29 |
30 | pdbonly
31 | true
32 | ..\Build\Release\
33 | TRACE
34 | prompt
35 |
36 |
37 |
38 | true
39 | full
40 | ..\..\QuickLook\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.TorrentViewer\
41 | DEBUG;TRACE
42 | x86
43 | prompt
44 |
45 |
46 |
47 | ..\Build\Release\
48 | TRACE
49 | true
50 | pdbonly
51 | x86
52 | prompt
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}
64 | QuickLook.Common
65 | False
66 |
67 |
68 |
69 |
70 |
71 |
72 | Always
73 |
74 |
75 |
76 |
77 |
78 | PreserveNewest
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Torrent2Magnet.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Net;
4 |
5 | namespace QuickLook.Plugin.TorrentViewer;
6 |
7 | public static class Torrent2Magnet
8 | {
9 | public static string GenerateMagnetLink(string infoHash, string fileName, string[] trackerUrls)
10 | {
11 | string encodedFileName = WebUtility.UrlEncode(fileName);
12 | string trackerUrl = trackerUrls.Select(tr => WebUtility.UrlEncode(tr)).Join("&tr=");
13 | string magnetLink = $"magnet:?xt=urn:btih:{infoHash}&dn={encodedFileName}&tr={trackerUrl}";
14 |
15 | return magnetLink;
16 | }
17 |
18 | public static string Join(this IEnumerable values, string? separator = null)
19 | {
20 | return string.Join(separator ?? string.Empty, values);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/TorrentFile.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace QuickLook.Plugin.TorrentViewer;
4 |
5 | public sealed class TorrentFile
6 | {
7 | public string? Name { get; set; }
8 | public long Size { get; set; }
9 | }
10 |
11 | public sealed class TorrentFiles
12 | {
13 | public string? Name { get; set; }
14 | public string? InfoHash { get; set; }
15 | public string? Magnet { get; set; }
16 | public IEnumerable Files { get; set; } = [];
17 | public IEnumerable Trackers { get; set; } = [];
18 | }
19 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/TorrentParser.cs:
--------------------------------------------------------------------------------
1 | // This file is part of TorrentCore.
2 | // https://torrentcore.org
3 | // Copyright (c) Samuel Fisher.
4 | //
5 | // Licensed under the GNU Lesser General Public License, version 3. See the
6 | // LICENSE file in the project root for full license information.
7 |
8 | using QuickLook.Plugin.TorrentViewer.Bencode.Parsing;
9 | using QuickLook.Plugin.TorrentViewer.Bencode.Torrents;
10 | using QuickLook.Plugin.TorrentViewer.Data;
11 | using System;
12 | using System.Collections.Generic;
13 | using System.IO;
14 | using System.Linq;
15 |
16 | namespace QuickLook.Plugin.TorrentViewer;
17 |
18 | ///
19 | /// Reads .torrent files.
20 | ///
21 | internal static class TorrentParser
22 | {
23 | ///
24 | /// Loads the specified Torrent file.
25 | ///
26 | /// Input stream to read.
27 | /// Metainfo data.
28 | public static Metainfo ReadFromStream(Stream input)
29 | {
30 | var parser = new BencodeParser();
31 | var torrent = parser.Parse(input);
32 |
33 | var files = new List();
34 | if (torrent.File != null)
35 | {
36 | // Single file
37 | files.Add(new ContainedFile(torrent.File.FileName, torrent.File.FileSize));
38 | }
39 | else
40 | {
41 | // Multiple files
42 | files.AddRange(torrent.Files.Select(x => new ContainedFile(x.FullPath, x.FileSize)));
43 | }
44 |
45 | // Construct pieces
46 | var pieces = new List();
47 | byte[] pieceHashes = torrent.Pieces;
48 | int pieceIndex = 0;
49 | for (long offset = 0; offset + torrent.PieceSize <= torrent.TotalSize; offset += torrent.PieceSize)
50 | {
51 | int length = (int)Math.Min(torrent.PieceSize, torrent.TotalSize - offset);
52 | byte[] hash = new byte[Sha1Hash.Length];
53 | Array.Copy(pieceHashes, pieceIndex * Sha1Hash.Length, hash, 0, Sha1Hash.Length);
54 | Piece piece = new(pieceIndex, length, new Sha1Hash(hash));
55 | pieces.Add(piece);
56 | pieceIndex++;
57 | }
58 |
59 | var metaInfo = new Metainfo(torrent.DisplayName,
60 | new Sha1Hash(torrent.OriginalInfoHashBytes),
61 | files,
62 | pieces,
63 | torrent.Trackers.Select(x => x.Select(y => new Uri(y))),
64 | []);
65 |
66 | return metaInfo;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/QuickLook.Plugin.TorrentViewer/Translations.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Total {0} file(s)
6 | Total size: {0}
7 |
8 |
9 | 共 {0} 个文件
10 | 总大小:{0}
11 |
12 |
13 | 共 {0} 個文件
14 | 總大小:{0}
15 |
16 |
17 | 合計 {0} ファイル
18 | 合計サイズ:{0}
19 |
20 |
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # QuickLook.Plugin.TorrentViewer
4 |
5 | This plugin allows [QuickLook](https://github.com/QL-Win/QuickLook) to preview Torrent file formats.
6 |
7 | ## Supported extensions
8 |
9 | The following file extensions are treated as Torrent files.
10 |
11 | - torrent
12 |
13 | ## Thanks to
14 |
15 | - https://github.com/SamuelFisher/torrentcore
16 | - https://github.com/Krusen/BencodeNET
17 |
18 | ## Licenses
19 |
20 | 
21 |
22 | This project references many other open-source projects. See [here](https://github.com/QL-Win/QuickLook/wiki/On-the-Shoulders-of-Giants) for the full list.
23 |
24 | All source codes are licensed under [GPL-3.0](https://opensource.org/licenses/GPL-3.0).
25 |
26 | If you want to make any modification on these source codes while keeping new codes not protected by GPL-3.0, please contact me for a sublicense instead.
27 |
--------------------------------------------------------------------------------
/Scripts/pack-zip.cmd:
--------------------------------------------------------------------------------
1 | powershell.exe -NoProfile -ExecutionPolicy Bypass -File pack-zip.ps1
2 | @pause
3 |
--------------------------------------------------------------------------------
/Scripts/pack-zip.ps1:
--------------------------------------------------------------------------------
1 | Remove-Item ..\QuickLook.Plugin.TorrentViewer.qlplugin -ErrorAction SilentlyContinue
2 |
3 | $files = Get-ChildItem -Path ..\Build\Release\ -Exclude *.pdb,*.xml
4 | Compress-Archive $files ..\QuickLook.Plugin.TorrentViewer.zip
5 | Move-Item ..\QuickLook.Plugin.TorrentViewer.zip ..\QuickLook.Plugin.TorrentViewer.qlplugin
--------------------------------------------------------------------------------
/Scripts/update-version.ps1:
--------------------------------------------------------------------------------
1 | $tag = git describe --always --tags "--abbrev=0"
2 | $revision = git describe --always --tags
3 |
4 | $text = @"
5 | // This file is generated by update-version.ps1
6 |
7 | using System.Reflection;
8 |
9 | [assembly: AssemblyVersion("$tag")]
10 | [assembly: AssemblyInformationalVersion("$revision")]
11 | "@
12 |
13 | $text | Out-File $PSScriptRoot\..\GitVersion.cs -Encoding utf8
14 |
15 |
16 | $xml = [xml](Get-Content $PSScriptRoot\..\QuickLook.Plugin.Metadata.Base.config)
17 | $xml.Metadata.Version="$revision"
18 | $xml.Save("$PSScriptRoot\..\QuickLook.Plugin.Metadata.config")
--------------------------------------------------------------------------------
/Settings.XamlStyler:
--------------------------------------------------------------------------------
1 | {
2 | "AttributesTolerance": 2,
3 | "KeepFirstAttributeOnSameLine": true,
4 | "MaxAttributeCharactersPerLine": 0,
5 | "MaxAttributesPerLine": 1,
6 | "NewlineExemptionElements": "RadialGradientBrush, GradientStop, LinearGradientBrush, ScaleTransform, SkewTransform, RotateTransform, TranslateTransform, Trigger, Condition, Setter",
7 | "SeparateByGroups": false,
8 | "AttributeIndentation": 0,
9 | "AttributeIndentationStyle": 1,
10 | "RemoveDesignTimeReferences": false,
11 | "IgnoreDesignTimeReferencePrefix": false,
12 | "EnableAttributeReordering": true,
13 | "AttributeOrderingRuleGroups": [
14 | "x:Class",
15 | "xmlns, xmlns:x",
16 | "xmlns:*",
17 | "x:Key, Key, x:Name, Name, x:Uid, Uid, Title",
18 | "Grid.Row, Grid.RowSpan, Grid.Column, Grid.ColumnSpan, Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom",
19 | "Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight",
20 | "Margin, Padding, HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Panel.ZIndex",
21 | "*:*, *",
22 | "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint",
23 | "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText",
24 | "Storyboard.*, From, To, Duration"
25 | ],
26 | "FirstLineAttributes": "",
27 | "OrderAttributesByName": true,
28 | "PutEndingBracketOnNewLine": false,
29 | "RemoveEndingTagOfEmptyElement": true,
30 | "SpaceBeforeClosingSlash": true,
31 | "RootElementLineBreakRule": 0,
32 | "ReorderVSM": 2,
33 | "ReorderGridChildren": false,
34 | "ReorderCanvasChildren": false,
35 | "ReorderSetters": 0,
36 | "FormatMarkupExtension": true,
37 | "NoNewLineMarkupExtensions": "x:Bind, Binding",
38 | "ThicknessSeparator": 2,
39 | "ThicknessAttributes": "Margin, Padding, BorderThickness, ThumbnailClipMargin",
40 | "FormatOnSave": true,
41 | "CommentPadding": 2
42 | }
--------------------------------------------------------------------------------