|
#!/usr/bin/env swift |
|
|
|
import Foundation |
|
import PDFKit |
|
|
|
struct RawArguments { |
|
var inputPath: String? |
|
var password: String? |
|
var outputDir: String? |
|
} |
|
|
|
struct Config { |
|
let inputPath: String |
|
let password: String |
|
let outputDir: String |
|
} |
|
|
|
func parseArguments() -> RawArguments? { |
|
let args = CommandLine.arguments |
|
var result = RawArguments() |
|
|
|
func nextValue(for flag: String, at index: Int) -> String? { |
|
guard index + 1 < args.count else { |
|
fputs("Error: \(flag) requires a value\n", stderr) |
|
return nil |
|
} |
|
return args[index + 1] |
|
} |
|
|
|
var i = 1 |
|
while i < args.count { |
|
switch args[i] { |
|
case "-i", "--input": |
|
guard let value = nextValue(for: args[i], at: i) else { return nil } |
|
result.inputPath = value |
|
i += 2 |
|
case "-p", "--password": |
|
guard let value = nextValue(for: args[i], at: i) else { return nil } |
|
result.password = value |
|
i += 2 |
|
case "-o", "--output": |
|
guard let value = nextValue(for: args[i], at: i) else { return nil } |
|
result.outputDir = value |
|
i += 2 |
|
case "-h", "--help": |
|
printUsage() |
|
return nil |
|
default: |
|
fputs("Error: unknown argument '\(args[i])'\n", stderr) |
|
printUsage() |
|
return nil |
|
} |
|
} |
|
|
|
return result |
|
} |
|
|
|
func validateArguments(_ raw: RawArguments) -> Config? { |
|
func require<T>(_ value: T?, name: String) -> T? { |
|
guard let value = value else { |
|
fputs("Error: \(name) is required\n", stderr) |
|
printUsage() |
|
return nil |
|
} |
|
return value |
|
} |
|
|
|
guard let inputPath = require(raw.inputPath, name: "--input"), |
|
let password = require(raw.password, name: "--password"), |
|
let outputDir = require(raw.outputDir, name: "--output") else { |
|
return nil |
|
} |
|
|
|
return Config(inputPath: inputPath, password: password, outputDir: outputDir) |
|
} |
|
|
|
func prepareOutput(inputPath: String, outputDir: String) -> String? { |
|
let outputDirURL = URL(fileURLWithPath: outputDir) |
|
|
|
do { |
|
try FileManager.default.createDirectory(at: outputDirURL, withIntermediateDirectories: true) |
|
} catch { |
|
fputs("Error: failed to create directory \(outputDir)\n", stderr) |
|
return nil |
|
} |
|
|
|
let basename = URL(fileURLWithPath: inputPath).deletingPathExtension().lastPathComponent |
|
return outputDirURL.appendingPathComponent("\(basename).pdf").path |
|
} |
|
|
|
func printUsage() { |
|
fputs(""" |
|
Usage: decrypt_pdf.swift -i <input.pdf> -p <password> -o <output_dir> |
|
|
|
Arguments: |
|
-i, --input path to password-protected PDF (required) |
|
-p, --password password to unlock the PDF (required) |
|
-o, --output output directory (required) |
|
-h, --help show this help message |
|
|
|
""", stderr) |
|
} |
|
|
|
func isPDF(url: URL) -> Bool { |
|
guard let handle = try? FileHandle(forReadingFrom: url) else { |
|
return false |
|
} |
|
defer { try? handle.close() } |
|
|
|
guard let data = try? handle.read(upToCount: 5) else { |
|
return false |
|
} |
|
|
|
// PDF magic bytes: %PDF- |
|
return data.starts(with: [0x25, 0x50, 0x44, 0x46, 0x2D]) |
|
} |
|
|
|
func validatePDF(url: URL, password: String) -> PDFDocument? { |
|
// Validate file is a PDF (magic bytes) |
|
guard isPDF(url: url) else { |
|
fputs("Error: input file is not a valid PDF\n", stderr) |
|
return nil |
|
} |
|
|
|
// Open PDF document |
|
guard let pdfDocument = PDFDocument(url: url) else { |
|
fputs("Error: failed to open PDF (file may be corrupted)\n", stderr) |
|
return nil |
|
} |
|
|
|
// Validate that PDF is password-protected |
|
guard pdfDocument.isLocked else { |
|
fputs("Error: PDF is not password-protected, no decryption needed\n", stderr) |
|
return nil |
|
} |
|
|
|
// Validate password |
|
guard pdfDocument.unlock(withPassword: password) else { |
|
fputs("Error: incorrect password\n", stderr) |
|
return nil |
|
} |
|
|
|
return pdfDocument |
|
} |
|
|
|
func decryptPDF(document: PDFDocument, outputPath: String) -> Bool { |
|
let newDocument = PDFDocument() |
|
|
|
for i in 0..<document.pageCount { |
|
guard let page = document.page(at: i) else { |
|
fputs("Error: failed to read page \(i + 1)\n", stderr) |
|
return false |
|
} |
|
newDocument.insert(page, at: i) |
|
} |
|
|
|
guard newDocument.write(to: URL(fileURLWithPath: outputPath)) else { |
|
fputs("Error: failed to save file\n", stderr) |
|
return false |
|
} |
|
|
|
print("Saved: \(outputPath)") |
|
return true |
|
} |
|
|
|
// MARK: - Main |
|
|
|
guard let rawArgs = parseArguments() else { |
|
exit(1) |
|
} |
|
|
|
guard let config = validateArguments(rawArgs) else { |
|
exit(1) |
|
} |
|
|
|
guard let outputPath = prepareOutput(inputPath: config.inputPath, outputDir: config.outputDir) else { |
|
exit(1) |
|
} |
|
|
|
guard let pdfDocument = validatePDF(url: URL(fileURLWithPath: config.inputPath), password: config.password) else { |
|
exit(1) |
|
} |
|
|
|
if decryptPDF(document: pdfDocument, outputPath: outputPath) { |
|
exit(0) |
|
} else { |
|
exit(1) |
|
} |