diff --git a/src/MonoTorrent.Client/MonoTorrent/Torrent.cs b/src/MonoTorrent.Client/MonoTorrent/Torrent.cs index aee787fa8..363b78c78 100644 --- a/src/MonoTorrent.Client/MonoTorrent/Torrent.cs +++ b/src/MonoTorrent.Client/MonoTorrent/Torrent.cs @@ -484,8 +484,26 @@ void ProcessInfo (BEncodedDictionary dictionary, ref PieceHashesV1? hashesV1, re if (v1File.Length != v2File.Length) throw new TorrentException ("Inconsistent hybrid torrent, file length mismatch."); - if (v1File.Padding != v2File.Padding) - throw new TorrentException ("Inconsistent hybrid torrent, file padding mismatch."); + if (v1File.Padding != v2File.Padding) { + // BEP47 says padding is there so the *subsequent* file aligns with a piece start boundary. + // By a literal reading, and in line with the rest of the bittorrent spec, the last file + // can and should be considered the 'end' of the torrent (obviously :p) and so does not + // have a subsequent file, and so does not need padding. Similar to how blocks are requested + // in 16kB chunks, except for the final block which is just wahtever bytes are left over. + // + // Requested a clarification on the BEP. However both variants will need to be supported + // regardless of what the spec says because both are in the wild. + // Issue: https://github.com/bittorrent/bittorrent.org/issues/160 + // + // If padding is mandatory for the last file, then remove the code which strips it out + // inside 'LoadTorrentFilesV2'. + if (v1File == v1Files.Last () && v2File == v2Files.Last ()) { + var mutableFiles = v2Files.ToList (); + mutableFiles[v2Files.Count - 1] = new TorrentFile (v2File.Path, v2File.Length, v2File.StartPieceIndex, v2File.EndPieceIndex, v2File.OffsetInTorrent, v2File.PiecesRoot, TorrentFileAttributes.None, v1File.Padding); + v2Files = Array.AsReadOnly (mutableFiles.ToArray ()); + } else + throw new TorrentException ("Inconsistent hybrid torrent, file padding mismatch."); + } } Files = v2Files; diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent/TorrentCreatorBep47Tests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent/TorrentCreatorBep47Tests.cs index ae597838b..7a5f3a941 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent/TorrentCreatorBep47Tests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent/TorrentCreatorBep47Tests.cs @@ -296,6 +296,74 @@ public async Task HybridTorrentWithPadding () Assert.IsTrue (sha256.AsSpan ().SequenceEqual (torrent.CreatePieceHashes ().GetHash (1).V2Hash.Span)); } + [Test] + public void HybridTorrent_FinalFileHasUnexpectedPadding ([Values(true, false)] bool hasFinalFilePadding) + { + // Test validating both variants of torrent can be loaded + // + // https://github.com/bittorrent/bittorrent.org/issues/160 + // + var v1Files = new BEncodedList { + new BEncodedDictionary { + { "length", (BEncodedNumber)9 }, + { "path", new BEncodedList{ (BEncodedString)"file1.txt" } }, + }, + new BEncodedDictionary { + { "attr", (BEncodedString) "p"}, + { "length", (BEncodedNumber)32759 }, + { "path", new BEncodedList{ (BEncodedString)".pad32759" } }, + }, + + new BEncodedDictionary { + { "length", (BEncodedNumber) 14 }, + { "path", new BEncodedList{ (BEncodedString)"file2.txt" } }, + } + }; + + if (hasFinalFilePadding) + v1Files.Add (new BEncodedDictionary { + { "attr", (BEncodedString) "p" }, + { "length", (BEncodedNumber)32754 }, + { "path", new BEncodedList{ (BEncodedString)".pad32754" } }, + }); + + var v2Files = new BEncodedDictionary { + { "file1.txt", new BEncodedDictionary { + {"", new BEncodedDictionary { + { "length" , (BEncodedNumber) 9 }, + { "pieces root", (BEncodedString) Enumerable.Repeat(0, 32).ToArray () } + } } + } }, + + { "file2.txt", new BEncodedDictionary { + {"", new BEncodedDictionary { + { "length", (BEncodedNumber) 14 }, + { "pieces root", (BEncodedString) Enumerable.Repeat(1, 32).ToArray () } + } } + } }, + }; + + var infoDict = new BEncodedDictionary { + {"files", v1Files }, + {"file tree", v2Files }, + { "meta version", (BEncodedNumber) 2 }, + { "name", (BEncodedString) "padding test"}, + { "piece length", (BEncodedNumber) 32768}, + { "pieces", (BEncodedString) new byte[40] } + }; + + var dict = new BEncodedDictionary { + { "info", infoDict } + }; + + var torrent = Torrent.Load (dict); + Assert.AreEqual (2, torrent.Files.Count); + Assert.AreEqual (9, torrent.Files[0].Length); + Assert.AreEqual (32768 - 9, torrent.Files[0].Padding); + Assert.AreEqual (14, torrent.Files[1].Length); + Assert.AreEqual (hasFinalFilePadding ? 32768 - 14 : 0, torrent.Files[1].Padding); + } + static BEncodedString SHA1SumZeros (long length) { using var hasher = IncrementalHash.CreateHash (HashAlgorithmName.SHA1);