diff --git a/Sources/CupsKit/PPD.swift b/Sources/CupsKit/PPD.swift index 9e26fbf..63bfc96 100644 --- a/Sources/CupsKit/PPD.swift +++ b/Sources/CupsKit/PPD.swift @@ -176,3 +176,25 @@ public struct PPD { options.first { $0.keyword == keyword } } } + +extension PPD { + /// Reads a PPD file, plain or gzip-compressed (vendors ship `.ppd.gz` / `.gz` under + /// /Library/Printers/PPDs). Text that isn't valid UTF-8 is read as ISO Latin-1, the PPD default. + public static func text(contentsOfFile path: String) throws -> String { + guard var data = FileManager.default.contents(atPath: path) else { + throw CupsAdminError.failed("can’t read \(path)") + } + if data.starts(with: [0x1F, 0x8B]) { + let result = try CupsTools.run("/usr/bin/gzip", ["-dc", path]) + guard result.succeeded else { throw CupsAdminError.failed("can’t decompress \(path): \(result.message)") } + data = result.standardOutput + } + return String(data: data, encoding: .utf8) ?? String(data: data, encoding: .isoLatin1) ?? "" + } + + /// True when a `ppdreport` argument names a PPD file rather than a queue. Queue names can't contain "/". + public static func looksLikeFile(_ argument: String) -> Bool { + argument.contains("/") || [".ppd", ".gz", ".ppd.gz"].contains { argument.lowercased().hasSuffix($0) } + } +} + diff --git a/Sources/cupsadmin/Commands/PPDReportCommand.swift b/Sources/cupsadmin/Commands/PPDReportCommand.swift index 6f83b55..ee80df8 100644 --- a/Sources/cupsadmin/Commands/PPDReportCommand.swift +++ b/Sources/cupsadmin/Commands/PPDReportCommand.swift @@ -8,8 +8,7 @@ enum PPDReportCommand { let source: String var attributes: IPPGroup? if let file { - guard let data = FileManager.default.contents(atPath: file) else { throw CupsAdminError.failed("can’t read \(file)") } - text = String(decoding: data, as: UTF8.self) + text = try PPD.text(contentsOfFile: file) source = file } else if let queue { guard try await client.queueExists(queue) else { throw CupsAdminError.failed("no such queue \(queue)") } diff --git a/Sources/cupsadmin/main.swift b/Sources/cupsadmin/main.swift index 35ebfd5..41479cb 100644 --- a/Sources/cupsadmin/main.swift +++ b/Sources/cupsadmin/main.swift @@ -42,7 +42,7 @@ let usage = """ quick list quick actions (task-named queue defaults) quick [code | --clear] apply one via lpadmin, then verify - ppdreport | --file + ppdreport every PPD group and option: keyword, label, type, default, choices help show this help """ @@ -130,7 +130,12 @@ func runCommand(_ args: [String]) async throws -> String { } try rejectUnknownFlags() guard (file == nil && rest.count == 1) || (file != nil && rest.isEmpty) else { - throw CupsAdminError.usage("ppdreport needs one queue name or --file ") + throw CupsAdminError.usage("ppdreport needs one queue name or PPD file path") + } + // A path (anything with "/", or ending in .ppd / .gz) is a PPD file; queue names can't contain "/". + if file == nil, let argument = rest.first, PPD.looksLikeFile(argument) { + file = argument + rest.removeAll() } return try await PPDReportCommand.run(client: client, queue: rest.first, file: file)