Files
vphone-cli/VPhoneExecutable/VPhoneCommand/VPhonePatchKit/PatchSet/VPhonePatchDeclaration.swift
T
LakrandClaude Opus 5.5 ae5e4531a5 Name every patch {component}-{effect}-{name}
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>
2026-09-30 15:20:20 +09:00

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 + ".")
}
}