From b32e0b2d5024b9e1468f18c48268b1cc9dcc3999 Mon Sep 17 00:00:00 2001 From: Alan McGovern Date: Sat, 31 Dec 2022 00:50:15 +0000 Subject: [PATCH] [bep52] Fix file ordering sanity check and padding for empty files Torrents can be created with empty files in them. When this occurs, we have to be careful how padding files are assigned to the first 'real' non-empty file, i.e. don't adding padding to empty files. --- src/MonoTorrent.Client/MonoTorrent/Torrent.cs | 4 +- .../MonoTorrent/TorrentCreator.cs | 6 ++- .../MonoTorrent/TorrentFile.cs | 2 + .../MonoTorrent/TorrentCreatorBep47Tests.cs | 41 ++++++++++++++++++- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/MonoTorrent.Client/MonoTorrent/Torrent.cs b/src/MonoTorrent.Client/MonoTorrent/Torrent.cs index fe70dc191..aaa1704de 100644 --- a/src/MonoTorrent.Client/MonoTorrent/Torrent.cs +++ b/src/MonoTorrent.Client/MonoTorrent/Torrent.cs @@ -629,7 +629,7 @@ void LoadInternal (BEncodedDictionary torrentInformation, RawInfoHashes infoHash var merkleTrees = dict.ToDictionary ( key => MerkleRoot.FromMemory (key.Key.AsMemory ()), - kvp => ReadOnlyMerkleLayers.FromLayer (PieceLength, MerkleRoot.FromMemory (kvp.Key.AsMemory ()), ((BEncodedString)kvp.Value).Span) ?? throw new TorrentException ($"Invalid merkle tree. A layer not produce the expected root hash.") + kvp => ReadOnlyMerkleLayers.FromLayer (PieceLength, MerkleRoot.FromMemory (kvp.Key.AsMemory ()), ((BEncodedString) kvp.Value).Span) ?? throw new TorrentException ($"Invalid merkle tree. A layer not produce the expected root hash.") ); hashesV2 = LoadHashesV2 (Files, merkleTrees, PieceLength); @@ -778,7 +778,7 @@ static void LoadTorrentFilesV2 (string key, BEncodedDictionary data, List CreateAsync (string name, ITorrentFileSo if (Type.HasV2 ()) rawFiles = rawFiles.OrderBy (t => t.Destination, StringComparer.Ordinal).ToArray (); - // The last file never has padding bytes - rawFiles[rawFiles.Length - 1].padding = 0; + // The last non-empty file never has padding bytes + var last = rawFiles.Where (t => t.length != 0).Last (); + var index = Array.IndexOf (rawFiles, last); + rawFiles[index].padding = 0; var files = TorrentFileInfo.Create (PieceLength, rawFiles); var manager = new TorrentManagerInfo (files, diff --git a/src/MonoTorrent.Client/MonoTorrent/TorrentFile.cs b/src/MonoTorrent.Client/MonoTorrent/TorrentFile.cs index c5bfc9bdb..74186ffbf 100644 --- a/src/MonoTorrent.Client/MonoTorrent/TorrentFile.cs +++ b/src/MonoTorrent.Client/MonoTorrent/TorrentFile.cs @@ -156,6 +156,8 @@ internal static ITorrentFile[] Create (int pieceLength, TorrentFileTuple[] files // Do not try to handle this, it would be very messy. throw new ArgumentException ("Can not handle torrent that starts with padding"); } else { + if (files[real].length == 0) + throw new InvalidOperationException ("Attempted to add padding bytes to a file which should not have padding."); // add the count to it will also work in case of consecutive padding files, also slightly edge-case-y files[real].padding += files[t].length; } diff --git a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent/TorrentCreatorBep47Tests.cs b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent/TorrentCreatorBep47Tests.cs index 75aace922..7aa91bfa3 100644 --- a/src/Tests/Tests.MonoTorrent.Client/MonoTorrent/TorrentCreatorBep47Tests.cs +++ b/src/Tests/Tests.MonoTorrent.Client/MonoTorrent/TorrentCreatorBep47Tests.cs @@ -78,7 +78,7 @@ private static async Task CreateTestTorrent (TorrentType type) [TestCase(TorrentType.V1OnlyWithPaddingFiles)] [TestCase(TorrentType.V1V2Hybrid)] [TestCase(TorrentType.V2Only)] - public async Task CreateHybridWithUnusualFilenames (TorrentType type) + public async Task CreateWithUnusualFilenames (TorrentType type) { var files = new Source { TorrentName = "asd", @@ -92,6 +92,45 @@ public async Task CreateHybridWithUnusualFilenames (TorrentType type) Assert.DoesNotThrow (() => Torrent.Load (torrent)); } + [Test] + [TestCase (TorrentType.V1Only)] + [TestCase (TorrentType.V1OnlyWithPaddingFiles)] + [TestCase (TorrentType.V1V2Hybrid)] + [TestCase (TorrentType.V2Only)] + public async Task CreateWithManyEmptyFiles (TorrentType type) + { + var files = new Source { + TorrentName = "asd", + Files = new[] { + new FileMapping("a", "a", 0), + new FileMapping("b", "b", PieceLength), + new FileMapping("c", "c", 0), + new FileMapping("d1", "d1", PieceLength / 2), + new FileMapping("d2", "d2", PieceLength / 2), + new FileMapping("e", "e", 0), + new FileMapping("f", "f", 0), + new FileMapping("g", "g", PieceLength + 1), + new FileMapping("h", "h", 0), + } + }; + + var torrent = Torrent.Load (await CreateTestBenc (type, files)); + foreach(var emptyFile in files.Files.Where (t => t.Length == 0)) { + var file = torrent.Files.Single (t => t.Path == emptyFile.Destination); + Assert.AreEqual (0, file.Length); + Assert.AreEqual (0, file.OffsetInTorrent); + Assert.AreEqual (0, file.StartPieceIndex); + Assert.AreEqual (0, file.EndPieceIndex); + Assert.AreEqual (0, file.Padding); + Assert.IsTrue (file.PiecesRoot.IsEmpty); + } + var expectedPadding = type == TorrentType.V1OnlyWithPaddingFiles || type == TorrentType.V1V2Hybrid ? PieceLength / 2 : 0; + Assert.AreEqual (0, torrent.Files.Single (t => t.Path == "b").Padding); + Assert.AreEqual (expectedPadding, torrent.Files.Single (t => t.Path == "d1").Padding); + Assert.AreEqual (expectedPadding, torrent.Files.Single (t => t.Path == "d2").Padding); + Assert.AreEqual (0, torrent.Files.Single (t => t.Path == "g").Padding); + } + [Test] public async Task FileLengthSameAsPieceLength ([Values (TorrentType.V1Only, TorrentType.V1V2Hybrid)] TorrentType type) {