#include "core/controller_identity.h" #include "profile/controller_profile.h" #include "controller_profile_legacy_fixtures.h" #include #include #include namespace { uint8_t encoded_database[CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE]{}; ControllerProfileDatabase database{}; ControllerProfileDatabase decoded_database{}; void require(bool condition, const char* message) { if (!condition) { std::cerr << message << '\n'; std::exit(1); } } ControllerIdentity identity(uint8_t suffix) { ControllerIdentity value{}; value.stable = true; value.transport = ControllerTransport::kClassic; value.address[5] = suffix; value.vendor_id = 0x057e; value.product_id = static_cast(0x2000u + suffix); return value; } bool read_encoded_database(void*, size_t offset, uint8_t* output, size_t size) { if (offset > sizeof(encoded_database) || size > sizeof(encoded_database) - offset) { return false; } memcpy(output, &encoded_database[offset], size); return true; } void require_no_new_swing_actions(const ControllerProfile& profile) { require(profile.swing.macro == CONTROLLER_PROFILE_NO_BUTTON && profile.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON && profile.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON && profile.nunchuk_swing.sensitivity == 1 && profile.nunchuk_swing.modifier == CONTROLLER_PROFILE_NO_BUTTON && profile.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON && profile.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON && profile.combined_swing.modifier == CONTROLLER_PROFILE_NO_BUTTON && profile.combination_window_ms == 100, "legacy profile enabled a new gesture action or lost its defaults"); } void test_pair_identity_wire_and_member_validation() { ControllerIdentity left = identity(1); left.transport = ControllerTransport::kBle; left.product_id = 0x2067; const uint8_t left_address[6] = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15}; memcpy(left.address, left_address, sizeof(left_address)); ControllerIdentity right = left; right.product_id = 0x2066; right.address_type = 1; right.address[0] = 0xc0; ControllerIdentity pair{}; uint8_t encoded[CONTROLLER_IDENTITY_ENCODED_SIZE]{}; const uint8_t expected[14] = { 5, 3, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0xc0, 0x11, 0x12, 0x13, 0x14, 0x15}; require(controller_identity_make_joycon_pair(left, right, &pair) && controller_identity_encode(pair, encoded, sizeof(encoded)) && memcmp(encoded, expected, sizeof(expected)) == 0, "pair identity did not encode both typed members"); ControllerIdentity decoded{}; ControllerIdentity decoded_left{}; ControllerIdentity decoded_right{}; require(controller_identity_decode(encoded, sizeof(encoded), &decoded) && controller_identity_equal(pair, decoded) && controller_identity_joycon_pair_members( decoded, &decoded_left, &decoded_right) && controller_identity_equal(left, decoded_left) && controller_identity_equal(right, decoded_right), "pair identity round trip lost a member or its model"); decoded.partner_address[5] ^= 1; require(!controller_identity_equal(pair, decoded), "pair equality ignored the right address"); for (uint8_t flags = 0; flags < 16; ++flags) { if (flags == 1 || flags == 5) { continue; } memcpy(encoded, expected, sizeof(encoded)); encoded[0] = flags; require(!controller_identity_decode(encoded, sizeof(encoded), &decoded), "pair decoder accepted unstable, reserved, or nonstatic flags"); } require(!controller_identity_make_joycon_pair(right, left, &decoded), "pair helper accepted reversed models"); right.product_id = 0x2069; require(!controller_identity_make_joycon_pair(left, right, &decoded), "pair helper accepted a non-Joy-Con member"); right.product_id = 0x2066; right.address_type = 2; require(!controller_identity_make_joycon_pair(left, right, &decoded), "pair helper accepted a noncanonical identity address type"); right.address_type = left.address_type; memcpy(right.address, left.address, sizeof(right.address)); require(!controller_identity_make_joycon_pair(left, right, &decoded), "pair helper accepted one physical typed address twice"); left.address[0] = right.address[0] = 0xc0; right.address_type = 1; require(controller_identity_make_joycon_pair(left, right, &decoded), "pair helper conflated public and static-random address namespaces"); left.partner_address[0] = 1; require(!controller_identity_encode(left, encoded, sizeof(encoded)) && !controller_identity_make_joycon_pair(left, right, &decoded), "ordinary member accepted stray partner fields"); left.partner_address[0] = 0; const uint8_t ordinary[14] = { 1, 2, 0, 0, 0xc0, 0x11, 0x12, 0x13, 0x14, 0x15, 0x7e, 0x05, 0x67, 0x20}; require(controller_identity_encode(left, encoded, sizeof(encoded)) && memcmp(encoded, ordinary, sizeof(encoded)) == 0 && controller_identity_decode(encoded, sizeof(encoded), &decoded) && controller_identity_equal(left, decoded), "ordinary identity wire encoding changed"); const uint8_t global[14]{}; require(controller_identity_encode(controller_identity_global(), encoded, sizeof(encoded)) && memcmp(encoded, global, sizeof(encoded)) == 0, "global identity wire encoding changed"); } void test_profile_wire_schema() { const ControllerProfile profile = controller_profile_default(controller_identity_global(), 0); uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_encode(profile, encoded, sizeof(encoded)), "default profile did not encode"); require(encoded[0] == 9 && encoded[1] == 0 && encoded[2] == 0x80 && encoded[3] == 1, "profile header is not little-endian v9/384"); for (uint8_t index = 0; index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) { require(encoded[4 + index] == index, "default direct mapping is not identity"); } require(encoded[26] == 0xff && encoded[27] == 0x7f && encoded[30] == 0, "default stick encoding changed"); require(encoded[54] == 0xff && encoded[55] == 0xff && encoded[58] == static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) && encoded[59] == static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8), "default trigger encoding changed"); require(encoded[60] == CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL && encoded[61] == 0 && encoded[70] == CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL && encoded[71] == 0, "default trigger mappings are not identity"); require(encoded[72] == 0xff && encoded[73] == 0xff && encoded[74] == 3 && encoded[75] == 0 && encoded[78] == 0 && encoded[79] == 0, "default rumble or action encoding changed"); require(encoded[96] == 0 && encoded[97] == 0 && encoded[98] == 0x7c && encoded[99] == 0 && encoded[100] == 0 && encoded[101] == 0, "default sparse macro descriptor changed"); ControllerProfile decoded{}; require(controller_profile_decode(encoded, sizeof(encoded), &decoded), "default profile did not decode"); ControllerProfile action_profile = profile; action_profile.switching_chord = (1u << CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL) | (1u << static_cast( ControllerProfileLogicalButton::kSouth)); action_profile.motion_toggle_chord = (1u << CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL); action_profile.macros[0].trigger_mask = (1u << CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL) | (1u << CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL); action_profile.macros[0].cancel_control = CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL; action_profile.macros[0].first_step = 0; action_profile.macros[0].step_count = 1; action_profile.macro_step_count = 1; action_profile.macro_steps[0].override_flags = kControllerProfileOverrideButtons; for (uint8_t macro_index = 1; macro_index < CONTROLLER_PROFILE_MACRO_COUNT; ++macro_index) { action_profile.macros[macro_index].first_step = 1; } action_profile.macro_steps[0].duration_ms = 25; action_profile.macro_steps[0].output_button_mask = 1; uint8_t action_encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_encode( action_profile, action_encoded, sizeof(action_encoded)), "sparse action profile did not encode"); require(action_encoded[75] == 0x21, "sparse action extension encoding changed"); require(action_encoded[96] == 0 && action_encoded[97] == 0 && action_encoded[98] == 0x43, "sparse macro binding encoding changed"); require(action_encoded[100] == 1 && action_encoded[101] == 5, "sparse macro step bounds changed"); require(controller_profile_decode( action_encoded, sizeof(action_encoded), &decoded), "sparse action profile did not decode"); require(decoded.switching_chord == action_profile.switching_chord && decoded.motion_toggle_chord == action_profile.motion_toggle_chord && decoded.macros[0].trigger_mask == action_profile.macros[0].trigger_mask && decoded.macros[0].cancel_control == action_profile.macros[0].cancel_control && decoded.macros[0].step_count == 1, "sparse trigger-backed macro did not round-trip"); encoded[252] = 1; require(!controller_profile_decode(encoded, sizeof(encoded), &decoded), "nonzero reserved profile byte was accepted"); ControllerProfile invalid = profile; invalid.button_map[0] = CONTROLLER_PROFILE_FIRST_EXTRA_CONTROL; require(!controller_profile_validate(invalid), "invalid direct output was accepted"); invalid = profile; invalid.sticks[0].inner_deadzone = invalid.sticks[0].outer_saturation; require(!controller_profile_validate(invalid), "empty stick range was accepted"); ControllerProfile boundary = profile; boundary.triggers[0].lower_deadzone = 30000; boundary.triggers[0].upper_saturation = 40000; boundary.triggers[0].digital_threshold = 0; uint8_t boundary_encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_encode(boundary, boundary_encoded, sizeof(boundary_encoded)) && controller_profile_decode(boundary_encoded, sizeof(boundary_encoded), &decoded) && decoded.triggers[0].digital_threshold == 0, "current profile rejected zero transformed trigger threshold"); boundary.triggers[0].digital_threshold = UINT16_MAX; require(controller_profile_encode(boundary, boundary_encoded, sizeof(boundary_encoded)) && controller_profile_decode(boundary_encoded, sizeof(boundary_encoded), &decoded) && decoded.triggers[0].digital_threshold == UINT16_MAX, "current profile rejected maximum transformed trigger threshold"); invalid = profile; invalid.triggers[0].lower_deadzone = invalid.triggers[0].upper_saturation; require(!controller_profile_validate(invalid), "empty raw trigger range was accepted"); invalid.triggers[0].lower_deadzone = UINT16_MAX; invalid.triggers[0].upper_saturation = UINT16_MAX - 1; require(!controller_profile_validate(invalid), "reversed raw trigger range was accepted"); invalid = profile; invalid.turbo_modes[0] = static_cast(4); require(!controller_profile_validate(invalid), "invalid Turbo mode was accepted"); invalid = profile; invalid.macros[0].first_step = 0; invalid.macros[0].step_count = 1; invalid.macro_step_count = 1; invalid.macro_steps[0].duration_ms = CONTROLLER_PROFILE_MAX_WAIT_MS + 1; require(!controller_profile_validate(invalid), "unbounded macro wait was accepted"); invalid.macro_steps[0].duration_ms = CONTROLLER_PROFILE_MAX_WAIT_MS; invalid.macros[1].first_step = 0; require(!controller_profile_validate(invalid), "noncanonical shared macro pool was accepted"); } void test_legacy_profile_migration() { ControllerProfile migrated{}; require(controller_profile_decode( kLegacyDefaultProfile, sizeof(kLegacyDefaultProfile), &migrated), "legacy default profile did not decode"); require(migrated.swing.button == CONTROLLER_PROFILE_NO_BUTTON && migrated.swing.sensitivity == 1 && migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON, "legacy padding enabled a swing gesture"); require_no_new_swing_actions(migrated); require(migrated.triggers[0].digital_threshold == CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD && migrated.triggers[1].digital_threshold == CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD, "legacy inherited thresholds were not migrated"); uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_encode(migrated, encoded, sizeof(encoded)), "migrated default profile did not encode"); require(migrated.macros[0].step_count == 0 && migrated.macro_step_count == 0, "legacy end marker was not removed during migration"); require(encoded[0] == CONTROLLER_PROFILE_SCHEMA_VERSION && encoded[58] == static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) && encoded[59] == static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8) && encoded[68] == static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) && encoded[69] == static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8) && encoded[60] == CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL && encoded[70] == CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL, "migrated default profile did not encode as the current schema"); require(controller_profile_decode( kLegacyNarrowRawRangeProfile, sizeof(kLegacyNarrowRawRangeProfile), &migrated), "legacy narrow-raw-range profile did not decode"); require(migrated.triggers[0].lower_deadzone == 30000 && migrated.triggers[0].upper_saturation == 40000 && migrated.triggers[0].digital_threshold == CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD && migrated.triggers[1].lower_deadzone == 30000 && migrated.triggers[1].upper_saturation == 40000 && migrated.triggers[1].digital_threshold == CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD, "legacy narrow raw range or inherited threshold was not migrated"); require(controller_profile_encode(migrated, encoded, sizeof(encoded)), "migrated narrow-raw-range profile did not encode"); require(controller_profile_decode( kLegacyCustomThresholdProfile, sizeof(kLegacyCustomThresholdProfile), &migrated), "legacy custom-threshold profile did not decode"); require(migrated.triggers[0].digital_threshold == 0x1234 && migrated.triggers[1].digital_threshold == 0xabcd, "legacy custom thresholds were not preserved"); require(migrated.triggers[0].output == CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL && migrated.triggers[1].output == CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL, "legacy trigger identity mappings were not restored"); require(controller_profile_encode(migrated, encoded, sizeof(encoded)), "legacy custom-threshold profile did not re-encode"); const uint8_t legacy_macro_trigger = kLegacyCustomThresholdProfile[78]; const uint32_t expected_macro_trigger = legacy_macro_trigger < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT ? static_cast(1u << legacy_macro_trigger) : 0; require(migrated.macros[0].trigger_mask == expected_macro_trigger && migrated.macros[0].cancel_control == kLegacyCustomThresholdProfile[79], "legacy macro trigger was not migrated to descriptor zero"); uint8_t previous_encoded[CONTROLLER_PROFILE_LEGACY_ENCODED_SIZE]{}; memcpy(previous_encoded, kLegacyDefaultProfile, sizeof(previous_encoded)); previous_encoded[0] = static_cast( CONTROLLER_PROFILE_TRIGGER_THRESHOLD_SCHEMA_VERSION); previous_encoded[58] = static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD); previous_encoded[59] = static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8); previous_encoded[68] = static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD); previous_encoded[69] = static_cast( CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8); previous_encoded[78] = static_cast( ControllerProfileLogicalButton::kSouth); previous_encoded[79] = static_cast( ControllerProfileLogicalButton::kCapture); require(controller_profile_decode( previous_encoded, sizeof(previous_encoded), &migrated) && migrated.triggers[0].output == CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL && migrated.triggers[1].output == CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL && migrated.macros[0].trigger_mask == static_cast( 1u << static_cast( ControllerProfileLogicalButton::kSouth)) && migrated.macros[0].cancel_control == static_cast( ControllerProfileLogicalButton::kCapture) && migrated.motion_toggle_chord == 0, "v2 profile controls did not migrate to the current schema"); previous_encoded[80] = 2; previous_encoded[100] = 0; previous_encoded[101] = kControllerProfileOverrideButtons; previous_encoded[102] = 25; previous_encoded[104] = 1; require(controller_profile_decode( previous_encoded, sizeof(previous_encoded), &migrated) && migrated.macros[0].step_count == 1 && migrated.macro_steps[0].output_button_mask == 1, "legacy nonempty macro did not migrate into shared pool"); previous_encoded[60] = CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL; previous_encoded[70] = CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL; previous_encoded[76] = 3; previous_encoded[78] = 1; previous_encoded[79] = 0; previous_encoded[81] = 11; previous_encoded[98] = 4; for (uint8_t version = 3; version <= 4; ++version) { previous_encoded[0] = version; previous_encoded[75] = version == 4 ? 0x21 : 0; migrated.swing = {2, 2, 24}; migrated.nunchuk_swing = {0, 0, 0}; migrated.combined_swing = {15, CONTROLLER_PROFILE_NO_BUTTON, 24}; migrated.combination_window_ms = 30; require(controller_profile_decode( previous_encoded, sizeof(previous_encoded), &migrated) && migrated.triggers[0].output == CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL && migrated.triggers[1].output == CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL && migrated.switching_chord == (3u | (version == 4 ? 1u << 16 : 0u)) && migrated.motion_toggle_chord == (4u | (version == 4 ? 1u << 17 : 0u)) && migrated.macros[0].trigger_mask == 1 && migrated.macros[0].cancel_control == 11 && migrated.macros[0].step_count == 1 && migrated.macro_steps[0].output_button_mask == 1 && migrated.swing.button == CONTROLLER_PROFILE_NO_BUTTON && migrated.swing.sensitivity == 1 && migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON, "legacy control mapping or action migration lost settings"); require_no_new_swing_actions(migrated); } ControllerProfile current = controller_profile_default(controller_identity_global(), 0); current.triggers[0].digital_threshold = 0x8000; require(controller_profile_encode(current, encoded, sizeof(encoded)) && controller_profile_decode(encoded, sizeof(encoded), &migrated) && migrated.triggers[0].digital_threshold == 0x8000, "v2 custom threshold matching the legacy default was migrated"); } void test_database_round_trip_and_capacity() { controller_profile_database_default(&database); for (uint8_t index = 0; index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY; ++index) { ControllerProfileDatabaseEntry* entry = nullptr; require(controller_profile_database_ensure( &database, identity(static_cast(index + 1)), &entry) == ControllerProfileDatabaseResult::kOk && entry != nullptr, "stable identity was not added"); entry->active_profile = index % CONTROLLER_PROFILE_COUNT; } ControllerProfileDatabaseEntry* rejected = nullptr; require(controller_profile_database_ensure( &database, identity(99), &rejected) == ControllerProfileDatabaseResult::kFull, "seventeenth stable identity was not rejected"); require(controller_profile_database_find(database, identity(1)) != nullptr, "full-table rejection evicted an existing identity"); for (size_t offset = 0; offset < sizeof(encoded_database); offset += CONTROLLER_PROFILE_ENCODED_SIZE) { const size_t size = sizeof(encoded_database) - offset < CONTROLLER_PROFILE_ENCODED_SIZE ? sizeof(encoded_database) - offset : CONTROLLER_PROFILE_ENCODED_SIZE; require(controller_profile_database_encode_range( database, offset, &encoded_database[offset], size), "database range did not encode"); } require(encoded_database[4] == CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION && encoded_database[5] == 0, "database encoder did not emit v3"); require(controller_profile_database_decode( read_encoded_database, nullptr, &decoded_database), "database did not decode"); require(controller_profile_database_find( decoded_database, identity(16)) != nullptr, "last database identity did not round trip"); encoded_database[12] = 1; require(!controller_profile_database_decode( read_encoded_database, nullptr, &decoded_database), "nonzero database header reservation was accepted"); } void test_set_b_sparse_extension_and_migration() { ControllerProfile profile = controller_profile_default(controller_identity_global(), 0); profile.shortcuts.modifier = 16; profile.shortcuts.selectors[0] = 0; profile.shortcuts.selectors[7] = 15; profile.shift.mode = ControllerProfileShiftMode::kToggle; profile.shift.modifier = 17; profile.shift.button_map[0] = CONTROLLER_PROFILE_NO_BUTTON; profile.turbo_modes[15] = ControllerProfileTurboMode::kBurst; profile.turbo_defaults = {30, 99, 255}; profile.turbo_override_mask = (1u << 2) | (1u << 15); profile.turbo_overrides[2] = {1, 1, 1}; profile.turbo_overrides[15] = {23, 37, 17}; profile.turbo_overrides[0] = {0, 0, 0}; profile.macros[0] = {1, 0xff, 0, 8, ControllerProfileMacroMode::kRepeat, 255}; profile.macro_step_count = 8; for (uint8_t macro = 1; macro < CONTROLLER_PROFILE_MACRO_COUNT; ++macro) { profile.macros[macro].first_step = 8; profile.macros[macro].mode = static_cast(macro - 1); } for (uint8_t step = 0; step < 8; ++step) { profile.macro_steps[step] = {31, 25, 1, -2, 3, -4, 5, 0x1234, 0xabcd}; } uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; ControllerProfile decoded{}; require(controller_profile_encode(profile, encoded, sizeof(encoded)) && controller_profile_decode(encoded, sizeof(encoded), &decoded), "full sparse stream and Set B extension did not round trip"); require(encoded[254] == 0xcd && encoded[255] == 0xab && encoded[256] == 16 && encoded[264] == 15 && encoded[265] == 2 && encoded[266] == 17 && encoded[267] == 0xff && encoded[283] == 30 && encoded[286] == 4 && encoded[287] == 0x80 && encoded[288] == 1 && encoded[291] == 23 && encoded[336] == 3 && encoded[337] == 255, "Set B fields overlap macro data or use wrong sparse ordering"); require(decoded.turbo_overrides[15].duty_percent == 37 && decoded.macros[0].repeat_count == 255 && decoded.shortcuts.selectors[7] == 15, "Set B extension settings were not decoded"); for (size_t offset = 294; offset < 336; ++offset) { require(encoded[offset] == 0, "absent override was not canonical zero"); } encoded[294] = 1; require(!controller_profile_decode(encoded, sizeof(encoded), &decoded), "nonzero sparse Turbo padding was accepted"); encoded[294] = 0; encoded[383] = 1; require(!controller_profile_decode(encoded, sizeof(encoded), &decoded), "nonzero extension reservation was accepted"); encoded[383] = 0; encoded[337] = 0; require(!controller_profile_decode(encoded, sizeof(encoded), &decoded), "zero macro repeat count was accepted"); encoded[337] = 255; uint8_t legacy[CONTROLLER_PROFILE_LEGACY_ENCODED_SIZE]{}; memcpy(legacy, encoded, sizeof(legacy)); legacy[0] = 5; legacy[2] = 0; legacy[95] = 2; require(controller_profile_decode(legacy, sizeof(legacy), &decoded) && decoded.macros[0].step_count == 8 && decoded.macro_steps[7].right_trigger == 0xabcd && decoded.macros[0].mode == ControllerProfileMacroMode::kOnce && decoded.shortcuts.modifier == CONTROLLER_PROFILE_NO_BUTTON && decoded.turbo_override_mask == 0 && decoded.swing.button == CONTROLLER_PROFILE_NO_BUTTON && decoded.swing.sensitivity == 1 && decoded.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON, "schema5 full136-byte macro stream did not migrate"); require_no_new_swing_actions(decoded); require(controller_profile_encode(decoded, encoded, sizeof(encoded)) && memcmp(&legacy[4], &encoded[4], sizeof(legacy) - 4) == 0, "schema5 migration changed existing profile data"); legacy[0] = 6; require(!controller_profile_decode(legacy, sizeof(legacy), &decoded), "schema6 accepted a legacy-sized payload"); profile.shortcuts.selectors[7] = 0; require(!controller_profile_validate(profile), "duplicate shortcut was accepted"); profile.shortcuts.selectors[7] = 15; profile.shift.button_map[0] = 16; require(!controller_profile_validate(profile), "analog Shift output was accepted"); profile.shift.button_map[0] = 0; profile.turbo_overrides[2].rate_hz = 31; require(!controller_profile_validate(profile), "out-of-range Turbo rate was accepted"); profile.turbo_overrides[2].rate_hz = 1; for (uint8_t step = 0; step < 8; ++step) { profile.macro_steps[step].duration_ms = 0; } require(!controller_profile_validate(profile), "zero-duration looping macro was accepted"); profile.macros[0].mode = ControllerProfileMacroMode::kOnce; require(controller_profile_validate(profile), "zero-duration once macro was rejected"); } void test_legacy_database_strides() { for (uint8_t version = 1; version <= 2; ++version) { const uint8_t count = version == 1 ? 4 : 8; const size_t entry_size = 16 + count * 256; const size_t entries_offset = 32 + count * 256; const size_t total_size = entries_offset + 16 * entry_size; memset(encoded_database, 0, sizeof(encoded_database)); memcpy(encoded_database, "SPDB", 4); encoded_database[4] = version; encoded_database[6] = static_cast(total_size); encoded_database[7] = static_cast(total_size >> 8); encoded_database[8] = 16; encoded_database[9] = count; encoded_database[10] = count - 1; encoded_database[11] = 1; for (uint8_t index = 0; index < count; ++index) { memcpy(&encoded_database[32 + index * 256], kLegacyDefaultProfile, 256); } const size_t last_entry = entries_offset + 15 * entry_size; require(controller_identity_encode(identity(16), &encoded_database[last_entry], 14), "legacy database identity did not encode"); encoded_database[last_entry + 14] = count - 1; encoded_database[last_entry + 15] = 1; for (uint8_t index = 0; index < count; ++index) { memcpy(&encoded_database[last_entry + 16 + index * 256], kLegacyCustomThresholdProfile, 256); } require(controller_profile_database_decode( read_encoded_database, nullptr, &decoded_database), "legacy database strides were not preserved"); const ControllerProfileDatabaseEntry* entry = controller_profile_database_find(decoded_database, identity(16)); require(entry != nullptr && entry->active_profile == count - 1 && entry->profiles[count - 1].triggers[1].digital_threshold == 0xabcd, "last legacy bank profile was read from the wrong offset"); if (count == 4) { require(entry->profiles[7].triggers[0].digital_threshold == CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD, "new profile slots were not defaulted during migration"); } encoded_database[4] = 3; require(!controller_profile_database_decode( read_encoded_database, nullptr, &decoded_database), "current database version accepted legacy strides"); } } void test_schema6_migration_preserves_every_setting() { ControllerProfile profile{}; uint8_t upgraded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_decode(kLegacySchema6Profile, sizeof(kLegacySchema6Profile), &profile) && controller_profile_encode(profile, upgraded, sizeof(upgraded)), "schema6 profile could not upgrade"); require(memcmp(upgraded + 2, kLegacySchema6Profile + 2, 342) == 0, "schema6 migration changed an existing encoded setting"); require(profile.swing.button == CONTROLLER_PROFILE_NO_BUTTON && profile.swing.sensitivity == 1 && profile.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON, "schema6 padding became a swing gesture"); require_no_new_swing_actions(profile); for (uint8_t index = 0; index < CONTROLLER_PROFILE_EXTRA_BUTTON_COUNT; ++index) { require(profile.extra_button_map[index] == CONTROLLER_PROFILE_NO_BUTTON && profile.shift.extra_button_map[index] == CONTROLLER_PROFILE_NO_BUTTON, "schema6 padding became an extra-button mapping"); } ControllerProfile reloaded{}; uint8_t round_trip[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_decode(upgraded, sizeof(upgraded), &reloaded) && controller_profile_encode(reloaded, round_trip, sizeof(round_trip)) && memcmp(upgraded, round_trip, sizeof(upgraded)) == 0, "upgraded schema6 profile was not stable after reload"); const uint16_t extra_field_offsets[] = {344, 351, 358, 362, 363}; for (uint16_t offset : extra_field_offsets) { uint8_t invalid[sizeof(kLegacySchema6Profile)]{}; memcpy(invalid, kLegacySchema6Profile, sizeof(invalid)); invalid[offset] = 1; require(!controller_profile_decode(invalid, sizeof(invalid), &reloaded), "schema6 interpreted an extra-control field in reserved padding"); } const uint16_t source_offsets[] = {256, 266}; for (uint16_t offset : source_offsets) { uint8_t invalid[sizeof(kLegacySchema6Profile)]{}; memcpy(invalid, kLegacySchema6Profile, sizeof(invalid)); invalid[offset] = CONTROLLER_PROFILE_FIRST_EXTRA_CONTROL; require(!controller_profile_decode(invalid, sizeof(invalid), &reloaded), "schema6 admitted an extra-control modifier"); } uint8_t invalid[sizeof(kLegacySchema6Profile)]{}; memcpy(invalid, kLegacySchema6Profile, sizeof(invalid)); invalid[98] = (CONTROLLER_PROFILE_FIRST_EXTRA_CONTROL << 2) | 1; require(!controller_profile_decode(invalid, sizeof(invalid), &reloaded), "schema6 admitted an extra-control macro cancellation"); } void test_extra_control_schema_round_trip_and_output_limits() { ControllerProfile profile{}; require(controller_profile_decode(kLegacySchema6Profile, sizeof(kLegacySchema6Profile), &profile), "extra-control fixture did not decode"); const uint8_t extra_map[] = {0, 16, 17, 12, 13, 14, 15}; const uint8_t shifted_map[] = {15, 14, 13, 12, 3, 2, 0xff}; memcpy(profile.extra_button_map, extra_map, sizeof(extra_map)); memcpy(profile.shift.extra_button_map, shifted_map, sizeof(shifted_map)); profile.shortcuts.modifier = 18; profile.shift.modifier = 24; profile.switching_chord |= 0x55u << 18; profile.motion_toggle_chord |= 0x2au << 18; for (uint8_t index = 0; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index) { profile.macros[index].trigger_mask |= (1u << index) << 18; profile.macros[index].cancel_control = 24 - index; } uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; ControllerProfile decoded{}; require(controller_profile_encode(profile, encoded, sizeof(encoded)) && controller_profile_decode(encoded, sizeof(encoded), &decoded), "extra-control profile did not round-trip"); require(memcmp(encoded + 344, extra_map, sizeof(extra_map)) == 0 && memcmp(encoded + 351, shifted_map, sizeof(shifted_map)) == 0 && encoded[358] == 1 && encoded[359] == 2 && encoded[360] == 4 && encoded[361] == 8 && encoded[362] == 0x55 && encoded[363] == 0x2a, "extra-control fields do not use the schema7 extension layout"); uint8_t round_trip[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_encode(decoded, round_trip, sizeof(round_trip)) && memcmp(encoded, round_trip, sizeof(encoded)) == 0, "extra controls lost masks, mappings, or modifiers on reload"); for (uint16_t offset = 358; offset < 364; ++offset) { encoded[offset] |= 0x80; require(!controller_profile_decode(encoded, sizeof(encoded), &decoded), "out-of-range extra-control mask was accepted"); encoded[offset] &= 0x7f; } profile.extra_button_map[0] = 18; require(!controller_profile_validate(profile), "extra input was accepted as a console output destination"); profile.extra_button_map[0] = 0; profile.triggers[0].output = 18; require(!controller_profile_validate(profile), "analog trigger was allowed to route into a source-only control"); profile.triggers[0].output = 0; profile.shift.extra_button_map[0] = 16; require(!controller_profile_validate(profile), "Shift extra mapping admitted an analog destination"); } void test_schema7_migration_preserves_extra_controls_and_macros() { uint8_t legacy[sizeof(kLegacySchema6Profile)]{}; memcpy(legacy, kLegacySchema6Profile, sizeof(legacy)); legacy[0] = 7; const uint8_t extra_map[] = {0, 16, 17, 12, 13, 14, 15}; const uint8_t shifted_map[] = {15, 14, 13, 12, 3, 2, 0xff}; memcpy(legacy + 344, extra_map, sizeof(extra_map)); memcpy(legacy + 351, shifted_map, sizeof(shifted_map)); legacy[256] = 18; legacy[266] = 24; for (uint8_t index = 0; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index) { legacy[98 + index * 6] = static_cast( (legacy[98 + index * 6] & 3u) | ((24 - index) << 2)); legacy[358 + index] = static_cast(1u << index); } legacy[362] = 0x55; legacy[363] = 0x2a; ControllerProfile migrated{}; migrated.swing = {2, 2, 24}; uint8_t upgraded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_decode(legacy, sizeof(legacy), &migrated) && controller_profile_encode(migrated, upgraded, sizeof(upgraded)) && memcmp(legacy + 2, upgraded + 2, 362) == 0 && migrated.swing.button == CONTROLLER_PROFILE_NO_BUTTON && migrated.swing.sensitivity == 1 && migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON, "schema7 migration lost existing settings or enabled swing"); require_no_new_swing_actions(migrated); for (size_t offset = 364; offset < sizeof(legacy); ++offset) { legacy[offset] = 1; require(!controller_profile_decode(legacy, sizeof(legacy), &migrated), "schema7 interpreted reserved padding as swing settings"); legacy[offset] = 0; } } void test_schema8_migration_preserves_remote_swing() { uint8_t legacy[sizeof(kLegacySchema6Profile)]{}; memcpy(legacy, kLegacySchema6Profile, sizeof(legacy)); legacy[0] = 8; legacy[344] = 16; legacy[351] = 15; legacy[358] = 0x40; legacy[362] = 0x55; legacy[363] = 0x2a; legacy[364] = 15; legacy[365] = 2; legacy[366] = 24; ControllerProfile migrated{}; migrated.swing.macro = 0; migrated.nunchuk_swing = {0, 0, 0}; migrated.combined_swing = {1, CONTROLLER_PROFILE_NO_BUTTON, 16}; migrated.combination_window_ms = 200; uint8_t upgraded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; require(controller_profile_decode(legacy, sizeof(legacy), &migrated) && controller_profile_encode(migrated, upgraded, sizeof(upgraded)) && memcmp(legacy + 2, upgraded + 2, 365) == 0 && migrated.swing.button == 15 && migrated.swing.sensitivity == 2 && migrated.swing.modifier == 24, "schema8 migration changed Remote, extras, or macro settings"); require_no_new_swing_actions(migrated); for (size_t offset = 367; offset < sizeof(legacy); ++offset) { legacy[offset] = 1; require(!controller_profile_decode(legacy, sizeof(legacy), &migrated), "schema8 interpreted reserved padding as new swing actions"); legacy[offset] = 0; } } void test_swing_wire_settings_and_rejection() { ControllerProfile profile = controller_profile_default(controller_identity_global(), 0); profile.swing = {15, 2, 24}; profile.nunchuk_swing = {0, 0, 0}; profile.combined_swing = {4, CONTROLLER_PROFILE_NO_BUTTON, 16}; profile.combination_window_ms = 30; profile.macros[0].step_count = 1; profile.macro_step_count = 1; profile.macro_steps[0].duration_ms = 1; for (uint8_t index = 1; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index) { profile.macros[index].first_step = 1; } uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; ControllerProfile decoded{}; const uint8_t button_actions[] = { 15, 2, 24, 255, 0, 0, 0, 255, 4, 255, 16, 30}; require(controller_profile_encode(profile, encoded, sizeof(encoded)) && memcmp(encoded + 364, button_actions, sizeof(button_actions)) == 0 && controller_profile_decode(encoded, sizeof(encoded), &decoded) && decoded.swing.button == 15 && decoded.swing.sensitivity == 2 && decoded.swing.modifier == 24 && decoded.nunchuk_swing.button == 0 && decoded.nunchuk_swing.sensitivity == 0 && decoded.nunchuk_swing.modifier == 0 && decoded.combined_swing.button == 4 && decoded.combined_swing.modifier == 16 && decoded.combination_window_ms == 30, "independent swing button actions did not use the schema9 layout"); const ControllerProfileSwingConfiguration invalid_settings[] = { {16, 2, 24}, {254, 2, 24}, {15, 3, 24}, {15, 255, 24}, {15, 2, 25}, {15, 2, 254}, {255, 3, 255}, {255, 1, 25}, {255, 1, 24, 4}, {255, 1, 24, 254}, {0, 1, 24, 0}}; for (uint8_t sensor = 0; sensor < 2; ++sensor) { const size_t offset = 364 + sensor * 4; for (const auto& settings : invalid_settings) { ControllerProfile invalid = profile; (sensor == 0 ? invalid.swing : invalid.nunchuk_swing) = settings; uint8_t malformed[sizeof(encoded)]{}; require(!controller_profile_validate(invalid) && !controller_profile_encode(invalid, malformed, sizeof(malformed)), "invalid single-sensor swing action was accepted for encoding"); memcpy(malformed, encoded, sizeof(malformed)); malformed[offset] = settings.button; malformed[offset + 1] = settings.sensitivity; malformed[offset + 2] = settings.modifier; malformed[offset + 3] = settings.macro; require(!controller_profile_decode(malformed, sizeof(malformed), &decoded), "invalid single-sensor swing wire action was accepted"); } } const ControllerProfileCombinedSwingConfiguration invalid_combined[] = { {16, 255, 24}, {254, 255, 24}, {255, 4, 24}, {255, 254, 24}, {0, 0, 24}, {15, 255, 25}, {255, 255, 254}}; for (const auto& settings : invalid_combined) { ControllerProfile invalid = profile; invalid.combined_swing = settings; uint8_t malformed[sizeof(encoded)]{}; require(!controller_profile_validate(invalid) && !controller_profile_encode(invalid, malformed, sizeof(malformed)), "invalid combined swing action was accepted for encoding"); memcpy(malformed, encoded, sizeof(malformed)); malformed[372] = settings.button; malformed[373] = settings.macro; malformed[374] = settings.modifier; require(!controller_profile_decode(malformed, sizeof(malformed), &decoded), "invalid combined swing wire action was accepted"); } const uint8_t invalid_windows[] = {29, 201}; for (uint8_t window : invalid_windows) { ControllerProfile invalid = profile; invalid.combination_window_ms = window; uint8_t malformed[sizeof(encoded)]{}; require(!controller_profile_validate(invalid) && !controller_profile_encode(invalid, malformed, sizeof(malformed)), "out-of-range combination window was accepted for encoding"); memcpy(malformed, encoded, sizeof(malformed)); malformed[375] = window; require(!controller_profile_decode(malformed, sizeof(malformed), &decoded), "out-of-range combination window was accepted from wire"); } for (size_t offset = 376; offset < sizeof(encoded); ++offset) { require(encoded[offset] == 0, "schema9 reserved tail was not zero"); encoded[offset] = 1; require(!controller_profile_decode(encoded, sizeof(encoded), &decoded), "nonzero swing extension reservation was accepted"); encoded[offset] = 0; } profile.swing = {CONTROLLER_PROFILE_NO_BUTTON, 0, CONTROLLER_PROFILE_NO_BUTTON}; profile.nunchuk_swing = profile.swing; profile.combined_swing = {}; require(controller_profile_encode(profile, encoded, sizeof(encoded)) && controller_profile_decode(encoded, sizeof(encoded), &decoded) && decoded.swing.button == CONTROLLER_PROFILE_NO_BUTTON && decoded.swing.macro == CONTROLLER_PROFILE_NO_BUTTON && decoded.swing.sensitivity == 0 && decoded.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON && decoded.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON && decoded.nunchuk_swing.sensitivity == 0 && decoded.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON && decoded.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON, "disabled gestures became actions or lost their sensitivity"); } void test_swing_macro_targets_require_playable_steps() { ControllerProfile profile = controller_profile_default(controller_identity_global(), 0); profile.macros[0].step_count = 2; for (uint8_t index = 1; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index) { profile.macros[index].first_step = 2; } profile.macros[3].step_count = 1; profile.macro_step_count = 3; profile.macro_steps[1].duration_ms = 1; profile.macro_steps[2].duration_ms = 10000; profile.swing = {255, 2, 24, 0}; profile.nunchuk_swing = {255, 0, 255, 3}; profile.combined_swing = {255, 0, 0}; profile.combination_window_ms = 200; uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{}; ControllerProfile decoded{}; const uint8_t macro_actions[] = { 255, 2, 24, 0, 255, 0, 255, 3, 255, 0, 0, 200}; for (uint8_t mode = 0; mode <= 3; ++mode) { profile.macros[0].mode = static_cast(mode); require(controller_profile_encode(profile, encoded, sizeof(encoded)) && memcmp(encoded + 364, macro_actions, sizeof(macro_actions)) == 0 && controller_profile_decode(encoded, sizeof(encoded), &decoded) && decoded.swing.macro == 0 && decoded.nunchuk_swing.macro == 3 && decoded.combined_swing.macro == 0 && decoded.combination_window_ms == 200 && decoded.macros[0].mode == profile.macros[0].mode && decoded.macros[0].trigger_mask == 0 && decoded.macro_steps[0].duration_ms == 0 && decoded.macro_steps[1].duration_ms == 1 && decoded.macro_steps[2].duration_ms == 10000, "gesture macro targets lost steps, playback mode, or index boundaries"); } profile.swing = {}; profile.nunchuk_swing = {}; profile.combined_swing = {}; profile.macros[0].mode = ControllerProfileMacroMode::kOnce; profile.macro_steps[1].duration_ms = 0; require(controller_profile_encode(profile, encoded, sizeof(encoded)), "unbound zero-duration one-shot macro was rejected"); const size_t macro_offsets[] = {367, 371, 373}; for (uint8_t gesture = 0; gesture < 3; ++gesture) { for (uint8_t target = 0; target < 2; ++target) { ControllerProfile invalid = profile; uint8_t* bindings[] = {&invalid.swing.macro, &invalid.nunchuk_swing.macro, &invalid.combined_swing.macro}; *bindings[gesture] = target; uint8_t malformed[sizeof(encoded)]{}; require(!controller_profile_validate(invalid) && !controller_profile_encode(invalid, malformed, sizeof(malformed)), "gesture accepted a zero-duration or empty macro target"); memcpy(malformed, encoded, sizeof(malformed)); malformed[macro_offsets[gesture]] = target; require(!controller_profile_decode(malformed, sizeof(malformed), &decoded), "wire gesture accepted a zero-duration or empty macro target"); } } profile.macro_steps[1].duration_ms = 1; profile.macro_steps[2].duration_ms = 0; profile.nunchuk_swing.macro = 3; require(!controller_profile_validate(profile), "gesture used another macro's duration to validate its target"); } } // namespace int main() { test_pair_identity_wire_and_member_validation(); test_profile_wire_schema(); test_legacy_profile_migration(); test_database_round_trip_and_capacity(); test_set_b_sparse_extension_and_migration(); test_legacy_database_strides(); test_schema6_migration_preserves_every_setting(); test_extra_control_schema_round_trip_and_output_limits(); test_schema7_migration_preserves_extra_controls_and_macros(); test_schema8_migration_preserves_remote_swing(); test_swing_wire_settings_and_rejection(); test_swing_macro_targets_require_playable_steps(); return 0; }