From 95c72524677527dfe6b3db4ed62723804b3ade21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 14:43:41 +0200 Subject: [PATCH 1/2] Add failing test case --- .../Database/BeatmapImporterTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 38746f2567..f3ca665380 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -1018,6 +1018,49 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestBeatmapFilesInNestedDirectoriesAreIgnored() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); + + string? temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + var subdirectory = Directory.CreateDirectory(Path.Combine(extractedFolder, "subdir")); + string modifiedCopyPath = Path.Combine(subdirectory.FullName, "duplicate.osu"); + File.Copy(Directory.GetFiles(extractedFolder, "*.osu").First(), modifiedCopyPath); + + using (var stream = File.OpenWrite(modifiedCopyPath)) + using (var textWriter = new StreamWriter(stream)) + await textWriter.WriteLineAsync("# adding a comment so that the hashes are different"); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + await importer.Import(temp); + + EnsureLoaded(realm.Realm); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + [Test] public void TestImportNestedStructure() { From 79f7f0ecad2f5721ee84470db9f415cdcb57f417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 14:46:39 +0200 Subject: [PATCH 2/2] Ignore `.osu` files not placed at top level of beatmap archive on import Closes https://github.com/ppy/osu/issues/34677. --- osu.Game/Beatmaps/BeatmapImporter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 28997509dc..f80c4de4ea 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -367,7 +367,11 @@ namespace osu.Game.Beatmaps { var beatmaps = new List(); - foreach (var file in beatmapSet.Files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) + // stable appears to ignore `.osu` files which are not placed at the top level of the beatmap archive. + // the logic that achieves this is very difficult to make sense of, but appears to be located somewhere around + // https://github.com/peppy/osu-stable-reference/blob/67795dba3c308e7d0493b296149dcb073ca47ecb/osu!/GameplayElements/Beatmaps/BeatmapManager.cs#L207-L208 + // only testing the `/` path separator character is sufficient as `RealmNamedFileUsage`s are normalised to use the front slash unix path separator convention + foreach (var file in beatmapSet.Files.Where(f => !f.Filename.Contains('/') && f.Filename.EndsWith(@".osu", StringComparison.OrdinalIgnoreCase))) { using (var memoryStream = new MemoryStream(Files.Store.Get(file.File.GetStoragePath()))) // we need a memory stream so we can seek {