mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
205 lines
9.1 KiB
Python
205 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Preview or explicitly save per-prefix IOKit platform overrides; defaults stay unchanged."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import plistlib
|
|
import shlex
|
|
import tempfile
|
|
import uuid
|
|
from xml.parsers.expat import ExpatError
|
|
|
|
MAX_SIZE = 65536
|
|
STRINGS = {'IOPlatformSerialNumber': 64, 'manufacturer': 128, 'model': 128,
|
|
'product-name': 128, 'board-id': 128, 'mlb-serial-number': 64}
|
|
BYTES = {'ROM': 6, 'system-id': 16}
|
|
ALIASES = {
|
|
'SystemProductName': 'model', 'Type': 'model', 'SMBIOS_MODEL': 'model',
|
|
'SMBIOS_PRODUCT_NAME': 'product-name', 'SMBIOS_MANUFACTURER': 'manufacturer',
|
|
'SMBIOS_BOARD_ID': 'board-id', 'SystemSerialNumber': 'IOPlatformSerialNumber',
|
|
'Serial': 'IOPlatformSerialNumber', 'SMBIOS_SERIAL_NUMBER': 'IOPlatformSerialNumber',
|
|
'SystemUUID': 'IOPlatformUUID', 'SmUUID': 'IOPlatformUUID', 'SMBIOS_PLATFORM_UUID': 'IOPlatformUUID',
|
|
'MLB': 'mlb-serial-number', 'Board Serial': 'mlb-serial-number', 'SMBIOS_MLB': 'mlb-serial-number',
|
|
'SMBIOS_ROM': 'ROM', 'SMBIOS_SYSTEM_ID': 'system-id',
|
|
}
|
|
|
|
|
|
def validate(properties):
|
|
if not isinstance(properties, dict) or len(properties) > 9:
|
|
raise ValueError('properties must be a dictionary of supported identity fields')
|
|
result = {}
|
|
for key, value in properties.items():
|
|
if key in STRINGS:
|
|
if not isinstance(value, str) or not 1 <= len(value) <= STRINGS[key] or any(not 32 <= ord(c) <= 126 for c in value):
|
|
raise ValueError('Invalid printable string for ' + key)
|
|
elif key == 'IOPlatformUUID':
|
|
if not isinstance(value, str):
|
|
raise ValueError('IOPlatformUUID must be a UUID string')
|
|
parsed = uuid.UUID(value)
|
|
if not parsed.int:
|
|
raise ValueError('IOPlatformUUID must not be zero')
|
|
value = str(parsed).upper()
|
|
elif key in BYTES:
|
|
if isinstance(value, str):
|
|
value = bytes.fromhex(value.replace(':', '').replace('-', ''))
|
|
if not isinstance(value, bytes) or len(value) != BYTES[key]:
|
|
raise ValueError(key + ' has an invalid byte length')
|
|
else:
|
|
raise ValueError('Unsupported identity field: ' + str(key))
|
|
result[key] = value
|
|
if 'model' in result:
|
|
result.setdefault('product-name', result['model'])
|
|
if 'IOPlatformUUID' in result:
|
|
system_id = uuid.UUID(result['IOPlatformUUID']).bytes_le
|
|
if 'system-id' in result and result['system-id'] != system_id:
|
|
raise ValueError('system-id does not match IOPlatformUUID')
|
|
result['system-id'] = system_id
|
|
return result
|
|
|
|
|
|
def load(source):
|
|
with Path(source).open("rb") as stream:
|
|
raw = stream.read(MAX_SIZE + 1)
|
|
if len(raw) > MAX_SIZE:
|
|
raise ValueError('Identity input exceeds 64 KiB')
|
|
if str(source).endswith('.conf'):
|
|
payload = {}
|
|
for line in raw.decode().splitlines():
|
|
if not line.strip() or line.lstrip().startswith('#'):
|
|
continue
|
|
key, sep, value = line.partition('=')
|
|
if not sep:
|
|
raise ValueError('Malformed legacy identity line')
|
|
values = shlex.split(value, comments=True)
|
|
if len(values) != 1 or key.strip() in payload:
|
|
raise ValueError('Malformed or duplicate legacy identity field')
|
|
payload[key.strip()] = values[0]
|
|
else:
|
|
def unique_keys(pairs):
|
|
result = {}
|
|
for key, value in pairs:
|
|
if key in result: raise ValueError('Duplicate JSON identity field: ' + key)
|
|
result[key] = value
|
|
return result
|
|
if raw.lstrip()[:1] in (b'{', b'['):
|
|
payload = json.loads(raw, object_pairs_hook=unique_keys)
|
|
else:
|
|
payload = plistlib.loads(raw)
|
|
if not isinstance(payload, dict):
|
|
raise ValueError('Identity input must be a dictionary')
|
|
if 'schema' in payload:
|
|
if set(payload) != {'schema', 'properties'} or type(payload['schema']) is not int or payload['schema'] != 1:
|
|
raise ValueError('Unsupported platform identity schema')
|
|
return validate(payload['properties'])
|
|
if 'PlatformInfo' in payload:
|
|
payload = payload['PlatformInfo'].get('Generic') if isinstance(payload['PlatformInfo'], dict) else None
|
|
elif 'Generic' in payload:
|
|
payload = payload['Generic']
|
|
if not isinstance(payload, dict):
|
|
raise ValueError('Missing OpenCore PlatformInfo/Generic dictionary')
|
|
result = {}
|
|
for key, value in payload.items():
|
|
name = ALIASES.get(key, key)
|
|
if name not in {*STRINGS, *BYTES, 'IOPlatformUUID'}:
|
|
continue # OpenCore includes unrelated boot, firmware and CPU settings.
|
|
if name in result and result[name] != value:
|
|
raise ValueError('Conflicting imports for ' + name)
|
|
result[name] = value
|
|
if not result:
|
|
raise ValueError('No supported identity fields found')
|
|
return validate(result)
|
|
|
|
|
|
def destination(prefix):
|
|
prefix = Path(prefix).expanduser()
|
|
if not prefix.is_absolute():
|
|
raise ValueError('Prefix must be absolute')
|
|
path = prefix / 'private/etc/darling-platform.plist'
|
|
if path.is_symlink() or not path.resolve().is_relative_to(prefix.resolve()):
|
|
raise ValueError('Identity destination must remain inside the selected prefix')
|
|
return path
|
|
|
|
|
|
def save(prefix, properties):
|
|
path = destination(prefix)
|
|
prefix = Path(prefix)
|
|
# A host write behind Darling's mounted overlay may not be visible. Never
|
|
# stop a user's session implicitly or write a second copy behind its back.
|
|
marker = prefix / '.init.pid'
|
|
if marker.exists():
|
|
try:
|
|
text = marker.read_text().strip()
|
|
if not text.isdecimal() or int(text) <= 1:
|
|
raise ValueError('Invalid .init.pid; verify the selected prefix is stopped before writing')
|
|
pid = int(text)
|
|
os.kill(pid, 0)
|
|
except ProcessLookupError:
|
|
pass
|
|
else:
|
|
raise ValueError('Shut down the selected Darling prefix before writing platform identity')
|
|
encoded = plistlib.dumps({'schema': 1, 'properties': validate(properties)}, sort_keys=True)
|
|
if path.exists() and path.read_bytes() == encoded:
|
|
return False
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
# Recheck after creating parents, before opening a temporary file.
|
|
path = destination(prefix)
|
|
if path.exists():
|
|
load(path) # refuse to silently overwrite an unknown schema
|
|
backup = path.with_name(path.name + '.before-platform-change')
|
|
if not backup.exists():
|
|
fd = os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
with os.fdopen(fd, 'wb') as stream:
|
|
stream.write(path.read_bytes())
|
|
fd, temporary = tempfile.mkstemp(prefix='.darling-platform-', dir=path.parent)
|
|
try:
|
|
with os.fdopen(fd, 'wb') as stream:
|
|
stream.write(encoded)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
os.replace(temporary, path)
|
|
finally:
|
|
Path(temporary).unlink(missing_ok=True)
|
|
return True
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--prefix', type=Path)
|
|
parser.add_argument('--import', dest='source', type=Path, help='OpenCore plist, GenSMBIOS JSON, legacy .conf, or schema-1 plist/JSON')
|
|
parser.add_argument('--set', action='append', default=[], metavar='KEY=VALUE')
|
|
parser.add_argument('--write', action='store_true', help='Write validated overrides to a stopped prefix (otherwise preview only)')
|
|
args = parser.parse_args(argv)
|
|
config = Path(__file__).resolve().parents[1] / 'build/config.json'
|
|
saved = json.loads(config.read_text()) if config.exists() else {}
|
|
prefix = args.prefix or Path(os.environ.get('DPREFIX') or saved.get('prefix', str(Path.home() / '.darling')))
|
|
try:
|
|
path = destination(prefix)
|
|
properties = load(path) if path.exists() else {}
|
|
if args.source:
|
|
incoming = load(args.source)
|
|
# Replacing a UUID also replaces its previously derived byte form.
|
|
if 'IOPlatformUUID' in incoming:
|
|
properties.pop('system-id', None)
|
|
properties.update(incoming)
|
|
for assignment in args.set:
|
|
key, sep, value = assignment.partition('=')
|
|
if not sep:
|
|
raise ValueError('--set requires KEY=VALUE')
|
|
if key == 'IOPlatformUUID': properties.pop('system-id', None)
|
|
if key == 'model': properties.pop('product-name', None)
|
|
properties[key] = value
|
|
properties = validate(properties)
|
|
if args.write:
|
|
changed = save(prefix, properties)
|
|
print('Platform identity ' + ('saved' if changed else 'unchanged') + '; restart iokitd to apply.')
|
|
else:
|
|
print(json.dumps({'schema': 1, 'properties': properties}, default=lambda value: value.hex().upper(), indent=2))
|
|
except (OSError, ValueError, TypeError, plistlib.InvalidFileException, ExpatError) as error:
|
|
parser.exit(1, str(error) + '\n')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|