mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-02 08:04:32 +08:00
The 117 bundled patch identifiers had grown five naming schemes
(kernel.x, jb.x, kernelcache_jb.x, txm_dev.x, bare names). Each one is now
{component}-{effect}-{name}:
- component: avpbooter, ibss, ibec, llb, txm, kernel, devicetree, dyld,
preboot, or system-<binary> for a guest binary or file.
- effect: boot when the patch is boot-essential, exp when the standard
preset leaves it off, cfw otherwise. A catalog test enforces this.
- name: snake_case, no hyphen, so the identifier splits from the right.
Record sites are now always <identifier>.<site>. The underscore-prefix
rule in covers(recordIdentifier:) and in the gate is gone: the new names
contain underscores, so kernel-boot-post_validation would otherwise have
covered kernel-boot-post_validation_unsigned. The 25 records that relied
on it (amfi_trustcache_1, launch_constraints_mov, sandbox_ext_N, ...) now
use a dot.
Old identifiers are not migrated. A VM whose PatchPlan or PatchSelection
names one must be patched again. The bundle becomes 2.2.0 and Launchpad
requires 2.2.0, so it never meets an old identifier from a bundle.
Launchpad's patch table shows Component, Effect and Name columns in place
of Identifier and Patch Set; the set moves to the detail line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.3 KiB
Swift
109 lines
4.3 KiB
Swift
// VPhonePatchDeclaration.swift — One selectable patch, as its set declares it.
|
|
//
|
|
// The identifier is the contract. It is the prefix of the ``PatchRecord``
|
|
// identifiers the patch emits, so a patch writing four records under
|
|
// `kernel-boot-cred_label_update_execve.*` declares one identifier and is selected or
|
|
// blocked as a unit — a half-applied patch of that shape would not boot.
|
|
|
|
import Foundation
|
|
|
|
public struct VPhonePatchDeclaration: Sendable, Hashable, Codable, Identifiable {
|
|
/// Stable selection identity, and the record-identifier prefix.
|
|
public var identifier: String
|
|
/// Short label for a preset picker. One line, no trailing period.
|
|
public var title: String
|
|
/// What the patch does and why the guest needs it.
|
|
public var summary: String
|
|
/// What the patch writes to.
|
|
public var target: VPhonePatchTarget
|
|
/// The OS pairings the patch applies to.
|
|
public var applicability: VPhonePatchApplicability
|
|
/// True when the guest does not boot without this patch.
|
|
///
|
|
/// Blocking one is allowed — that is how an external set replaces it — but
|
|
/// every caller that offers a choice says so first, and the resolver reports
|
|
/// which essentials a plan dropped.
|
|
public var bootEssential: Bool
|
|
|
|
public var id: String {
|
|
identifier
|
|
}
|
|
|
|
public init(
|
|
identifier: String,
|
|
title: String,
|
|
summary: String = "",
|
|
target: VPhonePatchTarget,
|
|
applicability: VPhonePatchApplicability = .always,
|
|
bootEssential: Bool = false,
|
|
) {
|
|
self.identifier = identifier
|
|
self.title = title
|
|
self.summary = summary
|
|
self.target = target
|
|
self.applicability = applicability
|
|
self.bootEssential = bootEssential
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case identifier = "Identifier"
|
|
case title = "Title"
|
|
case summary = "Summary"
|
|
case target = "Target"
|
|
case applicability = "Applicability"
|
|
case bootEssential = "BootEssential"
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
identifier = try container.decode(String.self, forKey: .identifier)
|
|
guard !identifier.isEmpty else {
|
|
throw DecodingError.dataCorruptedError(
|
|
forKey: .identifier,
|
|
in: container,
|
|
debugDescription: "A patch identifier is what a preset names; it cannot be empty",
|
|
)
|
|
}
|
|
title = try container.decode(String.self, forKey: .title)
|
|
summary = try container.decodeIfPresent(String.self, forKey: .summary) ?? ""
|
|
target = try container.decode(VPhonePatchTarget.self, forKey: .target)
|
|
applicability = try container.decodeIfPresent(
|
|
VPhonePatchApplicability.self,
|
|
forKey: .applicability,
|
|
) ?? .always
|
|
bootEssential = try container.decodeIfPresent(Bool.self, forKey: .bootEssential) ?? false
|
|
}
|
|
|
|
public func encode(to encoder: any Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(identifier, forKey: .identifier)
|
|
try container.encode(title, forKey: .title)
|
|
if !summary.isEmpty {
|
|
try container.encode(summary, forKey: .summary)
|
|
}
|
|
try container.encode(target, forKey: .target)
|
|
if applicability != .always {
|
|
try container.encode(applicability, forKey: .applicability)
|
|
}
|
|
if bootEssential {
|
|
try container.encode(bootEssential, forKey: .bootEssential)
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension VPhonePatchDeclaration {
|
|
/// Whether `recordIdentifier` came from this patch.
|
|
///
|
|
/// A record either is the declaration itself or sits under it as
|
|
/// `<identifier>.<site>` — `kernel-boot-kcall10.sy_call`,
|
|
/// `llb-boot-rootfs.cbz_0x3b7`. Only a dot separates a site: identifiers
|
|
/// are snake_case and contain underscores, so an underscore suffix would
|
|
/// let `kernel-boot-sandbox_mount_check_mount` cover a different patch
|
|
/// that happens to extend its name. A bare textual prefix does not match
|
|
/// either, so `kernel-cfw-debuggerless` is not a site of `kernel-cfw-debugger`.
|
|
func covers(recordIdentifier record: String) -> Bool {
|
|
record == identifier
|
|
|| record.hasPrefix(identifier + ".")
|
|
}
|
|
}
|