From 915ee5e401ccfb48a3753b35533cf6b77359b7ec Mon Sep 17 00:00:00 2001 From: Cipher-85 Date: Wed, 2 Sep 2026 21:30:40 +0800 Subject: [PATCH] fix: a pad reporting activeProfileId -1 took the app down on start A Creator Micro 2 over Bluetooth LE reports `activeProfileId: -1` in its keymap.json. The profile lookup only checked the upper bound, so -1 went straight into the subscript and trapped with "Index out of range" the moment the bridge started - which is the first click on the menu-bar icon, every time. Resolve the index in one place, falling back to the first profile for any id outside the range (what an oversized id already did), and use it from the three keymap lookups and the emulator. Two tests feed -1 and an oversized id through every path. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01EaYv7dSTLSNUJdZTsV3Fwv --- Sources/WLKit/KeymapManager.swift | 32 ++++++++++++++++------- Sources/WLKit/PadEmulator.swift | 7 +++-- Tests/WLKitTests/KeymapManagerTests.swift | 25 ++++++++++++++++++ 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/Sources/WLKit/KeymapManager.swift b/Sources/WLKit/KeymapManager.swift index 37d4ec7..ff86542 100644 --- a/Sources/WLKit/KeymapManager.swift +++ b/Sources/WLKit/KeymapManager.swift @@ -81,13 +81,25 @@ public enum KeymapManager { return config } + /// Which of `count` profiles `activeProfileId` names. The id is trusted + /// only when it is a real index: a Creator Micro 2 has been seen reporting + /// -1 here, and an upper-bound-only check let that straight through to a + /// subscript that took the app down the moment the bridge started. Out of + /// range in either direction falls back to the first profile, which is + /// what an oversized id always did. + static func activeProfileIndex(_ config: [String: Any], among count: Int) -> Int { + let id = config["activeProfileId"] as? Int ?? 0 + return (0.. [[String]]? { - let index = config["activeProfileId"] as? Int ?? 0 - guard let profiles = config["profiles"] as? [[String: Any]] else { return nil } - let profile = index < profiles.count ? profiles[index] : profiles.first - guard let layers = profile?["layers"] as? [[String: Any]], + guard let profiles = config["profiles"] as? [[String: Any]], !profiles.isEmpty else { + return nil + } + let profile = profiles[activeProfileIndex(config, among: profiles.count)] + guard let layers = profile["layers"] as? [[String: Any]], let layer = layers.first, let layout = layer["layout"] as? [String: Any], let keymap = layout["keymap"] as? [[String]] @@ -96,10 +108,11 @@ public enum KeymapManager { } static func activeLayerLayout(_ config: [String: Any]) -> [String: Any]? { - let index = config["activeProfileId"] as? Int ?? 0 - guard let profiles = config["profiles"] as? [[String: Any]] else { return nil } - let profile = index < profiles.count ? profiles[index] : profiles.first - guard let layers = profile?["layers"] as? [[String: Any]], + guard let profiles = config["profiles"] as? [[String: Any]], !profiles.isEmpty else { + return nil + } + let profile = profiles[activeProfileIndex(config, among: profiles.count)] + guard let layers = profile["layers"] as? [[String: Any]], let layer = layers.first else { return nil } return layer["layout"] as? [String: Any] @@ -142,11 +155,10 @@ public enum KeymapManager { /// joystick and lighting setting untouched. public static func withAgentKeymap(_ config: [String: Any]) throws -> [String: Any] { var next = config - let index = next["activeProfileId"] as? Int ?? 0 guard var profiles = next["profiles"] as? [[String: Any]], !profiles.isEmpty else { throw Failure.noProfiles } - let profileIndex = index < profiles.count ? index : 0 + let profileIndex = activeProfileIndex(next, among: profiles.count) var profile = profiles[profileIndex] guard var layers = profile["layers"] as? [[String: Any]], !layers.isEmpty else { throw Failure.noProfiles diff --git a/Sources/WLKit/PadEmulator.swift b/Sources/WLKit/PadEmulator.swift index 3413c95..0645c24 100644 --- a/Sources/WLKit/PadEmulator.swift +++ b/Sources/WLKit/PadEmulator.swift @@ -203,10 +203,9 @@ public final class PadEmulator: ObservableObject { dict.values.forEach(scan) } } - let index = keymap["activeProfileId"] as? Int ?? 0 - if let profiles = keymap["profiles"] as? [[String: Any]] { - let profile = index < profiles.count ? profiles[index] : profiles.first - if let layers = profile?["layers"] as? [[String: Any]], let layer = layers.first { + if let profiles = keymap["profiles"] as? [[String: Any]], !profiles.isEmpty { + let profile = profiles[KeymapManager.activeProfileIndex(keymap, among: profiles.count)] + if let layers = profile["layers"] as? [[String: Any]], let layer = layers.first { scan(layer) } } diff --git a/Tests/WLKitTests/KeymapManagerTests.swift b/Tests/WLKitTests/KeymapManagerTests.swift index ebf3042..ac8cebf 100644 --- a/Tests/WLKitTests/KeymapManagerTests.swift +++ b/Tests/WLKitTests/KeymapManagerTests.swift @@ -127,6 +127,31 @@ final class KeymapManagerTests: XCTestCase { XCTAssertNotNil(layers[0]["lights"], "per-layer lighting survives") } + /// A Creator Micro 2 has been seen reporting `activeProfileId: -1`. The + /// old check only caught ids that were too large, so -1 went straight into + /// a subscript and took the app down the moment the bridge started. + func testNegativeActiveProfileIdFallsBackToTheFirstProfile() throws { + var config = try backupKeymap() + config["activeProfileId"] = -1 + + XCTAssertEqual(KeymapManager.activeLayerKeymap(config)?[0], ["KC_F13", "KC_F14"]) + XCTAssertNotNil(KeymapManager.activeLayerLayout(config)) + XCTAssertFalse(KeymapManager.isAgentKeymapApplied(config)) + + let next = try KeymapManager.withAgentKeymap(config) + XCTAssertTrue(KeymapManager.isAgentKeymapApplied(next)) + XCTAssertEqual(next["activeProfileId"] as? Int, -1, + "the id is the pad's to own; only the lookup is clamped") + } + + func testOversizedActiveProfileIdFallsBackToTheFirstProfile() throws { + var config = try backupKeymap() + config["activeProfileId"] = 99 + + XCTAssertEqual(KeymapManager.activeLayerKeymap(config)?[0], ["KC_F13", "KC_F14"]) + XCTAssertTrue(KeymapManager.isAgentKeymapApplied(try KeymapManager.withAgentKeymap(config))) + } + func testMalformedInputIsRejectedRatherThanSilentlyAccepted() { XCTAssertThrowsError(try KeymapManager.parse(["nope": 1])) XCTAssertThrowsError(try KeymapManager.parse(["data": "not json"]))