From f5dfec00a33d4906e5bcc352447a7e9bbf372f60 Mon Sep 17 00:00:00 2001 From: John Burwell Date: Tue, 2 Jun 2026 19:22:32 -0500 Subject: [PATCH] Handle duplicate Java collection snapshots --- .../Models/Content/LibraryIndex.swift | 2 +- .../Persistence/SourceRestoration.swift | 16 +++--- .../World_Manager_for_MinecraftTests.swift | 54 +++++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/World Manager for Minecraft/Models/Content/LibraryIndex.swift b/World Manager for Minecraft/Models/Content/LibraryIndex.swift index 938b534..4e3c035 100644 --- a/World Manager for Minecraft/Models/Content/LibraryIndex.swift +++ b/World Manager for Minecraft/Models/Content/LibraryIndex.swift @@ -152,7 +152,7 @@ nonisolated struct CollectionSnapshot: Identifiable, Hashable, Sendable, Codable let childDirectoryCount: Int let fingerprint: String - var id: String { folderName } + var id: String { "\(folderName)::\(fingerprint)" } } nonisolated struct SourceSnapshot: Hashable, Sendable, Codable { diff --git a/World Manager for Minecraft/Services/Sources/Persistence/SourceRestoration.swift b/World Manager for Minecraft/Services/Sources/Persistence/SourceRestoration.swift index 4c15cec..85e95a9 100644 --- a/World Manager for Minecraft/Services/Sources/Persistence/SourceRestoration.swift +++ b/World Manager for Minecraft/Services/Sources/Persistence/SourceRestoration.swift @@ -214,20 +214,18 @@ enum SourceRestoration { _ currentCollections: [CollectionSnapshot], persistedCollections: [CollectionSnapshot] ) -> Bool { - let currentCollectionsByName = Dictionary( - uniqueKeysWithValues: currentCollections.map { ($0.folderName, $0) } - ) - let persistedCollectionsByName = Dictionary( - uniqueKeysWithValues: persistedCollections.map { ($0.folderName, $0) } - ) + let currentCollectionsByName = Dictionary(grouping: currentCollections, by: \.folderName) + .mapValues { $0.map(\.fingerprint).sorted() } + let persistedCollectionsByName = Dictionary(grouping: persistedCollections, by: \.folderName) + .mapValues { $0.map(\.fingerprint).sorted() } if currentCollectionsByName.count != persistedCollectionsByName.count { return true } - for (folderName, persistedCollection) in persistedCollectionsByName { - guard let currentCollection = currentCollectionsByName[folderName], - currentCollection.fingerprint == persistedCollection.fingerprint else { + for (folderName, persistedFingerprints) in persistedCollectionsByName { + guard let currentFingerprints = currentCollectionsByName[folderName], + currentFingerprints == persistedFingerprints else { return true } } diff --git a/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift b/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift index 8529272..d7c86d8 100644 --- a/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift +++ b/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift @@ -443,6 +443,60 @@ struct World_Manager_for_MinecraftTests { #expect(snapshots.map(\.folderName).contains("a/e/f/resourcepacks")) } + @Test func sourceRestorationComparesDuplicateCollectionNamesWithoutCrashing() async throws { + let fileManager = FileManager.default + let workingURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + defer { try? fileManager.removeItem(at: workingURL) } + try fileManager.createDirectory(at: workingURL, withIntermediateDirectories: true) + + let collectionSnapshots = [ + CollectionSnapshot( + folderName: "mods", + modifiedDate: Date(timeIntervalSince1970: 100), + childDirectoryCount: 1, + fingerprint: "a/b/c/mods::1::100" + ), + CollectionSnapshot( + folderName: "mods", + modifiedDate: Date(timeIntervalSince1970: 200), + childDirectoryCount: 2, + fingerprint: "x/y/z/mods::2::200" + ) + ] + var source = MinecraftSource( + folderURL: workingURL, + origin: .javaLocalFolder(bookmarkData: nil), + accessDescriptor: SourceAccessDescriptor( + accessorIdentifier: JavaLocalFolderSourceAccess().accessorIdentifier, + kind: .localFolder, + refreshStrategy: .eagerFullScan + ), + availability: .available + ) + source.edition = .java + source.snapshot = SourceSnapshot( + sourceID: workingURL, + rootModifiedDate: nil, + collectionSnapshots: collectionSnapshots, + itemSnapshots: [] + ) + + #expect(SourceRestoration.needsReconcile(source) { _, _ in collectionSnapshots } == false) + #expect( + SourceRestoration.needsReconcile(source) { _, _ in + [ + collectionSnapshots[0], + CollectionSnapshot( + folderName: "mods", + modifiedDate: Date(timeIntervalSince1970: 300), + childDirectoryCount: 3, + fingerprint: "x/y/z/mods::3::300" + ) + ] + } == true + ) + } + @Test func sourceLibraryAddSourceCandidatePreservesJavaAggregateProvider() async throws { let fileManager = FileManager.default let workingURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)