import Foundation import Testing @testable import RunnerCore /// Tests for ``PathResolution`` — the CLI's defence against shell quoting. /// /// The bug these exist for: `--ipsw ~/Downloads/UniversalMac_27.0_*.ipsw` /// behaves differently depending on whether the shell expanded the glob, so the /// tool has to resolve the argument itself and stop caring. @Suite("PathResolution") struct PathResolutionTests { /// A scratch directory holding `names`, deleted when `body` returns. private func withFiles(_ names: [String], _ body: (String) throws -> Void) throws { let dir = FileManager.default.temporaryDirectory .appendingPathComponent("gmr-path-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: dir) } for name in names { let path = dir.appendingPathComponent(name) #expect(FileManager.default.createFile(atPath: path.path, contents: Data())) } try body(dir.path) } // MARK: - Pattern detection @Test("only glob metacharacters make a string a pattern") func patternDetection() { #expect(!PathResolution.isPattern("/tmp/UniversalMac_27.0.ipsw")) // A tilde is expanded either way; it does not make this a glob. #expect(!PathResolution.isPattern("~/Downloads/x.ipsw")) #expect(PathResolution.isPattern("/tmp/a_*.ipsw")) #expect(PathResolution.isPattern("/tmp/a_?.ipsw")) #expect(PathResolution.isPattern("/tmp/a_[12].ipsw")) } // MARK: - Passthrough @Test("a plain path is returned untouched") func plainPathPassesThrough() throws { // Deliberately not checked for existence: the caller's own error knows // what the file was for and says something more useful than we could. #expect(try PathResolution.resolve("/tmp/nope.ipsw", label: "--ipsw") == "/tmp/nope.ipsw") #expect(try PathResolution.resolve("relative/x.ipsw", label: "--ipsw") == "relative/x.ipsw") } @Test("a leading tilde is expanded") func tildeIsExpanded() throws { let home = NSHomeDirectory() #expect(try PathResolution.resolve("~/Downloads/x.ipsw", label: "--ipsw") == home + "/Downloads/x.ipsw") // Only leading: a tilde inside a path component is an ordinary character. #expect(try PathResolution.resolve("/tmp/~x.ipsw", label: "--ipsw") == "/tmp/~x.ipsw") } // MARK: - Globbing @Test("a pattern matching exactly one file resolves to it") func singleMatchResolves() throws { try withFiles(["UniversalMac_27.0_ABC.ipsw", "notes.txt"]) { dir in let resolved = try PathResolution.resolve("\(dir)/UniversalMac_27.0_*.ipsw", label: "--ipsw") #expect(resolved == "\(dir)/UniversalMac_27.0_ABC.ipsw") } } @Test("a pattern matching nothing is a clear notFound") func zeroMatchesThrows() throws { try withFiles(["a_1.ipsw"]) { dir in #expect(throws: CoreError.self) { try PathResolution.resolve("\(dir)/nope_*.ipsw", label: "--ipsw") } do { _ = try PathResolution.resolve("\(dir)/nope_*.ipsw", label: "--ipsw") Issue.record("expected a throw") } catch let error as CoreError { guard case .notFound(let message) = error else { Issue.record("expected .notFound, got \(error)") return } #expect(message.contains("--ipsw")) #expect(message.contains("no file matches")) #expect(message.contains("\(dir)/nope_*.ipsw")) } } } @Test("a pattern matching several files lists them, sorted, and refuses") func multipleMatchesThrowsAndLists() throws { // Created out of order: the message must not depend on creation order, // because the operator is being asked to read it and pick one. try withFiles(["a_2.ipsw", "a_10.ipsw", "a_1.ipsw"]) { dir in do { _ = try PathResolution.resolve("\(dir)/a_*.ipsw", label: "--ipsw") Issue.record("expected a throw") } catch let error as CoreError { guard case .configInvalid(let message) = error else { Issue.record("expected .configInvalid, got \(error)") return } #expect(message.contains("--ipsw")) #expect(message.contains("3 files match")) for name in ["a_1.ipsw", "a_2.ipsw", "a_10.ipsw"] { #expect(message.contains("\(dir)/\(name)")) } // Sorted, so two runs read identically. let one = message.range(of: "a_1.ipsw")!.lowerBound let ten = message.range(of: "a_10.ipsw")!.lowerBound let two = message.range(of: "a_2.ipsw")!.lowerBound #expect(one < ten) #expect(ten < two) #expect(message.contains("name exactly one of them")) } } } @Test("brackets are a character class when they match, and a filename when they do not") func bracketsAreGlobOnlyWhenTheyAreOne() throws { // Used as a class: `[12]` selects the one file that exists. try withFiles(["build_1.ipsw"]) { dir in let asClass = try PathResolution.resolve("\(dir)/build_[12].ipsw", label: "--ipsw") #expect(asClass == "\(dir)/build_1.ipsw") } // A real file whose name contains brackets. Read as a class it matches // nothing, so the literal has to win — otherwise a legal filename is // unreachable through this flag. try withFiles(["report[1].ipsw"]) { dir in let asLiteral = try PathResolution.resolve("\(dir)/report[1].ipsw", label: "--ipsw") #expect(asLiteral == "\(dir)/report[1].ipsw") } } @Test("a pattern that resolves is not confused by neighbours of another extension") func matchingIsScopedToThePattern() throws { try withFiles(["a_1.ipsw", "a_1.ipsw.part", "a_1.txt"]) { dir in let resolved = try PathResolution.resolve("\(dir)/a_*.ipsw", label: "--ipsw") #expect(resolved == "\(dir)/a_1.ipsw") } } // MARK: - IPSW pre-validation /// Writes `bytes` at the front of a sparse file of `size` bytes. /// /// Sparse because a valid-size fixture is a gigabyte and nobody should wait /// for a gigabyte of zeroes to be written to test a two-byte check. private func withIPSW(bytes: [UInt8], size: UInt64, _ body: (String) throws -> Void) throws { let dir = FileManager.default.temporaryDirectory .appendingPathComponent("gmr-ipsw-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: dir) } let path = dir.appendingPathComponent("image.ipsw").path #expect(FileManager.default.createFile(atPath: path, contents: Data(bytes))) let handle = try FileHandle(forWritingTo: URL(fileURLWithPath: path)) try handle.truncate(atOffset: size) try handle.close() try body(path) } private let pk: [UInt8] = [0x50, 0x4B] private let validSize: UInt64 = 2_000_000_000 @Test("a plausible restore image passes") func healthyIPSWValidates() throws { try withIPSW(bytes: pk, size: validSize) { path in try IPSWFile.validate(path: path) } } @Test("a missing file is notFound, not a hang") func missingIPSWThrows() throws { #expect(throws: CoreError.self) { try IPSWFile.validate(path: "/tmp/gmr-definitely-absent.ipsw") } } @Test("a directory is rejected before the framework sees it") func directoryIsRejected() throws { try withFiles([]) { dir in do { try IPSWFile.validate(path: dir) Issue.record("expected a throw") } catch let error as CoreError { #expect("\(error)".contains("directory")) } } } @Test("a truncated download names its own size and says what happened") func truncatedIPSWIsRejected() throws { // 4 MB: the shape of a download that stopped early — right magic bytes, // nowhere near the right length. try withIPSW(bytes: pk, size: 4_000_000) { path in do { try IPSWFile.validate(path: path) Issue.record("expected a throw") } catch let error as CoreError { guard case .configInvalid(let message) = error else { Issue.record("expected .configInvalid, got \(error)") return } #expect(message.contains("3.8 MB")) #expect(message.contains("incomplete")) } } } @Test("an empty file is rejected on size, before anything reads it") func emptyIPSWIsRejected() throws { try withIPSW(bytes: [], size: 0) { path in #expect(throws: CoreError.self) { try IPSWFile.validate(path: path) } } } @Test("a big file that is not a zip is rejected on its magic bytes") func wrongMagicIsRejected() throws { try withIPSW(bytes: [0x00, 0x01], size: validSize) { path in do { try IPSWFile.validate(path: path) Issue.record("expected a throw") } catch let error as CoreError { guard case .configInvalid(let message) = error else { Issue.record("expected .configInvalid, got \(error)") return } #expect(message.contains("PK")) #expect(message.contains("0001")) } } } @Test("sizes are rendered the way the operator will compare them") func sizeDescriptions() { #expect(IPSWFile.describeSize(0) == "0 B") #expect(IPSWFile.describeSize(512) == "512 B") #expect(IPSWFile.describeSize(22_567_352_533) == "21.0 GB") } @Test("the label names the offending flag so the operator knows what to fix") func labelAppearsInErrors() throws { try withFiles([]) { dir in do { _ = try PathResolution.resolve("\(dir)/*.xip", label: "--xcode-xip") Issue.record("expected a throw") } catch let error as CoreError { #expect("\(error)".contains("--xcode-xip")) } } } }