From 2e0f049e811279aa00a009b3e88dddb4c32fffe0 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sat, 24 Oct 2026 01:30:24 -0400 Subject: [PATCH] tools: pass snap_i2c_fnkeys with cycle --input-fnkeys installed-fresh --input-fnkeys FLAGS (with --input) appends the guest boot-arg snap_i2c_fnkeys=FLAGS, which a SnapI2CHID built with SNAP_I2C_HID_FNKEYS (PROFILE=desktop-fnkeys) reads; other builds ignore it. The sets the kext would refuse are refused here: 0, bits above 0x1f, brightness encoding 3, the host Fn-lock (0x4) without translation (0x1), an encoding alone. Without the option the boot argument addition is unchanged. python3 tools/test-input-fnkeys.py: 4 tests OK; python3 tools/test-zone-log.py: 4 tests OK. --- tools/cycle.py | 6 ++++ tools/cycle_lib/installed_fresh.py | 10 ++++++ tools/test-input-fnkeys.py | 56 ++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tools/test-input-fnkeys.py diff --git a/tools/cycle.py b/tools/cycle.py index 67286f6..5f125bc 100755 --- a/tools/cycle.py +++ b/tools/cycle.py @@ -295,6 +295,12 @@ def parse(argv=None): p.add_argument('--input-active-us', type=int, default=None, help='with --present-latch: guest boot-arg snap_i2c_active_us, the poll period while touched ' '(default 4000; 0 = the configured 8 ms throughout)') + p.add_argument('--input-fnkeys', type=lambda v: int(v, 0), default=None, metavar='FLAGS', + help='with --input: guest boot-arg snap_i2c_fnkeys=FLAGS for a SnapI2CHID built with ' + 'SNAP_I2C_HID_FNKEYS (drivers/snap-i2c-hid PROFILE=desktop-fnkeys; others ignore it): 0x1 ' + 'the keyboard\'s ASUS hotkeys as macOS usages, 0x2 the rate-limited raw log of what the F-row ' + 'sends, 0x4 the host Fn-lock on Fn+Esc (needs 0x1), 0x8/0x10 the brightness encoding ' + '(hid-keyboard.h); give it to every mode of the attempt') p.add_argument('--hide-amx', action='store_true', help='clear the application AMX-version capability field; preserves kernel AMXIDR ' 'and does not expose SME; give it to every mode of the attempt') diff --git a/tools/cycle_lib/installed_fresh.py b/tools/cycle_lib/installed_fresh.py index abc5c3d..64e09c8 100644 --- a/tools/cycle_lib/installed_fresh.py +++ b/tools/cycle_lib/installed_fresh.py @@ -1164,6 +1164,16 @@ def boot_addition(args): if active_us is not None: need(active_us == 0 or 2000 <= active_us <= 100000, '--input-active-us must be 0 or 2000..100000') extra.append(f'snap_i2c_active_us={active_us}') + # --input-fnkeys: the keyboard's Fn-key layer (drivers/snap-i2c-hid/src/hid-keyboard.h HID_FNKEYS_*), read by a + # SnapI2CHID built with SNAP_I2C_HID_FNKEYS; the same sets the kext refuses are refused here. + fnkeys = getattr(args, 'input_fnkeys', None) + if fnkeys is not None: + need(getattr(args, 'input', False), '--input-fnkeys needs --input (the SnapI2CHID AuxKC)') + need(0 < fnkeys <= 0x1f and not fnkeys & ~0x1f, '--input-fnkeys must be 0x1..0x1f (hid-keyboard.h HID_FNKEYS_*)') + need((fnkeys >> 3) & 3 != 3, '--input-fnkeys: brightness encoding 3 (0x18) does not exist') + need(not fnkeys & 0x4 or fnkeys & 0x1, '--input-fnkeys: the host Fn-lock (0x4) needs translation (0x1)') + need(fnkeys & 0x3, '--input-fnkeys: give translation (0x1) or the raw log (0x2)') + extra.append(f'snap_i2c_fnkeys={fnkeys}') if zone is not None: need(ZONE_LOG_NAME.fullmatch(zone), f'--zone-log {zone!r}: XNU zone names are [A-Za-z0-9_.], at most 64') extra.append(f'zlog1={zone}') diff --git a/tools/test-input-fnkeys.py b/tools/test-input-fnkeys.py new file mode 100644 index 0000000..e2a6a41 --- /dev/null +++ b/tools/test-input-fnkeys.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Host test for cycle.py installed-fresh --input-fnkeys. No A16 access. + +Without the option the boot-argument addition is exactly what earlier attempts qualified; with it, it appends +snap_i2c_fnkeys=FLAGS (decimal, as XNU's boot-arg parser reads any number), and refuses what SnapI2CHID's +hid_fnkeys_init would refuse (drivers/snap-i2c-hid/src/hid-keyboard.h) and a run without the input AuxKC. +""" +import argparse +import importlib.util +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'tools')) +from cycle_lib import installed_fresh as fresh # noqa: E402 +from cycle_lib.common import Refused # noqa: E402 + +spec = importlib.util.spec_from_file_location('cycle_cli', ROOT / 'tools/cycle.py') +cli = importlib.util.module_from_spec(spec) +spec.loader.exec_module(cli) + +BASE = ' '.join([fresh.TIMESYNC_FLAG, fresh.COMPRESSOR_SW_FLAG]) + + +def ns(**kw): + return argparse.Namespace(**kw) + + +class InputFnKeys(unittest.TestCase): + def test_default_unchanged(self): + self.assertEqual(fresh.boot_addition(ns()), BASE) + self.assertEqual(fresh.boot_addition(ns(input=True, input_fnkeys=None)), BASE) + + def test_flags(self): + for flags in (0x1, 0x2, 0x3, 0x5, 0x7, 0x9, 0x13, 0x17): + self.assertEqual(fresh.boot_addition(ns(input=True, input_fnkeys=flags)), f'{BASE} snap_i2c_fnkeys={flags}') + self.assertEqual(fresh.boot_addition(ns(input=True, console=True, input_fnkeys=3)), + ' '.join([BASE, fresh.CONSOLE_FLAG, 'snap_i2c_fnkeys=3'])) + + def test_refusals(self): + for flags in (0, -1, 0x20, 0x100, 0x19, 0x1f, 0x4, 0x6, 0x8, 0x10): + with self.assertRaises(Refused, msg=hex(flags)): + fresh.boot_addition(ns(input=True, input_fnkeys=flags)) + with self.assertRaises(Refused): + fresh.boot_addition(ns(input_fnkeys=3)) + + def test_cli_reaches_args(self): + a = cli.parse(['installed-fresh', '--prefix', 'x', '--variant', 'fast', '--check', '--input', '--input-fnkeys', + '0x3']) + self.assertEqual(a.input_fnkeys, 3) + + +if __name__ == '__main__': + unittest.main()