diff --git a/BencodeNET/Torrents/Torrent.cs b/BencodeNET/Torrents/Torrent.cs
index eead9c5afe..2f49b4f8f9 100644
--- a/BencodeNET/Torrents/Torrent.cs
+++ b/BencodeNET/Torrents/Torrent.cs
@@ -9,6 +9,7 @@
using System.Threading.Tasks;
using BencodeNET.Exceptions;
using BencodeNET.Objects;
+using BencodeNET.Torrents.Validation;
namespace BencodeNET.Torrents
{
@@ -17,6 +18,11 @@ namespace BencodeNET.Torrents
///
public class Torrent : BObject
{
+ ///
+ /// Number of bytes a piece has
+ ///
+ private const int PIECE_NUMBER_OF_BYTES = 20;
+
///
///
///
@@ -173,20 +179,37 @@ 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 += PIECE_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;
+ }
+ }
///
/// 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)
@@ -195,14 +218,14 @@ public virtual string PiecesAsHexString
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);
}
- Pieces = bytes;
+ PiecesConcatenated = bytes;
}
}
@@ -232,10 +255,23 @@ 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;
+ ///
+ /// Verify integrity of the torrent content versus existing data
+ ///
+ /// 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, ValidationOptions options = null)
+ {
+ var validator = new Validator(this, options);
+ return await validator.ValidateExistingDataAsync(path);
+ }
+
///
/// Converts the torrent to a .
///
@@ -284,10 +320,10 @@ protected virtual BDictionary CreateInfoDictionary(Encoding encoding)
var info = new BDictionary();
if (PieceSize > 0)
- info[TorrentInfoFields.PieceLength] = (BNumber) PieceSize;
+ 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),
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..0c7492f470
--- /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;
+ }
+ }
+}