From 986b7acbd6025733839081c284519adeda21bd07 Mon Sep 17 00:00:00 2001 From: djon2003 Date: Thu, 10 Dec 2020 14:46:04 -0500 Subject: [PATCH 1/8] Rename Pieces to PiecesConcatenated and PiecesAsHexString to PiecesConcatenatedAsHexString --- BencodeNET/Torrents/Torrent.cs | 20 ++++++++++---------- BencodeNET/Torrents/TorrentParser.cs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs index eead9c5afe..54e4067e8f 100644 --- a/BencodeNET/Torrents/Torrent.cs +++ b/BencodeNET/Torrents/Torrent.cs @@ -176,17 +176,17 @@ public virtual string DisplayNameUtf8 /// /// A concatenation of all 20-byte SHA1 hash values (one for each piece). - /// Use to get/set this value as a hex string instead. + /// Use to get/set this value as a hex string instead. /// - public virtual byte[] Pieces { get; set; } = new byte[0]; + public virtual byte[] PiecesConcatenated { get; set; } = new byte[0]; /// - /// Gets or sets from/to a hex string (without dashes), e.g. 1C115D26444AEF2A5E936133DCF8789A552BBE9F[...]. + /// Gets or sets from/to a hex string (without dashes), e.g. 1C115D26444AEF2A5E936133DCF8789A552BBE9F[...]. /// The length of the string must be a multiple of 40. /// - public virtual string PiecesAsHexString + public virtual string PiecesConcatenatedAsHexString { - get => BitConverter.ToString(Pieces).Replace("-", ""); + get => BitConverter.ToString(PiecesConcatenated).Replace("-", ""); set { if (value?.Length % 40 != 0) @@ -202,7 +202,7 @@ public virtual string PiecesAsHexString bytes[i] = Convert.ToByte(str, 16); } - Pieces = bytes; + PiecesConcatenated = bytes; } } @@ -232,8 +232,8 @@ public virtual long TotalSize /// /// The total number of file pieces. /// - public virtual int NumberOfPieces => Pieces != null - ? (int) Math.Ceiling((double) Pieces.Length / 20) + public virtual int NumberOfPieces => PiecesConcatenated != null + ? (int) Math.Ceiling((double) PiecesConcatenated.Length / 20) : 0; /// @@ -286,8 +286,8 @@ protected virtual BDictionary CreateInfoDictionary(Encoding encoding) if (PieceSize > 0) info[TorrentInfoFields.PieceLength] = (BNumber) PieceSize; - if (Pieces?.Length > 0) - info[TorrentInfoFields.Pieces] = new BString(Pieces, encoding); + if (PiecesConcatenated?.Length > 0) + info[TorrentInfoFields.Pieces] = new BString(PiecesConcatenated, encoding); if (IsPrivate) info[TorrentInfoFields.Private] = (BNumber)1; diff --git a/BencodeNET/Torrents/TorrentParser.cs b/BencodeNET/Torrents/TorrentParser.cs index 2fde44558c..fde88df3e0 100644 --- a/BencodeNET/Torrents/TorrentParser.cs +++ b/BencodeNET/Torrents/TorrentParser.cs @@ -112,7 +112,7 @@ protected Torrent CreateTorrent(BDictionary data) { IsPrivate = info.Get(TorrentInfoFields.Private) == 1, PieceSize = info.Get(TorrentInfoFields.PieceLength), - Pieces = info.Get(TorrentInfoFields.Pieces)?.Value.ToArray() ?? new byte[0], + PiecesConcatenated = info.Get(TorrentInfoFields.Pieces)?.Value.ToArray() ?? new byte[0], Comment = data.Get(TorrentFields.Comment)?.ToString(encoding), CreatedBy = data.Get(TorrentFields.CreatedBy)?.ToString(encoding), From 39e2b2464ce3b9d5651d0f39e7915408233444e0 Mon Sep 17 00:00:00 2001 From: djon2003 Date: Thu, 10 Dec 2020 14:46:14 -0500 Subject: [PATCH 2/8] Add Pieces property --- BencodeNET/Torrents/Torrent.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs index 54e4067e8f..5ff4b7dd25 100644 --- a/BencodeNET/Torrents/Torrent.cs +++ b/BencodeNET/Torrents/Torrent.cs @@ -17,6 +17,8 @@ namespace BencodeNET.Torrents /// public class Torrent : BObject { + private const int SHA1_NUMBER_OF_BYTES = 20; + /// /// /// @@ -173,6 +175,23 @@ public virtual string DisplayNameUtf8 public virtual long PieceSize { get; set; } // TODO: Split into list of 20-byte hashes and rename to something appropriate? + /// + /// A list of all 20-byte SHA1 hash values (one for each piece). + /// + public List Pieces + { + get + { + var pieces = new List(); + for (int i = 0; i < PiecesConcatenated.Length; i += SHA1_NUMBER_OF_BYTES) + { + var piece = new byte[SHA1_NUMBER_OF_BYTES]; + Array.Copy(PiecesConcatenated, i, piece, 0, SHA1_NUMBER_OF_BYTES); + pieces.Add(piece); + } + return pieces; + } + } /// /// A concatenation of all 20-byte SHA1 hash values (one for each piece). From f3fcdb3d4e54fff12624d53b558ee50ff8243167 Mon Sep 17 00:00:00 2001 From: djon2003 Date: Thu, 10 Dec 2020 14:50:47 -0500 Subject: [PATCH 3/8] Add validation of existing files versus the torrent --- BencodeNET/Torrents/Torrent.cs | 124 +++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs index 5ff4b7dd25..ef5c5a7b8e 100644 --- a/BencodeNET/Torrents/Torrent.cs +++ b/BencodeNET/Torrents/Torrent.cs @@ -3,6 +3,7 @@ using System.IO; using System.IO.Pipelines; using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -17,7 +18,24 @@ namespace BencodeNET.Torrents /// public class Torrent : BObject { + private struct FileValidation + { + public bool isValid; + public int remainder; + public byte[] buffer; + public bool validateRemainder; + + public FileValidation(long bufferSize, bool validateReminder) + { + isValid = false; + remainder = 0; + buffer = new byte[bufferSize]; + this.validateRemainder = validateReminder; + } + } + private const int SHA1_NUMBER_OF_BYTES = 20; + private HashAlgorithm sha1 = SHA1.Create(); /// /// @@ -255,6 +273,112 @@ public virtual long TotalSize ? (int) Math.Ceiling((double) PiecesConcatenated.Length / 20) : 0; + /// + /// Verify integrity of the torrent content versus existing data + /// + /// either a folder path in multi mode or a file path in single mode + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] + public async virtual Task ValidateExistingDataAsync(string path) + { + var isDirectory = Directory.Exists(path); + var isFile = System.IO.File.Exists(path); + if (isDirectory && FileMode != TorrentFileMode.Multi) + { + throw new BencodeException("The path represents a directory but the torrent is not set as a multi mode"); + } + else if (isFile && FileMode != TorrentFileMode.Single) + { + throw new BencodeException("The path represents a file but the torrent is not set as a single mode"); + } + else if (!isFile && !isDirectory) + { + throw new BencodeException("The path does not exist"); + } + + var validation = new FileValidation(PieceSize, false); + if (isFile) + { + validation = await ValidateExistingFileAsync(new System.IO.FileInfo(path)); + } + else if (isDirectory) + { + validation.isValid = true; + var piecesOffset = 0; + for (int i = 0; i < Files.Count && validation.isValid; i++) + { + validation.validateRemainder = (i + 1) == Files.Count; + var file = new FileInfo(Path.Combine(path, Files.DirectoryName, Files[i].FullPath)); + validation = await ValidateExistingFileAsync(file, piecesOffset, validation); + if (!validation.isValid) + { + break; + } + + piecesOffset += (file.Exists ? (int)(file.Length / PieceSize) : 0); + } + } + + return validation.isValid; + } + + /// + /// Validate integrity of an existing file + /// + /// file to validate + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] + private async Task ValidateExistingFileAsync(FileInfo file) + { + return await ValidateExistingFileAsync(file, 0, new FileValidation(PieceSize, true)); + } + + /// + /// Validate integrity of an existing file + /// + /// file to validate + /// next piece index to validate + /// current validation data + /// Based on https://raw.githubusercontent.com/eclipse/ecf/master/protocols/bundles/org.eclipse.ecf.protocol.bittorrent/src/org/eclipse/ecf/protocol/bittorrent/TorrentFile.java + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] + private async Task ValidateExistingFileAsync(FileInfo file, int piecesOffset, FileValidation validation) + { + if (!file.Exists) + { + return validation; + } + + int piecesIndex = piecesOffset, bytesRead = validation.remainder; + using (var stream = file.OpenRead()) + { + while ((bytesRead += await stream.ReadAsync(validation.buffer, validation.remainder, (int)PieceSize - validation.remainder)) == PieceSize) + { + if (!Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(validation.buffer))) + { + return validation; + } + piecesIndex++; + bytesRead = 0; + validation.remainder = 0; + } + } + + validation.remainder = bytesRead; + if (!validation.validateRemainder || validation.remainder == 0) + { + validation.isValid = true; + return validation; + } + + byte[] lastBuffer = new byte[validation.remainder]; + Array.Copy(validation.buffer, lastBuffer, bytesRead); + + validation.isValid = Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(lastBuffer)); + + return validation; + } + /// /// Converts the torrent to a . /// From 81c40b8be20e49c5d95e184665e27269dd4981b7 Mon Sep 17 00:00:00 2001 From: djon2003 Date: Tue, 15 Dec 2020 00:16:52 -0500 Subject: [PATCH 4/8] Different format --- BencodeNET/Torrents/Torrent.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs index ef5c5a7b8e..ced346c5cb 100644 --- a/BencodeNET/Torrents/Torrent.cs +++ b/BencodeNET/Torrents/Torrent.cs @@ -232,10 +232,10 @@ public virtual string PiecesConcatenatedAsHexString if (Regex.IsMatch(value, "[^0-9A-F]")) throw new ArgumentException("Value must only contain hex characters (0-9 and A-F) and only uppercase."); - var bytes = new byte[value.Length/2]; + var bytes = new byte[value.Length / 2]; for (var i = 0; i < bytes.Length; i++) { - var str = $"{value[i*2]}{value[i*2+1]}"; + var str = $"{value[i * 2]}{value[i * 2 + 1]}"; bytes[i] = Convert.ToByte(str, 16); } @@ -270,7 +270,7 @@ public virtual long TotalSize /// The total number of file pieces. /// public virtual int NumberOfPieces => PiecesConcatenated != null - ? (int) Math.Ceiling((double) PiecesConcatenated.Length / 20) + ? (int)Math.Ceiling((double)PiecesConcatenated.Length / 20) : 0; /// @@ -427,7 +427,7 @@ protected virtual BDictionary CreateInfoDictionary(Encoding encoding) var info = new BDictionary(); if (PieceSize > 0) - info[TorrentInfoFields.PieceLength] = (BNumber) PieceSize; + info[TorrentInfoFields.PieceLength] = (BNumber)PieceSize; if (PiecesConcatenated?.Length > 0) info[TorrentInfoFields.Pieces] = new BString(PiecesConcatenated, encoding); From 9965153a533ed82d12db9945db92b8c50a39132c Mon Sep 17 00:00:00 2001 From: djon2003 Date: Tue, 15 Dec 2020 00:19:07 -0500 Subject: [PATCH 5/8] Bug fixes --- BencodeNET/Torrents/Torrent.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs index ced346c5cb..a7cf15d5f6 100644 --- a/BencodeNET/Torrents/Torrent.cs +++ b/BencodeNET/Torrents/Torrent.cs @@ -283,6 +283,11 @@ public async virtual Task ValidateExistingDataAsync(string path) { var isDirectory = Directory.Exists(path); var isFile = System.IO.File.Exists(path); + if (String.IsNullOrEmpty(path)) + { + return false; + } + if (isDirectory && FileMode != TorrentFileMode.Multi) { throw new BencodeException("The path represents a directory but the torrent is not set as a multi mode"); @@ -293,7 +298,7 @@ public async virtual Task ValidateExistingDataAsync(string path) } else if (!isFile && !isDirectory) { - throw new BencodeException("The path does not exist"); + return false; } var validation = new FileValidation(PieceSize, false); @@ -346,6 +351,7 @@ private async Task ValidateExistingFileAsync(FileInfo file, int { if (!file.Exists) { + validation.isValid = false; return validation; } @@ -356,6 +362,7 @@ private async Task ValidateExistingFileAsync(FileInfo file, int { if (!Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(validation.buffer))) { + validation.isValid = false; return validation; } piecesIndex++; From 00e3e5d64ea0d91829a12c5c24b481c2e4731f3a Mon Sep 17 00:00:00 2001 From: djon2003 Date: Mon, 21 Dec 2020 14:20:59 -0500 Subject: [PATCH 6/8] Add partial validation support --- BencodeNET/Torrents/Torrent.cs | 41 ++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs index a7cf15d5f6..c37d18b3da 100644 --- a/BencodeNET/Torrents/Torrent.cs +++ b/BencodeNET/Torrents/Torrent.cs @@ -21,16 +21,20 @@ public class Torrent : BObject private struct FileValidation { public bool isValid; - public int remainder; + public int piecesValidated; + public long remainder; public byte[] buffer; public bool validateRemainder; + public double tolerance; - public FileValidation(long bufferSize, bool validateReminder) + public FileValidation(long bufferSize, bool validateReminder, double tolerance) { + piecesValidated = 0; isValid = false; remainder = 0; buffer = new byte[bufferSize]; this.validateRemainder = validateReminder; + this.tolerance = tolerance; } } @@ -277,10 +281,16 @@ public virtual long TotalSize /// Verify integrity of the torrent content versus existing data /// /// either a folder path in multi mode or a file path in single mode + /// percentage of torrent so it is concidered valid (>=1 = 100%, 0.95 = 95%). Only valid in MultiFile mode. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] - public async virtual Task ValidateExistingDataAsync(string path) + public async virtual Task ValidateExistingDataAsync(string path, double tolerance = 1) { + if (tolerance > 1) + { + tolerance = 1; + } + var isDirectory = Directory.Exists(path); var isFile = System.IO.File.Exists(path); if (String.IsNullOrEmpty(path)) @@ -301,7 +311,7 @@ public async virtual Task ValidateExistingDataAsync(string path) return false; } - var validation = new FileValidation(PieceSize, false); + var validation = new FileValidation(PieceSize, false, tolerance); if (isFile) { validation = await ValidateExistingFileAsync(new System.IO.FileInfo(path)); @@ -310,21 +320,23 @@ public async virtual Task ValidateExistingDataAsync(string path) { validation.isValid = true; var piecesOffset = 0; - for (int i = 0; i < Files.Count && validation.isValid; i++) + for (int i = 0; i < Files.Count && piecesOffset < NumberOfPieces; i++) { + var previousRemainder = validation.remainder; validation.validateRemainder = (i + 1) == Files.Count; var file = new FileInfo(Path.Combine(path, Files.DirectoryName, Files[i].FullPath)); validation = await ValidateExistingFileAsync(file, piecesOffset, validation); - if (!validation.isValid) + if (!validation.isValid && tolerance == 1) { break; } - piecesOffset += (file.Exists ? (int)(file.Length / PieceSize) : 0); + validation.remainder = (Files[i].FileSize + previousRemainder) % PieceSize; // Set again the remainder in case the file was not existing or partially good + piecesOffset += (int)((Files[i].FileSize + previousRemainder) / PieceSize); } } - return validation.isValid; + return ((double)validation.piecesValidated / (double)NumberOfPieces) >= tolerance; } /// @@ -335,7 +347,7 @@ public async virtual Task ValidateExistingDataAsync(string path) [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] private async Task ValidateExistingFileAsync(FileInfo file) { - return await ValidateExistingFileAsync(file, 0, new FileValidation(PieceSize, true)); + return await ValidateExistingFileAsync(file, 0, new FileValidation(PieceSize, true, 1)); ; } /// @@ -355,16 +367,20 @@ private async Task ValidateExistingFileAsync(FileInfo file, int return validation; } - int piecesIndex = piecesOffset, bytesRead = validation.remainder; + int piecesIndex = piecesOffset, bytesRead = (int)validation.remainder; using (var stream = file.OpenRead()) { - while ((bytesRead += await stream.ReadAsync(validation.buffer, validation.remainder, (int)PieceSize - validation.remainder)) == PieceSize) + while ((bytesRead += await stream.ReadAsync(validation.buffer, (int)validation.remainder, (int)(PieceSize - validation.remainder))) == PieceSize) { - if (!Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(validation.buffer))) + var isFileTooLarge = piecesIndex >= NumberOfPieces; + var isPieceNotMatching = !isFileTooLarge && !Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(validation.buffer)) && validation.tolerance == 1; + if (isFileTooLarge || isPieceNotMatching) { validation.isValid = false; return validation; } + + validation.piecesValidated++; piecesIndex++; bytesRead = 0; validation.remainder = 0; @@ -382,6 +398,7 @@ private async Task ValidateExistingFileAsync(FileInfo file, int Array.Copy(validation.buffer, lastBuffer, bytesRead); validation.isValid = Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(lastBuffer)); + validation.piecesValidated += (validation.isValid ? 1 : 0); return validation; } From 61ab80c12ff2567aaf2312dfa5b57b114efcd098 Mon Sep 17 00:00:00 2001 From: djon2003 Date: Tue, 22 Dec 2020 16:54:20 -0500 Subject: [PATCH 7/8] Refactor to extract validation in three different classes --- BencodeNET/Torrents/Torrent.cs | 157 ++---------------- .../Torrents/Validation/ValidationData.cs | 24 +++ .../Torrents/Validation/ValidationOptions.cs | 23 +++ BencodeNET/Torrents/Validation/Validator.cs | 155 +++++++++++++++++ 4 files changed, 215 insertions(+), 144 deletions(-) create mode 100644 BencodeNET/Torrents/Validation/ValidationData.cs create mode 100644 BencodeNET/Torrents/Validation/ValidationOptions.cs create mode 100644 BencodeNET/Torrents/Validation/Validator.cs diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs index c37d18b3da..2f49b4f8f9 100644 --- a/BencodeNET/Torrents/Torrent.cs +++ b/BencodeNET/Torrents/Torrent.cs @@ -3,13 +3,13 @@ using System.IO; using System.IO.Pipelines; using System.Linq; -using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BencodeNET.Exceptions; using BencodeNET.Objects; +using BencodeNET.Torrents.Validation; namespace BencodeNET.Torrents { @@ -18,28 +18,10 @@ namespace BencodeNET.Torrents /// public class Torrent : BObject { - private struct FileValidation - { - public bool isValid; - public int piecesValidated; - public long remainder; - public byte[] buffer; - public bool validateRemainder; - public double tolerance; - - public FileValidation(long bufferSize, bool validateReminder, double tolerance) - { - piecesValidated = 0; - isValid = false; - remainder = 0; - buffer = new byte[bufferSize]; - this.validateRemainder = validateReminder; - this.tolerance = tolerance; - } - } - - private const int SHA1_NUMBER_OF_BYTES = 20; - private HashAlgorithm sha1 = SHA1.Create(); + /// + /// Number of bytes a piece has + /// + private const int PIECE_NUMBER_OF_BYTES = 20; /// /// @@ -205,10 +187,10 @@ public List Pieces get { var pieces = new List(); - for (int i = 0; i < PiecesConcatenated.Length; i += SHA1_NUMBER_OF_BYTES) + for (int i = 0; i < PiecesConcatenated.Length; i += PIECE_NUMBER_OF_BYTES) { - var piece = new byte[SHA1_NUMBER_OF_BYTES]; - Array.Copy(PiecesConcatenated, i, piece, 0, SHA1_NUMBER_OF_BYTES); + var piece = new byte[PIECE_NUMBER_OF_BYTES]; + Array.Copy(PiecesConcatenated, i, piece, 0, PIECE_NUMBER_OF_BYTES); pieces.Add(piece); } return pieces; @@ -280,127 +262,14 @@ public virtual long TotalSize /// /// Verify integrity of the torrent content versus existing data /// - /// either a folder path in multi mode or a file path in single mode - /// percentage of torrent so it is concidered valid (>=1 = 100%, 0.95 = 95%). Only valid in MultiFile mode. + /// Either a folder path in multi mode or a file path in single mode + /// Validation options. Null means the dafault options /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] - public async virtual Task ValidateExistingDataAsync(string path, double tolerance = 1) + public async virtual Task ValidateExistingDataAsync(string path, ValidationOptions options = null) { - if (tolerance > 1) - { - tolerance = 1; - } - - var isDirectory = Directory.Exists(path); - var isFile = System.IO.File.Exists(path); - if (String.IsNullOrEmpty(path)) - { - return false; - } - - if (isDirectory && FileMode != TorrentFileMode.Multi) - { - throw new BencodeException("The path represents a directory but the torrent is not set as a multi mode"); - } - else if (isFile && FileMode != TorrentFileMode.Single) - { - throw new BencodeException("The path represents a file but the torrent is not set as a single mode"); - } - else if (!isFile && !isDirectory) - { - return false; - } - - var validation = new FileValidation(PieceSize, false, tolerance); - if (isFile) - { - validation = await ValidateExistingFileAsync(new System.IO.FileInfo(path)); - } - else if (isDirectory) - { - validation.isValid = true; - var piecesOffset = 0; - for (int i = 0; i < Files.Count && piecesOffset < NumberOfPieces; i++) - { - var previousRemainder = validation.remainder; - validation.validateRemainder = (i + 1) == Files.Count; - var file = new FileInfo(Path.Combine(path, Files.DirectoryName, Files[i].FullPath)); - validation = await ValidateExistingFileAsync(file, piecesOffset, validation); - if (!validation.isValid && tolerance == 1) - { - break; - } - - validation.remainder = (Files[i].FileSize + previousRemainder) % PieceSize; // Set again the remainder in case the file was not existing or partially good - piecesOffset += (int)((Files[i].FileSize + previousRemainder) / PieceSize); - } - } - - return ((double)validation.piecesValidated / (double)NumberOfPieces) >= tolerance; - } - - /// - /// Validate integrity of an existing file - /// - /// file to validate - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] - private async Task ValidateExistingFileAsync(FileInfo file) - { - return await ValidateExistingFileAsync(file, 0, new FileValidation(PieceSize, true, 1)); ; - } - - /// - /// Validate integrity of an existing file - /// - /// file to validate - /// next piece index to validate - /// current validation data - /// Based on https://raw.githubusercontent.com/eclipse/ecf/master/protocols/bundles/org.eclipse.ecf.protocol.bittorrent/src/org/eclipse/ecf/protocol/bittorrent/TorrentFile.java - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] - private async Task ValidateExistingFileAsync(FileInfo file, int piecesOffset, FileValidation validation) - { - if (!file.Exists) - { - validation.isValid = false; - return validation; - } - - int piecesIndex = piecesOffset, bytesRead = (int)validation.remainder; - using (var stream = file.OpenRead()) - { - while ((bytesRead += await stream.ReadAsync(validation.buffer, (int)validation.remainder, (int)(PieceSize - validation.remainder))) == PieceSize) - { - var isFileTooLarge = piecesIndex >= NumberOfPieces; - var isPieceNotMatching = !isFileTooLarge && !Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(validation.buffer)) && validation.tolerance == 1; - if (isFileTooLarge || isPieceNotMatching) - { - validation.isValid = false; - return validation; - } - - validation.piecesValidated++; - piecesIndex++; - bytesRead = 0; - validation.remainder = 0; - } - } - - validation.remainder = bytesRead; - if (!validation.validateRemainder || validation.remainder == 0) - { - validation.isValid = true; - return validation; - } - - byte[] lastBuffer = new byte[validation.remainder]; - Array.Copy(validation.buffer, lastBuffer, bytesRead); - - validation.isValid = Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(lastBuffer)); - validation.piecesValidated += (validation.isValid ? 1 : 0); - - return validation; + var validator = new Validator(this, options); + return await validator.ValidateExistingDataAsync(path); } /// diff --git a/BencodeNET/Torrents/Validation/ValidationData.cs b/BencodeNET/Torrents/Validation/ValidationData.cs new file mode 100644 index 0000000000..30a8edcd2d --- /dev/null +++ b/BencodeNET/Torrents/Validation/ValidationData.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BencodeNET.Torrents.Validation +{ + class ValidationData + { + public bool isValid; + public int piecesValidated; + public long remainder; + public byte[] buffer; + public bool validateRemainder; + + public ValidationData(long bufferSize, bool validateReminder) + { + piecesValidated = 0; + isValid = false; + remainder = 0; + buffer = new byte[bufferSize]; + this.validateRemainder = validateReminder; + } + } +} diff --git a/BencodeNET/Torrents/Validation/ValidationOptions.cs b/BencodeNET/Torrents/Validation/ValidationOptions.cs new file mode 100644 index 0000000000..1148920484 --- /dev/null +++ b/BencodeNET/Torrents/Validation/ValidationOptions.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BencodeNET.Torrents.Validation +{ + /// + /// Options for torrent file(s) validation + /// + public class ValidationOptions + { + /// + /// What percentage validated is considered as valid torrent existing data + /// + /// >=1 = 100%, 0.95 = 95%. Only valid with torrent in MultiFile mode. + public double Tolerance { get; set; } = 1; + + /// + /// + /// + public static readonly ValidationOptions DefaultOptions = new ValidationOptions(); + } +} diff --git a/BencodeNET/Torrents/Validation/Validator.cs b/BencodeNET/Torrents/Validation/Validator.cs new file mode 100644 index 0000000000..6199412af8 --- /dev/null +++ b/BencodeNET/Torrents/Validation/Validator.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; +using System.Linq; + +namespace BencodeNET.Torrents.Validation +{ + class Validator + { + private readonly Torrent torrent; + private readonly ValidationOptions options; + private readonly HashAlgorithm sha1 = SHA1.Create(); + + // Shorthand helpers + private TorrentFileMode FileMode => torrent.FileMode; + private MultiFileInfoList Files => torrent.Files; + private long PieceSize => torrent.PieceSize; + private int NumberOfPieces => torrent.NumberOfPieces; + private List Pieces => torrent.Pieces; + + public Validator(Torrent torrent, ValidationOptions options) + { + this.options = options ?? ValidationOptions.DefaultOptions; + this.torrent = torrent; + + // Ensure options are appropriate toward the current torrent + if (this.torrent.FileMode == TorrentFileMode.Single || this.options.Tolerance > 1) + { + this.options.Tolerance = 1; + } + } + + /// + /// Verify integrity of the torrent content versus existing data + /// + /// either a folder path in multi mode or a file path in single mode + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] + public async virtual Task ValidateExistingDataAsync(string path) + { + var isDirectory = Directory.Exists(path); + var isFile = System.IO.File.Exists(path); + if (String.IsNullOrEmpty(path)) + { + return false; + } + + if (isDirectory && FileMode != TorrentFileMode.Multi) + { + throw new ArgumentException("The path represents a directory but the torrent is not set as a multi mode"); + } + else if (isFile && FileMode != TorrentFileMode.Single) + { + throw new ArgumentException("The path represents a file but the torrent is not set as a single mode"); + } + else if (!isFile && !isDirectory) + { + return false; + } + + var validation = new ValidationData(PieceSize, false); + if (isFile) + { + validation = await ValidateExistingFileAsync(new System.IO.FileInfo(path)); + } + else if (isDirectory) + { + validation.isValid = true; + var piecesOffset = 0; + for (int i = 0; i < Files.Count && piecesOffset < NumberOfPieces; i++) + { + var previousRemainder = validation.remainder; + validation.validateRemainder = (i + 1) == Files.Count; + var file = new FileInfo(Path.Combine(path, Files.DirectoryName, Files[i].FullPath)); + validation = await ValidateExistingFileAsync(file, piecesOffset, validation); + if (!validation.isValid && options.Tolerance == 1) + { + break; + } + + validation.remainder = (Files[i].FileSize + previousRemainder) % PieceSize; // Set again the remainder in case the file was not existing or partially good + piecesOffset += (int)((Files[i].FileSize + previousRemainder) / PieceSize); + } + } + + return ((double)validation.piecesValidated / (double)NumberOfPieces) >= options.Tolerance; + } + + /// + /// Validate integrity of an existing file + /// + /// file to validate + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] + private async Task ValidateExistingFileAsync(FileInfo file) + { + return await ValidateExistingFileAsync(file, 0, new ValidationData(PieceSize, true)); ; + } + + /// + /// Validate integrity of an existing file + /// + /// file to validate + /// next piece index to validate + /// current validation data + /// Based on https://raw.githubusercontent.com/eclipse/ecf/master/protocols/bundles/org.eclipse.ecf.protocol.bittorrent/src/org/eclipse/ecf/protocol/bittorrent/TorrentFile.java + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] + private async Task ValidateExistingFileAsync(FileInfo file, int piecesOffset, ValidationData validation) + { + if (!file.Exists) + { + validation.isValid = false; + return validation; + } + + int piecesIndex = piecesOffset, bytesRead = (int)validation.remainder; + using (var stream = file.OpenRead()) + { + while ((bytesRead += await stream.ReadAsync(validation.buffer, (int)validation.remainder, (int)(PieceSize - validation.remainder))) == PieceSize) + { + var isFileTooLarge = piecesIndex >= NumberOfPieces; + var isPieceNotMatching = !isFileTooLarge && !Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(validation.buffer)) && options.Tolerance == 1; + if (isFileTooLarge || isPieceNotMatching) + { + validation.isValid = false; + return validation; + } + + validation.piecesValidated++; + piecesIndex++; + bytesRead = 0; + validation.remainder = 0; + } + } + + validation.remainder = bytesRead; + if (!validation.validateRemainder || validation.remainder == 0) + { + validation.isValid = true; + return validation; + } + + byte[] lastBuffer = new byte[validation.remainder]; + Array.Copy(validation.buffer, lastBuffer, bytesRead); + + validation.isValid = Pieces[piecesIndex].SequenceEqual(sha1.ComputeHash(lastBuffer)); + validation.piecesValidated += (validation.isValid ? 1 : 0); + + return validation; + } + } +} From 9293d18d02d3fed85751e55215951b5f6de97d46 Mon Sep 17 00:00:00 2001 From: djon2003 Date: Tue, 22 Dec 2020 16:57:13 -0500 Subject: [PATCH 8/8] Fix small CodeFactor issue --- BencodeNET/Torrents/Validation/Validator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BencodeNET/Torrents/Validation/Validator.cs b/BencodeNET/Torrents/Validation/Validator.cs index 6199412af8..0c7492f470 100644 --- a/BencodeNET/Torrents/Validation/Validator.cs +++ b/BencodeNET/Torrents/Validation/Validator.cs @@ -96,7 +96,7 @@ public async virtual Task ValidateExistingDataAsync(string path) [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD111:Use ConfigureAwait(bool)", Justification = "")] private async Task ValidateExistingFileAsync(FileInfo file) { - return await ValidateExistingFileAsync(file, 0, new ValidationData(PieceSize, true)); ; + return await ValidateExistingFileAsync(file, 0, new ValidationData(PieceSize, true)); } ///