mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 13:09:06 +08:00
Run SwiftFormat on firmware patcher Remove legacy Python firmware patchers Fix compare pipeline pyimg4 PATH handling Restore Python patchers and prefer fresh restore Update BinaryBuffer.swift Avoid double scanning in patcher apply Prefer Python TXM site before fallback Retarget TXM trustcache finder for 26.1 Remove legacy Python firmware patchers Fail fast on nested virtualization hosts Return nonzero on fatal boot startup Add amfidont helper for signed boot binary Stage AMFI boot args for next host reboot Add host preflight for boot entitlements Fail fast when boot entitlements are unavailable Switch firmware patch targets to Swift CLI Record real Swift firmware parity results Verify Swift firmware pipeline end-to-end parity Fix Swift firmware pipeline JB dry-run
373 lines
12 KiB
Swift
373 lines
12 KiB
Swift
// DeviceTreePatcher.swift — DeviceTree payload patcher.
|
|
//
|
|
// Historical note: derived from the legacy Python firmware patcher during the Swift migration.
|
|
//
|
|
// Strategy:
|
|
// 1. Parse the flat device tree binary into a node/property tree.
|
|
// 2. Apply a fixed set of property patches (serial-number, home-button-type,
|
|
// artwork-device-subtype, island-notch-location).
|
|
// 3. Serialize the modified tree back to flat binary.
|
|
|
|
import Foundation
|
|
|
|
/// Patcher for DeviceTree payloads.
|
|
public final class DeviceTreePatcher: Patcher {
|
|
public let component = "devicetree"
|
|
public let verbose: Bool
|
|
|
|
let buffer: BinaryBuffer
|
|
var patches: [PatchRecord] = []
|
|
var rebuiltData: Data?
|
|
|
|
// MARK: - Patch Definitions
|
|
|
|
/// A single property patch specification.
|
|
struct PropertyPatch {
|
|
let nodePath: [String]
|
|
let property: String
|
|
let length: Int
|
|
let flags: UInt16
|
|
let value: PropertyValue
|
|
let patchID: String
|
|
let description: String
|
|
}
|
|
|
|
/// The value to write into a device tree property.
|
|
enum PropertyValue {
|
|
case string(String)
|
|
case integer(UInt64)
|
|
}
|
|
|
|
/// Fixed set of device tree patches, matching scripts/dtree.py PATCHES.
|
|
static let propertyPatches: [PropertyPatch] = [
|
|
PropertyPatch(
|
|
nodePath: ["device-tree"],
|
|
property: "serial-number",
|
|
length: 12,
|
|
flags: 0,
|
|
value: .string("vphone-1337"),
|
|
patchID: "devicetree.serial_number",
|
|
description: "Set serial number to vphone-1337"
|
|
),
|
|
PropertyPatch(
|
|
nodePath: ["device-tree", "buttons"],
|
|
property: "home-button-type",
|
|
length: 4,
|
|
flags: 0,
|
|
value: .integer(2),
|
|
patchID: "devicetree.home_button_type",
|
|
description: "Set home button type to 2"
|
|
),
|
|
PropertyPatch(
|
|
nodePath: ["device-tree", "product"],
|
|
property: "artwork-device-subtype",
|
|
length: 4,
|
|
flags: 0,
|
|
value: .integer(2556),
|
|
patchID: "devicetree.artwork_device_subtype",
|
|
description: "Set artwork device subtype to 2556"
|
|
),
|
|
PropertyPatch(
|
|
nodePath: ["device-tree", "product"],
|
|
property: "island-notch-location",
|
|
length: 4,
|
|
flags: 0,
|
|
value: .integer(144),
|
|
patchID: "devicetree.island_notch_location",
|
|
description: "Set island notch location to 144"
|
|
),
|
|
]
|
|
|
|
// MARK: - Device Tree Structures
|
|
|
|
/// A single property in a device tree node.
|
|
final class DTProperty {
|
|
var name: String
|
|
var length: Int
|
|
var flags: UInt16
|
|
var value: Data
|
|
/// File offset of the property value within the flat binary.
|
|
let valueOffset: Int
|
|
|
|
init(name: String, length: Int, flags: UInt16, value: Data, valueOffset: Int) {
|
|
self.name = name
|
|
self.length = length
|
|
self.flags = flags
|
|
self.value = value
|
|
self.valueOffset = valueOffset
|
|
}
|
|
}
|
|
|
|
/// A node in the device tree containing properties and child nodes.
|
|
final class DTNode {
|
|
var properties: [DTProperty] = []
|
|
var children: [DTNode] = []
|
|
}
|
|
|
|
// MARK: - Init
|
|
|
|
public init(data: Data, verbose: Bool = true) {
|
|
buffer = BinaryBuffer(data)
|
|
self.verbose = verbose
|
|
}
|
|
|
|
// MARK: - Patcher
|
|
|
|
public func findAll() throws -> [PatchRecord] {
|
|
patches = []
|
|
rebuiltData = nil
|
|
let root = try parsePayload(buffer.data)
|
|
try applyPatches(root: root)
|
|
rebuiltData = serializePayload(root)
|
|
return patches
|
|
}
|
|
|
|
@discardableResult
|
|
public func apply() throws -> Int {
|
|
if patches.isEmpty, rebuiltData == nil {
|
|
let _ = try findAll()
|
|
}
|
|
if let rebuiltData {
|
|
buffer.data = rebuiltData
|
|
} else {
|
|
for record in patches {
|
|
buffer.writeBytes(at: record.fileOffset, bytes: record.patchedBytes)
|
|
}
|
|
}
|
|
if verbose, !patches.isEmpty {
|
|
print("\n [\(patches.count) DeviceTree patch(es) applied]")
|
|
}
|
|
return patches.count
|
|
}
|
|
|
|
public var patchedData: Data {
|
|
rebuiltData ?? buffer.data
|
|
}
|
|
|
|
// MARK: - Parsing
|
|
|
|
/// Align a value up to the next 4-byte boundary.
|
|
private static func align4(_ n: Int) -> Int {
|
|
(n + 3) & ~3
|
|
}
|
|
|
|
/// Decode a null-terminated C string from raw bytes.
|
|
private static func decodeCString(_ data: Data) -> String {
|
|
if let nullIndex = data.firstIndex(of: 0) {
|
|
let slice = data[data.startIndex ..< nullIndex]
|
|
return String(bytes: slice, encoding: .utf8) ?? ""
|
|
}
|
|
return String(bytes: data, encoding: .utf8) ?? ""
|
|
}
|
|
|
|
/// Parse a device tree node from the flat binary at the given offset.
|
|
/// Returns the parsed node and the offset past the end of the node.
|
|
private func parseNode(_ blob: Data, offset: Int) throws -> (DTNode, Int) {
|
|
guard offset + 8 <= blob.count else {
|
|
throw PatcherError.invalidFormat("DeviceTree: truncated node header at offset \(offset)")
|
|
}
|
|
|
|
let nProps = blob.loadLE(UInt32.self, at: offset)
|
|
let nChildren = blob.loadLE(UInt32.self, at: offset + 4)
|
|
var pos = offset + 8
|
|
|
|
let node = DTNode()
|
|
|
|
for _ in 0 ..< nProps {
|
|
guard pos + 36 <= blob.count else {
|
|
throw PatcherError.invalidFormat("DeviceTree: truncated property header at offset \(pos)")
|
|
}
|
|
|
|
let nameData = blob[blob.startIndex.advanced(by: pos) ..< blob.startIndex.advanced(by: pos + 32)]
|
|
let name = Self.decodeCString(Data(nameData))
|
|
let length = Int(blob.loadLE(UInt16.self, at: pos + 32))
|
|
let flags = blob.loadLE(UInt16.self, at: pos + 34)
|
|
pos += 36
|
|
|
|
guard pos + length <= blob.count else {
|
|
throw PatcherError.invalidFormat("DeviceTree: truncated property value '\(name)' at offset \(pos)")
|
|
}
|
|
|
|
let value = Data(blob[blob.startIndex.advanced(by: pos) ..< blob.startIndex.advanced(by: pos + length)])
|
|
let valueOffset = pos
|
|
pos += Self.align4(length)
|
|
|
|
node.properties.append(DTProperty(
|
|
name: name, length: length, flags: flags,
|
|
value: value, valueOffset: valueOffset
|
|
))
|
|
}
|
|
|
|
for _ in 0 ..< nChildren {
|
|
let (child, nextPos) = try parseNode(blob, offset: pos)
|
|
node.children.append(child)
|
|
pos = nextPos
|
|
}
|
|
|
|
return (node, pos)
|
|
}
|
|
|
|
/// Parse the entire device tree payload.
|
|
private func parsePayload(_ blob: Data) throws -> DTNode {
|
|
let (root, end) = try parseNode(blob, offset: 0)
|
|
guard end == blob.count else {
|
|
throw PatcherError.invalidFormat(
|
|
"DeviceTree: unexpected trailing bytes (\(blob.count - end) extra)"
|
|
)
|
|
}
|
|
return root
|
|
}
|
|
|
|
private func serializeNode(_ node: DTNode) -> Data {
|
|
var out = Data()
|
|
out.append(contentsOf: withUnsafeBytes(of: UInt32(node.properties.count).littleEndian) { Data($0) })
|
|
out.append(contentsOf: withUnsafeBytes(of: UInt32(node.children.count).littleEndian) { Data($0) })
|
|
|
|
for prop in node.properties {
|
|
var name = Data(prop.name.utf8)
|
|
if name.count >= 32 {
|
|
name = Data(name.prefix(31))
|
|
}
|
|
name.append(contentsOf: [UInt8](repeating: 0, count: 32 - name.count))
|
|
out.append(name)
|
|
|
|
out.append(contentsOf: withUnsafeBytes(of: UInt16(prop.length).littleEndian) { Data($0) })
|
|
out.append(contentsOf: withUnsafeBytes(of: prop.flags.littleEndian) { Data($0) })
|
|
out.append(prop.value)
|
|
|
|
let pad = Self.align4(prop.length) - prop.length
|
|
if pad > 0 {
|
|
out.append(Data(repeating: 0, count: pad))
|
|
}
|
|
}
|
|
|
|
for child in node.children {
|
|
out.append(serializeNode(child))
|
|
}
|
|
return out
|
|
}
|
|
|
|
private func serializePayload(_ root: DTNode) -> Data {
|
|
serializeNode(root)
|
|
}
|
|
|
|
// MARK: - Node Navigation
|
|
|
|
/// Get the "name" property value from a node.
|
|
private func nodeName(_ node: DTNode) -> String {
|
|
for prop in node.properties {
|
|
if prop.name == "name" {
|
|
return Self.decodeCString(prop.value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
/// Find a direct child node by name.
|
|
private func findChild(_ node: DTNode, name: String) throws -> DTNode {
|
|
for child in node.children {
|
|
if nodeName(child) == name {
|
|
return child
|
|
}
|
|
}
|
|
throw PatcherError.patchSiteNotFound("DeviceTree: missing child node '\(name)'")
|
|
}
|
|
|
|
/// Resolve a node path like ["device-tree", "buttons"] from the root.
|
|
private func resolveNode(_ root: DTNode, path: [String]) throws -> DTNode {
|
|
guard !path.isEmpty, path[0] == "device-tree" else {
|
|
throw PatcherError.patchSiteNotFound("DeviceTree: invalid node path \(path)")
|
|
}
|
|
var node = root
|
|
for name in path.dropFirst() {
|
|
node = try findChild(node, name: name)
|
|
}
|
|
return node
|
|
}
|
|
|
|
/// Find a property by name within a node.
|
|
private func findProperty(_ node: DTNode, name: String) throws -> DTProperty {
|
|
for prop in node.properties {
|
|
if prop.name == name {
|
|
return prop
|
|
}
|
|
}
|
|
throw PatcherError.patchSiteNotFound("DeviceTree: missing property '\(name)'")
|
|
}
|
|
|
|
// MARK: - Value Encoding
|
|
|
|
/// Encode a string value with null termination, padded/truncated to a fixed length.
|
|
private static func encodeFixedString(_ text: String, length: Int) -> Data {
|
|
var raw = Data(text.utf8)
|
|
raw.append(0) // null terminator
|
|
if raw.count > length {
|
|
return Data(raw.prefix(length))
|
|
}
|
|
raw.append(contentsOf: [UInt8](repeating: 0, count: length - raw.count))
|
|
return raw
|
|
}
|
|
|
|
/// Encode an integer value as little-endian bytes.
|
|
private static func encodeInteger(_ value: UInt64, length: Int) throws -> Data {
|
|
var data = Data(count: length)
|
|
switch length {
|
|
case 1:
|
|
data[0] = UInt8(value & 0xFF)
|
|
case 2:
|
|
let v = UInt16(value & 0xFFFF)
|
|
data.withUnsafeMutableBytes { $0.storeBytes(of: v.littleEndian, as: UInt16.self) }
|
|
case 4:
|
|
let v = UInt32(value & 0xFFFF_FFFF)
|
|
data.withUnsafeMutableBytes { $0.storeBytes(of: v.littleEndian, as: UInt32.self) }
|
|
case 8:
|
|
data.withUnsafeMutableBytes { $0.storeBytes(of: value.littleEndian, as: UInt64.self) }
|
|
default:
|
|
throw PatcherError.invalidFormat("DeviceTree: unsupported integer length \(length)")
|
|
}
|
|
return data
|
|
}
|
|
|
|
// MARK: - Patch Application
|
|
|
|
/// Apply all property patches and record each change.
|
|
private func applyPatches(root: DTNode) throws {
|
|
for patch in Self.propertyPatches {
|
|
let node = try resolveNode(root, path: patch.nodePath)
|
|
let prop = try findProperty(node, name: patch.property)
|
|
|
|
let originalBytes = Data(prop.value.prefix(patch.length))
|
|
|
|
let newValue: Data = switch patch.value {
|
|
case let .string(s):
|
|
Self.encodeFixedString(s, length: patch.length)
|
|
case let .integer(v):
|
|
try Self.encodeInteger(v, length: patch.length)
|
|
}
|
|
|
|
prop.length = patch.length
|
|
prop.flags = patch.flags
|
|
prop.value = newValue
|
|
|
|
let record = PatchRecord(
|
|
patchID: patch.patchID,
|
|
component: component,
|
|
fileOffset: prop.valueOffset,
|
|
virtualAddress: nil,
|
|
originalBytes: originalBytes,
|
|
patchedBytes: newValue,
|
|
description: patch.description
|
|
)
|
|
patches.append(record)
|
|
|
|
if verbose {
|
|
print(String(format: " 0x%06X: %@ → %@ [%@]",
|
|
prop.valueOffset,
|
|
originalBytes.hex,
|
|
newValue.hex,
|
|
patch.patchID))
|
|
}
|
|
}
|
|
}
|
|
}
|