A macOS CLI tool that exports original photos and videos from the macOS Photos library using PhotoKit.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Harden review findings: private buildExportScript, exit code 77

- Make buildExportScript private to prevent calling it without UUID
validation from other LadderKit code
- Use exit code 77 for permission errors so attic can detect them
via exit code instead of fragile string matching on stderr
- Convert escaping tests to test escapeForAppleScript directly

+25 -32
+8 -2
Sources/CLI/Main.swift
··· 39 39 // Pre-flight: verify Automation permission before starting exports 40 40 do { 41 41 try await exporter.checkPermissions() 42 + } catch let error as AppleScriptError { 43 + fatalExit(error.localizedDescription, code: permissionExitCode) 42 44 } catch { 43 45 fatalExit(error.localizedDescription) 44 46 } ··· 74 76 return try JSONDecoder().decode(ExportRequest.self, from: data) 75 77 } 76 78 77 - private static func fatalExit(_ message: String) -> Never { 79 + /// Exit code 77 signals a permission error to the calling process, 80 + /// so it can abort early without string-matching stderr. 81 + private static let permissionExitCode: Int32 = 77 82 + 83 + private static func fatalExit(_ message: String, code: Int32 = 1) -> Never { 78 84 FileHandle.standardError.write(Data("ladder: \(message)\n".utf8)) 79 - exit(1) 85 + exit(code) 80 86 } 81 87 }
+1 -1
Sources/LadderKit/AppleScriptExporter.swift
··· 171 171 172 172 // MARK: - Helpers 173 173 174 - func buildExportScript(uuid: String, destination: String) -> String { 174 + private func buildExportScript(uuid: String, destination: String) -> String { 175 175 let safeUUID = escapeForAppleScript(uuid) 176 176 let safeDest = escapeForAppleScript(destination) 177 177 // AppleScript export command — Photos.app handles iCloud download transparently
+16 -29
Tests/AppleScriptExporterTests.swift
··· 65 65 == "8A3B1C2D-4E5F-6789-ABCD-EF0123456789") 66 66 } 67 67 68 - // MARK: - buildExportScript 69 - 70 - @Test("builds correct AppleScript") 71 - func exportScript() { 72 - let script = buildExportScript( 73 - uuid: "ABC-123", 74 - destination: "/tmp/staging/as_ABC-123" 75 - ) 76 - #expect(script.contains("media item id \"ABC-123\"")) 77 - #expect(script.contains("POSIX file \"/tmp/staging/as_ABC-123\"")) 78 - #expect(script.contains("with using originals")) 79 - } 80 - 81 68 // MARK: - PhotoExporter with script fallback 82 69 83 70 @Test("falls back to AppleScript for missing UUIDs") ··· 277 264 278 265 // MARK: - AppleScript escaping 279 266 280 - @Test("buildExportScript escapes quotes in UUID") 281 - func scriptEscapesQuotes() { 282 - let script = buildExportScript( 283 - uuid: #"ABC" & evil & ""#, 284 - destination: "/tmp/safe" 285 - ) 286 - // The quote should be escaped, not terminating the string 287 - #expect(script.contains(#"ABC\" & evil & \""#)) 288 - #expect(!script.contains(#"id "ABC" & evil"#)) 267 + @Test("escapeForAppleScript escapes double quotes") 268 + func escapesQuotes() { 269 + let result = escapeForAppleScript(#"ABC" & evil & ""#) 270 + #expect(result == #"ABC\" & evil & \""#) 289 271 } 290 272 291 - @Test("buildExportScript escapes backslashes in destination") 292 - func scriptEscapesBackslashes() { 293 - let script = buildExportScript( 294 - uuid: "ABC-123", 295 - destination: #"/tmp/path\with\backslashes"# 296 - ) 297 - #expect(script.contains(#"path\\with\\backslashes"#)) 273 + @Test("escapeForAppleScript escapes backslashes") 274 + func escapesBackslashes() { 275 + let result = escapeForAppleScript(#"/tmp/path\with\backslashes"#) 276 + #expect(result == #"/tmp/path\\with\\backslashes"#) 277 + } 278 + 279 + @Test("escapeForAppleScript handles combined quotes and backslashes") 280 + func escapesCombined() { 281 + // Input: a\"b (backslash then quote) 282 + // Expected: a\\\"b (backslash escaped, then quote escaped) 283 + let result = escapeForAppleScript(#"a\"b"#) 284 + #expect(result == #"a\\\"b"#) 298 285 } 299 286 300 287 // MARK: - UUID validation