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"]))