From 234b7c2be9677b2e606c7f2aa7b0b5c1d6f5e14b Mon Sep 17 00:00:00 2001 From: yuvi2118 Date: Fri, 6 Nov 2026 03:44:39 +0900 Subject: [PATCH] Don't overwrite same-named items when moving admin-owned files to the Trash Storage.clean picks each item's Trash name with uniqueName(_:in:) while it builds the list of administrator commands, but the moves only run later, in one batch. A name chosen for one item is not in the Trash yet, so two items with the same file name (for example two Cache.db files) got the same target, and the second `mv -f` overwrote the first. uniqueName now takes the names already handed out in the batch, and clean reserves each name it picks. Two self-test checks cover the new behaviour. Co-Authored-By: Claude Sonnet 5.5 --- Sources/removemacai/SelfTest.swift | 2 ++ Sources/removemacai/Storage.swift | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/removemacai/SelfTest.swift b/Sources/removemacai/SelfTest.swift index 79e71e0..8d3d28e 100644 --- a/Sources/removemacai/SelfTest.swift +++ b/Sources/removemacai/SelfTest.swift @@ -321,6 +321,8 @@ func selfTest() -> Bool { } check(Storage.uniqueName("a.app", in: "/nonexistent-folder") == "a.app", "trash names stay as they are when free") + check(Storage.uniqueName("a.db", in: "/nonexistent-folder", avoiding: ["a.db"]) == "a 2.db", "a name already given to another item in the batch is numbered") + check(Storage.uniqueName("a.db", in: "/nonexistent-folder", avoiding: ["a.db", "a 2.db"]) == "a 3.db", "numbering skips names already given in the batch") let script = BackgroundItem(label: "com.example.sync", plist: "", program: "/bin/bash", arguments: ["/bin/bash", "/Users/x/bin/sync.sh"], system: false) let helper = BackgroundItem(label: "dev.orbstack.OrbStack.privhelper", plist: "", diff --git a/Sources/removemacai/Storage.swift b/Sources/removemacai/Storage.swift index f95171c..45985a8 100644 --- a/Sources/removemacai/Storage.swift +++ b/Sources/removemacai/Storage.swift @@ -210,6 +210,7 @@ enum Storage { static func clean(_ items: [StorageItem]) -> CleanResult { var result = CleanResult() var adminCommands: [String] = [] + var reservedNames = Set() let trash = home + "/.Trash" for item in items { switch item.kind { @@ -229,7 +230,11 @@ enum Storage { continue } let name = (path as NSString).lastPathComponent - let target = trash + "/" + uniqueName(name, in: trash) + // These moves run later, in one batch, so a name picked for one item + // isn't in the Trash yet. Reserve it so a same-named item gets another. + let unique = uniqueName(name, in: trash, avoiding: reservedNames) + reservedNames.insert(unique) + let target = trash + "/" + unique adminCommands.append("/bin/mv -f \(Shell.quote(path)) \(Shell.quote(target))") } } @@ -247,10 +252,10 @@ enum Storage { return result } - static func uniqueName(_ name: String, in folder: String) -> String { + static func uniqueName(_ name: String, in folder: String, avoiding taken: Set = []) -> String { var candidate = name var n = 2 - while FileManager.default.fileExists(atPath: folder + "/" + candidate) { + while FileManager.default.fileExists(atPath: folder + "/" + candidate) || taken.contains(candidate) { let ext = (name as NSString).pathExtension let base = (name as NSString).deletingPathExtension candidate = ext.isEmpty ? "\(base) \(n)" : "\(base) \(n).\(ext)"