Merge nucleic/mellow-dewy-falcon-rjhr into main
This commit is contained in:
@@ -108,3 +108,80 @@ public enum PathResolution {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// Cheap sanity checks on a `.ipsw` before Virtualization.framework sees it.
|
||||
///
|
||||
/// Lives beside ``PathResolution`` because it is the other half of the same
|
||||
/// job — what the CLI does with a path an operator typed — and because
|
||||
/// `RunnerCore` is the only target that unit-tests on both platforms.
|
||||
///
|
||||
/// The checks earn their place: `VZMacOSRestoreImage.image(from:)` reports no
|
||||
/// progress at all while it works, and on a partial download it can sit for a
|
||||
/// very long time rather than failing. Without these, "I pointed it at the
|
||||
/// wrong file" and "my 21 GB download stopped at 4 GB" both present to the
|
||||
/// operator as an unexplained hang.
|
||||
public enum IPSWFile {
|
||||
/// The floor a real macOS restore image clears by an order of magnitude —
|
||||
/// they run ~15-22 GB. Anything under this is a truncated download, a
|
||||
/// placeholder, or the wrong file entirely.
|
||||
public static let minimumBytes: Int64 = 1_000_000_000
|
||||
|
||||
/// The first two bytes of every `.ipsw`: an IPSW is a zip archive.
|
||||
static let zipMagic = Data([0x50, 0x4B]) // "PK"
|
||||
|
||||
/// Fails fast if `path` cannot be a usable restore image.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: An already-resolved absolute path (see ``PathResolution``).
|
||||
/// - label: What the file is, for error messages.
|
||||
/// - Throws: ``CoreError/notFound(_:)`` if it is not there or unreadable,
|
||||
/// ``CoreError/configInvalid(_:)`` if it is there but cannot be an IPSW.
|
||||
public static func validate(path: String, label: String = "restore image") throws {
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else {
|
||||
throw CoreError.notFound("\(label) not found at \(path)")
|
||||
}
|
||||
guard !isDirectory.boolValue else {
|
||||
throw CoreError.configInvalid(
|
||||
"\(label) at \(path) is a directory, not an .ipsw file"
|
||||
)
|
||||
}
|
||||
|
||||
let attributes = try? FileManager.default.attributesOfItem(atPath: path)
|
||||
let size = (attributes?[.size] as? NSNumber)?.int64Value ?? 0
|
||||
guard size >= minimumBytes else {
|
||||
throw CoreError.configInvalid(
|
||||
"\(label) at \(path) is only \(describeSize(size)) — a macOS restore image is "
|
||||
+ "~15-22 GB. The download is most likely incomplete; delete the file and "
|
||||
+ "fetch it again."
|
||||
)
|
||||
}
|
||||
|
||||
guard let handle = FileHandle(forReadingAtPath: path) else {
|
||||
throw CoreError.notFound("\(label) at \(path) could not be opened for reading")
|
||||
}
|
||||
defer { try? handle.close() }
|
||||
|
||||
let magic = (try? handle.read(upToCount: zipMagic.count)) ?? Data()
|
||||
guard magic == zipMagic else {
|
||||
let found = magic.map { String(format: "%02x", $0) }.joined()
|
||||
throw CoreError.configInvalid(
|
||||
"\(label) at \(path) does not look like an .ipsw: expected a zip archive "
|
||||
+ "(magic \"PK\", 504b) but the file starts with \(found.isEmpty ? "nothing" : found). "
|
||||
+ "Check the path, or re-download the file."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A byte count an operator can compare against "~15 GB" at a glance.
|
||||
static func describeSize(_ bytes: Int64) -> String {
|
||||
let units = ["B", "KB", "MB", "GB", "TB"]
|
||||
var value = Double(bytes)
|
||||
var unit = 0
|
||||
while value >= 1024, unit < units.count - 1 {
|
||||
value /= 1024
|
||||
unit += 1
|
||||
}
|
||||
return unit == 0 ? "\(Int(value)) B" : String(format: "%.1f %@", value, units[unit])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user