From 4ade76ea90d241ae89880e8a6dd04e4852289e8f Mon Sep 17 00:00:00 2001 From: Leonardo Francisco Date: Sat, 4 Apr 2026 23:40:40 -0400 Subject: [PATCH] validity: Add firmware extension upload (Iteration 3) Implement the firmware extension (fwext) upload module for Validity/Synaptics VCSFW sensors. When the sensor reports no firmware loaded (GET_FW_INFO returns status 0xB004), the driver uploads the .xpfwext firmware file using the following sequence: 1. WRITE_HW_REG32 (0x08) to prepare hardware register 2. READ_HW_REG32 (0x07) to verify register state 3. Load .xpfwext file from filesystem search paths 4. For each 4KB chunk: a. Send db_write_enable blob (encrypted auth token) b. WRITE_FLASH (0x41) with chunk payload c. CLEANUP (0x1A) to commit chunk 5. WRITE_FW_SIG (0x42) to upload RSA signature 6. GET_FW_INFO (0x43) to verify successful upload 7. REBOOT (0x05 0x02 0x00) to activate new firmware Architecture: Uses the NULL-callback subsm pattern where SEND states call vcsfw_cmd_send(self, ssm, cmd, len, NULL) and RECV states read self->cmd_response_status/data directly. This avoids the double-advance bug with fpi_ssm_start_subsm auto-advancing the parent. New files: - validity_fwext.h: Structures, SSM state enum, API declarations - validity_fwext.c: Upload SSM, file parser, command builders - validity_blob_dbe_009a.inc: db_write_enable blob for 06cb:009a - test-validity-fwext.c: 19 unit tests covering all pure functions Modified files: - validity.h: Add cmd_response_status field to FpiDeviceValidity - validity.c: Add OPEN_UPLOAD_FWEXT state to open sequence - vcsfw_protocol.c: Save status in cmd_receive_cb for RECV states - meson.build: Add validity_fwext.c to driver sources Test results: 34 OK, 0 Fail, 2 Skipped --- libfprint/drivers/validity/validity.c | 61 +- libfprint/drivers/validity/validity.h | 1 + .../validity/validity_blob_dbe_009a.inc | 230 ++++++ libfprint/drivers/validity/validity_fwext.c | 693 ++++++++++++++++++ libfprint/drivers/validity/validity_fwext.h | 144 ++++ libfprint/drivers/validity/vcsfw_protocol.c | 3 +- libfprint/meson.build | 3 +- tests/meson.build | 14 + tests/test-validity-fwext.c | 643 ++++++++++++++++ 9 files changed, 1788 insertions(+), 4 deletions(-) create mode 100644 libfprint/drivers/validity/validity_blob_dbe_009a.inc create mode 100644 libfprint/drivers/validity/validity_fwext.c create mode 100644 libfprint/drivers/validity/validity_fwext.h create mode 100644 tests/test-validity-fwext.c diff --git a/libfprint/drivers/validity/validity.c b/libfprint/drivers/validity/validity.c index 4e904f38..c7e13744 100644 --- a/libfprint/drivers/validity/validity.c +++ b/libfprint/drivers/validity/validity.c @@ -23,6 +23,7 @@ #include "drivers_api.h" #include "fpi-byte-reader.h" #include "validity.h" +#include "validity_fwext.h" #include "validity_tls.h" #include "vcsfw_protocol.h" @@ -162,8 +163,9 @@ err_close: * 1) GET_VERSION (0x01) * 2) UNKNOWN_INIT (0x19) * 3) GET_FW_INFO (0x43 0x02) — check if fwext loaded - * 4) Send init_hardcoded blob - * 5) If no fwext: send init_hardcoded_clean_slate blob + * 4) Upload firmware extension (if not loaded) + * 5) Send init_hardcoded blob + * 6) If no fwext: send init_hardcoded_clean_slate blob */ typedef enum { @@ -173,6 +175,7 @@ typedef enum { OPEN_RECV_CMD19, OPEN_SEND_GET_FW_INFO, OPEN_RECV_GET_FW_INFO, + OPEN_UPLOAD_FWEXT, OPEN_TLS_READ_FLASH, OPEN_TLS_DERIVE_PSK, OPEN_TLS_HANDSHAKE, @@ -245,6 +248,33 @@ flash_read_ssm_done (FpiSsm *ssm, fpi_ssm_next_state (self->open_ssm); } +/* Callback for fwext upload child SSM */ +static void +fwext_upload_ssm_done (FpiSsm *ssm, + FpDevice *dev, + GError *error) +{ + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + if (error) + { + fp_warn ("Firmware extension upload failed: %s", error->message); + /* Non-fatal: the device will work without fwext (TLS will be skipped) + * but user should be informed. Continue to OPEN_DONE. */ + g_clear_error (&error); + fpi_ssm_jump_to_state (self->open_ssm, OPEN_DONE); + return; + } + + /* After successful upload and reboot, the device will re-enumerate + * on USB. We cannot continue the open sequence — report an error + * that tells fprintd to retry. */ + fp_info ("Firmware extension uploaded successfully — device rebooting"); + fpi_ssm_mark_failed (self->open_ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_REMOVED, + "Device rebooting after firmware upload")); +} + /* Callback for optional TLS handshake child SSM */ static void tls_handshake_ssm_done (FpiSsm *ssm, @@ -334,6 +364,33 @@ open_run_state (FpiSsm *ssm, fw_info_recv_cb, NULL); break; + case OPEN_UPLOAD_FWEXT: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + /* If fwext is already loaded, skip upload */ + if (self->fwext_loaded) + { + fpi_ssm_next_state (ssm); + return; + } + + /* In emulation mode, skip upload — no real device */ + if (g_strcmp0 (g_getenv ("FP_DEVICE_EMULATION"), "1") == 0) + { + fp_dbg ("Emulation mode — skipping fwext upload"); + fpi_ssm_next_state (ssm); + return; + } + + fp_info ("Firmware extension not loaded — starting upload"); + + self->open_ssm = ssm; + FpiSsm *fwext_ssm = validity_fwext_upload_ssm_new (dev); + fpi_ssm_start (fwext_ssm, fwext_upload_ssm_done); + } + break; + case OPEN_TLS_READ_FLASH: { FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); diff --git a/libfprint/drivers/validity/validity.h b/libfprint/drivers/validity/validity.h index 0ecb1dac..0eb25e1f 100644 --- a/libfprint/drivers/validity/validity.h +++ b/libfprint/drivers/validity/validity.h @@ -108,6 +108,7 @@ struct _FpiDeviceValidity FpiSsm *open_ssm; /* Pending response data stashed for higher-level SSM consumption */ + guint16 cmd_response_status; guint8 *cmd_response_data; gsize cmd_response_len; }; diff --git a/libfprint/drivers/validity/validity_blob_dbe_009a.inc b/libfprint/drivers/validity/validity_blob_dbe_009a.inc new file mode 100644 index 00000000..5ec92f15 --- /dev/null +++ b/libfprint/drivers/validity/validity_blob_dbe_009a.inc @@ -0,0 +1,230 @@ +/* db_write_enable blob for 06cb:009a (3621 bytes) */ +static const guint8 db_write_enable_009a[] = { + 0x06, 0x02, 0x00, 0x00, 0x01, 0xf4, 0x80, 0x01, 0x07, 0x48, 0x92, 0xb6, 0xc5, 0x7d, 0xeb, 0x78, + 0x89, 0xb5, 0xeb, 0xf8, 0x6b, 0xc3, 0x04, 0x0f, 0x6d, 0x91, 0xff, 0x1f, 0x68, 0x76, 0x5f, 0x04, + 0x65, 0x91, 0x18, 0x4b, 0xe0, 0x8c, 0xf3, 0x6c, 0x15, 0x4b, 0x7e, 0xc5, 0x36, 0x81, 0x39, 0xd0, + 0xf9, 0x53, 0x23, 0x82, 0x21, 0x43, 0x79, 0xaf, 0xf3, 0xff, 0xbf, 0xe4, 0x65, 0x9e, 0x2f, 0x27, + 0x4e, 0x86, 0x4b, 0xd0, 0xad, 0x66, 0x0f, 0x99, 0xe2, 0x1d, 0xa2, 0xba, 0xb6, 0x77, 0xdb, 0xfa, + 0x90, 0x7a, 0x66, 0xce, 0x11, 0x0c, 0x18, 0x0d, 0x2d, 0xdc, 0x5d, 0xfe, 0x40, 0xb8, 0xed, 0x97, + 0x5c, 0xbe, 0xdf, 0xfc, 0x11, 0x63, 0x1f, 0x12, 0xf8, 0xbd, 0x64, 0x6a, 0x0e, 0xe8, 0x2d, 0x44, + 0xd2, 0xa6, 0xc1, 0xec, 0x9c, 0xfb, 0xd4, 0x0f, 0x48, 0x5c, 0xb3, 0xd9, 0x12, 0x43, 0x76, 0xb9, + 0x7b, 0x4a, 0x33, 0x49, 0xb0, 0xa7, 0x30, 0xad, 0xda, 0x62, 0x6d, 0x8a, 0xc2, 0x8e, 0xc2, 0x0e, + 0x88, 0x6a, 0xab, 0x1b, 0x88, 0x51, 0xde, 0xee, 0x34, 0x31, 0xc4, 0xd8, 0x9c, 0x8b, 0xb3, 0xe7, + 0x87, 0xea, 0xa9, 0xc0, 0x32, 0x3d, 0xfe, 0x58, 0x3d, 0x54, 0x24, 0xd3, 0x64, 0x36, 0xe4, 0x43, + 0x50, 0x43, 0xe0, 0x4f, 0xd4, 0xea, 0x46, 0xb1, 0xfb, 0x25, 0x07, 0xca, 0x6f, 0x0e, 0xb0, 0x3b, + 0xaf, 0x27, 0xc8, 0x4b, 0x81, 0x9c, 0xbc, 0x96, 0xce, 0xc3, 0x1a, 0x78, 0x04, 0x5e, 0xb6, 0x48, + 0x33, 0x9e, 0x2a, 0xa4, 0x78, 0x9e, 0x76, 0x72, 0xd9, 0x33, 0x93, 0x60, 0x05, 0xf4, 0x72, 0x0c, + 0x8f, 0xfd, 0xc1, 0xea, 0x23, 0xa4, 0xf3, 0x0a, 0x1c, 0xdc, 0x8f, 0x6e, 0x87, 0x77, 0x5c, 0x24, + 0x1b, 0x9a, 0xb1, 0x56, 0x6f, 0x77, 0x71, 0x85, 0x7c, 0xc4, 0x70, 0x3d, 0x57, 0x1f, 0x11, 0x06, + 0xc5, 0x26, 0xf9, 0x52, 0x32, 0x92, 0x5a, 0x6a, 0x93, 0xec, 0x8e, 0x91, 0x90, 0x22, 0xfb, 0xe3, + 0x03, 0xa5, 0x15, 0xf9, 0xaa, 0xa8, 0xca, 0x21, 0x50, 0x72, 0x06, 0x93, 0x11, 0xdd, 0x3f, 0x97, + 0xd9, 0xa4, 0xf5, 0x62, 0x59, 0xba, 0xb3, 0xa1, 0xb7, 0xa8, 0x58, 0x2d, 0x6d, 0xc2, 0xf9, 0x2d, + 0x49, 0xf0, 0x23, 0xd6, 0xf2, 0x5a, 0x05, 0x83, 0x7e, 0x15, 0x36, 0xa6, 0x33, 0xe2, 0x52, 0xef, + 0x64, 0x52, 0x25, 0xf4, 0x29, 0x39, 0x55, 0x04, 0x1a, 0x0d, 0x54, 0xdc, 0xb1, 0xd1, 0xdd, 0x7e, + 0x09, 0x7b, 0x78, 0x39, 0xde, 0x5f, 0xde, 0x2a, 0x6c, 0xe9, 0x99, 0x96, 0x6d, 0x71, 0x2b, 0x4c, + 0xb2, 0xfd, 0x9d, 0x78, 0x30, 0x03, 0x1d, 0xa5, 0x5d, 0x9f, 0xaa, 0x99, 0xf8, 0x66, 0xfb, 0xb7, + 0xe5, 0x20, 0x56, 0x6e, 0xfb, 0xa4, 0x3c, 0x25, 0x09, 0x28, 0x6b, 0xf2, 0x8e, 0x1a, 0x20, 0xc6, + 0xa8, 0x36, 0xdb, 0x8a, 0x1f, 0xa4, 0xcb, 0x9b, 0x8d, 0x19, 0x37, 0x80, 0xaa, 0xb5, 0x92, 0xd4, + 0x16, 0x53, 0x83, 0x96, 0x70, 0x12, 0x90, 0x66, 0xac, 0x56, 0xf1, 0x26, 0x8e, 0x6f, 0x76, 0x13, + 0x37, 0xf7, 0x68, 0x55, 0x5e, 0x13, 0xc5, 0xd6, 0x81, 0x37, 0xc6, 0x0f, 0x83, 0xdb, 0xa8, 0xdc, + 0x38, 0x63, 0xe0, 0x0e, 0x73, 0xfd, 0x3a, 0xf2, 0x1e, 0x23, 0xa5, 0x66, 0xda, 0xa6, 0x7f, 0x3f, + 0x14, 0xdd, 0x93, 0x4e, 0x32, 0x36, 0x51, 0x16, 0x70, 0x21, 0xca, 0x6b, 0x82, 0xa6, 0x10, 0x3c, + 0xb3, 0x0b, 0xe8, 0x49, 0x44, 0x6e, 0x2f, 0x54, 0xdd, 0xe6, 0x4a, 0x05, 0x37, 0x70, 0x52, 0xb5, + 0x73, 0x32, 0xe9, 0xbf, 0x08, 0xa1, 0x8c, 0xf5, 0x2d, 0xa2, 0xa1, 0x3e, 0xbb, 0xd5, 0x5e, 0x60, + 0x33, 0x3f, 0x8b, 0xc3, 0x19, 0xe1, 0x45, 0x7f, 0x38, 0xec, 0x5d, 0x48, 0x39, 0xec, 0x0e, 0xcd, + 0x03, 0x48, 0x25, 0xbd, 0xea, 0xf6, 0x49, 0x26, 0x85, 0x8c, 0x6e, 0x8c, 0x2d, 0xf4, 0x18, 0x71, + 0x7b, 0x5f, 0x67, 0x13, 0x5a, 0xbc, 0x03, 0x88, 0x35, 0xd3, 0xe4, 0xe1, 0xaa, 0x80, 0x95, 0x46, + 0xfd, 0x0d, 0x7f, 0x01, 0x06, 0x6a, 0x71, 0x53, 0x7f, 0x96, 0xbd, 0x1e, 0xce, 0xc3, 0x68, 0x75, + 0x83, 0xe1, 0xb5, 0x11, 0xbf, 0x48, 0xc2, 0x77, 0x6f, 0x46, 0x70, 0x15, 0x8e, 0x56, 0x16, 0x4c, + 0x62, 0xda, 0x20, 0xf6, 0x71, 0x76, 0x4c, 0x78, 0x5c, 0x35, 0x2f, 0xc3, 0xcc, 0xe2, 0x2c, 0xef, + 0xa2, 0x07, 0x60, 0xac, 0xff, 0x8f, 0x45, 0xef, 0xb5, 0x4a, 0x93, 0x4f, 0x98, 0x34, 0xd5, 0x4f, + 0x97, 0x01, 0xde, 0xda, 0xcd, 0x4d, 0x38, 0x3a, 0xc0, 0x1f, 0x8c, 0xca, 0x92, 0x56, 0x2e, 0xec, + 0x77, 0x4a, 0x58, 0xda, 0x6f, 0x55, 0xda, 0x25, 0x2c, 0x49, 0x1e, 0xe2, 0xab, 0x58, 0xff, 0x76, + 0x9f, 0x89, 0xa9, 0x64, 0x9d, 0x39, 0x56, 0x68, 0x2c, 0xa7, 0xd0, 0x6b, 0xbf, 0x33, 0xf9, 0xa9, + 0x35, 0xb7, 0x81, 0xdf, 0xc2, 0x1b, 0x12, 0x3b, 0x16, 0x69, 0x44, 0x24, 0xe7, 0x2d, 0x6a, 0x3e, + 0x67, 0x81, 0xdc, 0xf1, 0x95, 0xef, 0xfd, 0x36, 0x47, 0x0a, 0x4e, 0xab, 0x0f, 0xdc, 0x74, 0xe8, + 0x71, 0x02, 0x87, 0x9e, 0xc8, 0x1f, 0xea, 0x65, 0x49, 0x92, 0x0c, 0xce, 0x45, 0x4a, 0xc7, 0x81, + 0x39, 0x97, 0xb8, 0x2d, 0x51, 0xe7, 0xb8, 0xc1, 0xee, 0x24, 0xfa, 0xd3, 0x89, 0x90, 0x44, 0x78, + 0xf8, 0x47, 0x65, 0x4e, 0xc3, 0xa6, 0x3b, 0xc5, 0x95, 0xb9, 0xa7, 0xdd, 0xe7, 0x98, 0xdb, 0x5c, + 0x0b, 0x6f, 0x24, 0x49, 0x01, 0xf2, 0x39, 0xe7, 0x67, 0x4c, 0x98, 0xee, 0xbb, 0x42, 0xb6, 0x6e, + 0x89, 0x56, 0xa7, 0x33, 0xc3, 0x79, 0x65, 0x86, 0x28, 0x0a, 0x19, 0xa1, 0xdf, 0x8a, 0x69, 0x22, + 0x4a, 0xcd, 0x25, 0x56, 0xf7, 0xec, 0x2e, 0x27, 0xca, 0xe3, 0x7c, 0x69, 0xb3, 0x32, 0xb2, 0xc0, + 0xec, 0x85, 0x99, 0x1a, 0xe4, 0x87, 0x22, 0xf9, 0x88, 0x93, 0x5f, 0x65, 0x8b, 0x9c, 0xf3, 0x2f, + 0x46, 0xdf, 0xc6, 0xd9, 0x6a, 0x5a, 0x36, 0xf1, 0x8b, 0x6b, 0xf9, 0xf6, 0x57, 0xb5, 0x9b, 0x3d, + 0xa4, 0x24, 0x14, 0xe4, 0xd5, 0x6c, 0x0a, 0x24, 0x48, 0x5a, 0xa2, 0x98, 0xd2, 0xd0, 0xd1, 0xb1, + 0x77, 0xe7, 0xd0, 0xda, 0xfe, 0x60, 0x2a, 0x4f, 0xb4, 0xf4, 0x23, 0xde, 0xf4, 0xbd, 0xb0, 0x10, + 0xfd, 0xc6, 0x26, 0xc9, 0x47, 0x58, 0x7e, 0x19, 0xe7, 0xe4, 0xb0, 0xe6, 0xf9, 0xf2, 0xda, 0x41, + 0xc2, 0x9a, 0x8f, 0x19, 0x03, 0xd0, 0xd2, 0x80, 0x33, 0x65, 0xfe, 0x0a, 0x11, 0x3a, 0xbb, 0xa1, + 0x92, 0x20, 0x14, 0x1d, 0x1a, 0xc7, 0xce, 0xc6, 0x83, 0x96, 0x20, 0x30, 0xd3, 0xf6, 0x59, 0x1f, + 0x98, 0xea, 0x3d, 0xd0, 0x91, 0x62, 0x71, 0x5e, 0x5c, 0x12, 0xf4, 0x03, 0x32, 0xb4, 0x7c, 0x53, + 0x16, 0x45, 0x32, 0x82, 0x7e, 0x55, 0x96, 0xfb, 0x2c, 0xc0, 0xaa, 0x8f, 0x31, 0x68, 0x3c, 0xc6, + 0x3e, 0xc1, 0x4c, 0x03, 0x4c, 0x6f, 0x3d, 0x2c, 0x70, 0xb8, 0xc4, 0x76, 0x11, 0xb4, 0xc5, 0xcb, + 0x53, 0x48, 0xa2, 0x55, 0x9f, 0xb1, 0x62, 0xa7, 0x80, 0xa2, 0xb4, 0x03, 0xb0, 0x12, 0x0a, 0x68, + 0x46, 0xe2, 0x7d, 0x60, 0x57, 0xa3, 0xab, 0x9e, 0x1b, 0x18, 0x91, 0x5a, 0xe2, 0x03, 0x9e, 0x81, + 0xcc, 0x6c, 0x50, 0xd2, 0xa1, 0x4d, 0x59, 0x13, 0x61, 0x7b, 0xac, 0xae, 0x78, 0xfe, 0x9b, 0x91, + 0xe9, 0xe4, 0x9d, 0x2e, 0x82, 0xde, 0xf4, 0x75, 0x65, 0xc1, 0x2f, 0xf9, 0x38, 0xb1, 0x82, 0xf8, + 0xce, 0x94, 0x1d, 0x27, 0x81, 0xb7, 0x73, 0x47, 0x95, 0x38, 0xc7, 0x6e, 0xd9, 0xf7, 0xd4, 0x46, + 0x9f, 0x6f, 0xe5, 0xba, 0x7f, 0x6e, 0x3a, 0xd9, 0x88, 0x71, 0xb2, 0x86, 0x6f, 0x0e, 0xf4, 0xf3, + 0x62, 0x77, 0xda, 0xa7, 0x6c, 0x10, 0x42, 0xc8, 0x3f, 0x77, 0xdf, 0x0f, 0xf2, 0xe2, 0x63, 0x95, + 0x40, 0xbb, 0x35, 0x5e, 0xa8, 0x42, 0x73, 0x41, 0x1c, 0x45, 0x30, 0x81, 0xbd, 0x1e, 0x10, 0x35, + 0xc4, 0x02, 0xc5, 0x31, 0x90, 0xd0, 0xbd, 0x90, 0x5e, 0x8d, 0x01, 0xfc, 0x37, 0x87, 0xc6, 0x5b, + 0x69, 0x17, 0x2c, 0xca, 0x5b, 0x23, 0x4e, 0x92, 0xe3, 0x58, 0x46, 0x3b, 0xbb, 0x8d, 0x23, 0xe3, + 0x8c, 0x74, 0xa3, 0xa8, 0xe2, 0x73, 0x55, 0x42, 0xb9, 0x96, 0xba, 0x5e, 0xc2, 0x2c, 0x50, 0x95, + 0xa7, 0x77, 0xb6, 0x77, 0x5a, 0x72, 0x8d, 0xf5, 0x9c, 0x35, 0x60, 0xc7, 0xf3, 0x6b, 0x83, 0xd5, + 0x5f, 0x81, 0x9f, 0x19, 0x65, 0x73, 0xf8, 0xfd, 0x35, 0x63, 0x79, 0xfe, 0x9a, 0x5e, 0x7c, 0xec, + 0xb3, 0x76, 0x39, 0x5e, 0x01, 0x30, 0x9e, 0x20, 0x05, 0xb2, 0x9e, 0x3b, 0x16, 0x0c, 0xb7, 0x4c, + 0x6a, 0x58, 0x56, 0x09, 0x34, 0x80, 0xdd, 0x06, 0xae, 0xa5, 0xfb, 0x3f, 0xbe, 0x23, 0xe0, 0x04, + 0xf8, 0xd7, 0xa3, 0x8f, 0xd0, 0x78, 0x66, 0xcd, 0xf2, 0x41, 0x61, 0x39, 0x1c, 0xc7, 0x56, 0xf6, + 0xff, 0x71, 0xff, 0x07, 0x2e, 0x30, 0x8b, 0x35, 0xe2, 0x59, 0x43, 0x51, 0x11, 0xbe, 0xe0, 0x9d, + 0xdf, 0x2b, 0x8d, 0xf9, 0x9d, 0x0f, 0x2c, 0x2e, 0x8e, 0xda, 0xa4, 0xec, 0xaa, 0xbc, 0x69, 0x75, + 0xa5, 0x8f, 0x23, 0xbb, 0x6b, 0xfc, 0x94, 0xeb, 0xcb, 0xbb, 0xa0, 0xd5, 0x81, 0xf1, 0x6b, 0xe9, + 0xd0, 0x43, 0xc4, 0xe4, 0x10, 0xb3, 0x21, 0xc6, 0xdf, 0x42, 0x4e, 0xca, 0xee, 0xa9, 0x4e, 0xdb, + 0xe5, 0x80, 0x1e, 0xb7, 0x86, 0x19, 0x91, 0x24, 0x22, 0x2b, 0x09, 0x1e, 0x5b, 0x33, 0xba, 0xd6, + 0x76, 0x14, 0x45, 0xa8, 0xa6, 0x60, 0x6d, 0x0e, 0x78, 0x1c, 0x07, 0xa6, 0xf9, 0x1c, 0xd5, 0xfe, + 0x18, 0x8d, 0xdb, 0x9f, 0x9e, 0x17, 0xf5, 0xe0, 0x7b, 0x0c, 0xba, 0x31, 0x9c, 0x52, 0xe5, 0xfb, + 0x03, 0xf5, 0x3d, 0xf5, 0x70, 0xf8, 0x2d, 0xdb, 0x60, 0x3d, 0x30, 0x5b, 0x72, 0xa2, 0x40, 0x6b, + 0xc7, 0xc1, 0xa3, 0x7f, 0x92, 0x04, 0x05, 0xf8, 0xf1, 0x4d, 0x3d, 0xdf, 0x5d, 0x83, 0x6b, 0xa6, + 0x8d, 0x83, 0xc1, 0xa8, 0xd7, 0xf1, 0xa4, 0x1d, 0x14, 0x8c, 0xc3, 0x4b, 0x1e, 0xf9, 0x96, 0xdd, + 0xfb, 0x43, 0xef, 0x19, 0xd2, 0xfb, 0xf0, 0xad, 0xca, 0xd3, 0x01, 0xa4, 0x73, 0x49, 0x77, 0x39, + 0xea, 0xa1, 0x0b, 0xbc, 0xe8, 0x5e, 0x15, 0xc3, 0x2f, 0x1d, 0x90, 0xc8, 0xab, 0x86, 0x05, 0xd0, + 0xae, 0x94, 0x1e, 0xb9, 0x14, 0x08, 0x65, 0x92, 0xd0, 0x87, 0xa5, 0x21, 0xfd, 0xe3, 0x3a, 0x67, + 0x6c, 0xdf, 0xb9, 0x4a, 0x42, 0x47, 0xf6, 0x0f, 0x51, 0xed, 0xd3, 0x72, 0x94, 0x51, 0x1e, 0x92, + 0xec, 0x71, 0xa9, 0xa5, 0x4b, 0xab, 0x68, 0xa0, 0xed, 0xaa, 0xbd, 0xcb, 0x2c, 0x1a, 0x3a, 0xde, + 0xa7, 0x78, 0xf4, 0x16, 0xe3, 0x92, 0x00, 0xaf, 0x4c, 0x51, 0x7d, 0xd7, 0x15, 0x2b, 0xb7, 0x24, + 0x76, 0xc5, 0xd1, 0x41, 0x3f, 0x04, 0x70, 0x46, 0x15, 0xd7, 0x95, 0x30, 0x0f, 0x3a, 0x09, 0x12, + 0x14, 0xf4, 0xe4, 0xac, 0x2e, 0xf4, 0x19, 0x69, 0xc8, 0x1f, 0x8f, 0xc0, 0x86, 0x10, 0x86, 0x49, + 0x07, 0xb2, 0xe6, 0xed, 0xfa, 0x5f, 0xdb, 0x09, 0x26, 0xb6, 0xf0, 0x64, 0xb2, 0xa1, 0xc3, 0xb8, + 0xc7, 0xb6, 0x31, 0xcc, 0x75, 0x66, 0x3c, 0xed, 0xad, 0x5e, 0x71, 0x86, 0x8a, 0xbc, 0x9b, 0xac, + 0x67, 0x8e, 0x43, 0x01, 0x44, 0x61, 0x3c, 0xb0, 0xe5, 0x19, 0x82, 0xb9, 0xe0, 0x19, 0x09, 0x90, + 0x26, 0xb0, 0x69, 0xbb, 0x7a, 0x4d, 0xc3, 0x76, 0xcd, 0xd6, 0xa3, 0xc5, 0x95, 0x66, 0x31, 0x79, + 0x76, 0x21, 0x36, 0x72, 0x75, 0x4f, 0xac, 0x87, 0xdf, 0x85, 0x95, 0x3c, 0xdc, 0x0d, 0xe2, 0x76, + 0xfb, 0x87, 0x42, 0xf4, 0x8b, 0xa2, 0x18, 0xd4, 0x20, 0x2f, 0xe6, 0xf8, 0x65, 0x83, 0x41, 0x52, + 0x97, 0x9d, 0x6d, 0xa9, 0xb4, 0x73, 0xe5, 0xd4, 0x76, 0xc0, 0xaa, 0xa6, 0x84, 0x91, 0xf5, 0x45, + 0x09, 0x1b, 0x87, 0x9c, 0x01, 0x98, 0x60, 0x78, 0xd6, 0x4f, 0xa5, 0xf4, 0x9f, 0x60, 0xe6, 0x15, + 0xcb, 0x86, 0x5f, 0x15, 0x4f, 0x48, 0xb4, 0x51, 0x73, 0xa1, 0xdc, 0x85, 0xf2, 0xeb, 0x11, 0x28, + 0x65, 0x22, 0x90, 0xbd, 0x38, 0x3c, 0xde, 0xdc, 0xd8, 0xf2, 0x80, 0x11, 0x7e, 0x60, 0xbe, 0x03, + 0x4c, 0xe2, 0x24, 0xf9, 0x26, 0x73, 0x93, 0x4e, 0xd9, 0xe0, 0x07, 0x7d, 0x5f, 0x78, 0x99, 0xf4, + 0xe0, 0xee, 0xe0, 0x97, 0x93, 0x3a, 0x35, 0xe4, 0x0f, 0x20, 0x5d, 0x84, 0xa1, 0x07, 0x33, 0xf4, + 0x92, 0xda, 0x61, 0x98, 0x02, 0xff, 0x70, 0xd9, 0xb9, 0x49, 0xca, 0x0c, 0x2b, 0xcb, 0x9b, 0xa6, + 0x8c, 0x29, 0x0f, 0x2e, 0xf9, 0xa2, 0x0a, 0x3b, 0xf4, 0x96, 0x83, 0x4c, 0x66, 0x95, 0x6a, 0x8e, + 0xc4, 0x17, 0x92, 0x66, 0x99, 0x9d, 0x9f, 0x87, 0xbd, 0xfc, 0x14, 0xae, 0xa8, 0x65, 0xf0, 0x48, + 0x7e, 0x2b, 0xe1, 0x0a, 0x64, 0xbe, 0xcb, 0xa6, 0x95, 0x47, 0xd0, 0x16, 0x58, 0x93, 0x5e, 0x63, + 0x70, 0x39, 0x86, 0xa5, 0x6d, 0x6c, 0xe3, 0x8f, 0xe6, 0x6d, 0xbf, 0x61, 0xd7, 0x54, 0xba, 0x9a, + 0x1a, 0x27, 0x83, 0x53, 0x91, 0x34, 0x22, 0xe4, 0xf2, 0xe4, 0x10, 0x0c, 0x59, 0x62, 0x99, 0x9a, + 0x3e, 0xaa, 0x3e, 0x16, 0x72, 0xbc, 0x73, 0xed, 0xcf, 0xcc, 0x75, 0x25, 0xa2, 0xd3, 0xdb, 0xe9, + 0x56, 0x83, 0xb4, 0xbf, 0x38, 0xf7, 0x44, 0x4a, 0xc0, 0xf4, 0x70, 0xf0, 0xe9, 0x80, 0x79, 0x91, + 0x6e, 0x4e, 0x1f, 0xba, 0x3f, 0xcd, 0x5b, 0x08, 0x2f, 0xc2, 0x77, 0x2e, 0x63, 0xb5, 0xe0, 0x66, + 0x3f, 0x87, 0x63, 0x8a, 0x16, 0x38, 0x58, 0xf5, 0x90, 0x84, 0x52, 0x40, 0xa8, 0xc2, 0x2d, 0xac, + 0xf6, 0xf7, 0x99, 0x9c, 0x43, 0x1a, 0x2a, 0xb5, 0x20, 0x4a, 0x7d, 0xa7, 0x83, 0x9c, 0x9a, 0x93, + 0x26, 0x08, 0xc7, 0xf8, 0x3a, 0x87, 0xd1, 0xd7, 0x3d, 0x7d, 0x8b, 0x2f, 0xec, 0x65, 0xab, 0xb9, + 0x52, 0x21, 0xfa, 0xda, 0x44, 0x36, 0x5f, 0xe2, 0x10, 0x61, 0xdb, 0xcd, 0xe5, 0x2c, 0xb8, 0x4c, + 0xbf, 0xe9, 0xf0, 0x61, 0xc4, 0xda, 0xb3, 0xbe, 0x86, 0x00, 0x2e, 0x76, 0x83, 0xee, 0xd1, 0x6c, + 0x23, 0xc6, 0x87, 0xce, 0x61, 0xc5, 0xd9, 0x23, 0xff, 0xba, 0xb4, 0x0b, 0xee, 0x6a, 0xe9, 0x3e, + 0xd7, 0xf8, 0x57, 0xf3, 0x04, 0xe5, 0xeb, 0x16, 0xec, 0x6d, 0x08, 0x85, 0x63, 0x52, 0x4e, 0x90, + 0xd9, 0x16, 0xe4, 0x1a, 0x3a, 0x8c, 0x77, 0x77, 0xe2, 0x97, 0x31, 0xf0, 0xf4, 0x5c, 0x12, 0x50, + 0x82, 0xc4, 0x23, 0xa5, 0xc0, 0x27, 0x04, 0xc0, 0x7c, 0x6f, 0xc1, 0x9b, 0x1c, 0x48, 0x38, 0xee, + 0x3e, 0xab, 0xe1, 0x25, 0x62, 0x82, 0x9e, 0x67, 0x58, 0x1d, 0x31, 0x2c, 0x72, 0x0b, 0x79, 0x2a, + 0x41, 0x74, 0x4d, 0xec, 0x1e, 0x15, 0x74, 0x26, 0xab, 0x75, 0x13, 0x6d, 0x31, 0xee, 0x2f, 0x20, + 0x81, 0x47, 0x03, 0x90, 0x91, 0x45, 0x3c, 0x0b, 0x0e, 0x39, 0x70, 0xc5, 0x62, 0x4d, 0x7a, 0x53, + 0xdf, 0x80, 0x76, 0xe9, 0xd1, 0x62, 0x5d, 0x2c, 0x8e, 0x69, 0x3e, 0x0e, 0x9a, 0x81, 0xe2, 0x38, + 0x62, 0xdc, 0xa7, 0x89, 0x21, 0xb6, 0x6c, 0xa4, 0xc3, 0xc5, 0xed, 0x35, 0xb0, 0xb5, 0xed, 0x2e, + 0x24, 0x62, 0x2e, 0xb2, 0x16, 0xba, 0x0b, 0xa6, 0xe0, 0xc0, 0xea, 0xf9, 0x7c, 0x75, 0x4e, 0xeb, + 0x3d, 0xb4, 0xa5, 0x06, 0xd5, 0x85, 0x4a, 0x3e, 0xdc, 0x92, 0xd0, 0x11, 0x1a, 0xf3, 0xd2, 0x13, + 0x5a, 0x99, 0x87, 0x29, 0x12, 0x3f, 0x03, 0xd0, 0xf9, 0x36, 0x6b, 0xb0, 0xd2, 0xc6, 0x81, 0xcf, + 0xc6, 0x2c, 0x59, 0xbc, 0xd7, 0x5c, 0x6b, 0x41, 0x0d, 0x8e, 0x69, 0x97, 0xcc, 0xa5, 0x5c, 0x98, + 0x9f, 0x01, 0x03, 0x93, 0xd6, 0xc2, 0x42, 0xf7, 0xce, 0x1e, 0xa7, 0x1c, 0x6f, 0x26, 0x2e, 0x49, + 0x88, 0x55, 0x58, 0x43, 0x47, 0xb0, 0x4c, 0xe2, 0x6c, 0xce, 0x2e, 0x82, 0x2b, 0x8c, 0x6b, 0x7b, + 0x49, 0x37, 0x14, 0x8a, 0x45, 0xc9, 0x47, 0x07, 0x3b, 0x30, 0x0f, 0x7c, 0x72, 0xb6, 0xe7, 0x8c, + 0x42, 0x31, 0x07, 0x8d, 0x80, 0x53, 0x1b, 0x7f, 0x93, 0x17, 0xc1, 0xbb, 0x4d, 0x60, 0x70, 0xf2, + 0x99, 0xe9, 0xa9, 0x77, 0x31, 0xb1, 0xbe, 0xfe, 0xee, 0xc2, 0xda, 0xe0, 0xa1, 0xa0, 0x36, 0x45, + 0x68, 0xac, 0xbe, 0xba, 0xb0, 0x69, 0xa4, 0xb9, 0x01, 0x47, 0x77, 0x6f, 0xf7, 0xe7, 0xf7, 0x9c, + 0x1c, 0xc9, 0x8b, 0x2f, 0xe6, 0x21, 0x47, 0x92, 0x50, 0x15, 0x54, 0xf4, 0x19, 0x57, 0x83, 0xb0, + 0xf9, 0x18, 0x8c, 0xcf, 0xe9, 0x6a, 0xd8, 0xcd, 0x29, 0xf5, 0x46, 0x34, 0x09, 0xc2, 0x05, 0x4e, + 0x4a, 0x24, 0x96, 0xee, 0x65, 0xea, 0xa1, 0xfc, 0xda, 0x3d, 0x77, 0x64, 0xcd, 0x3e, 0x84, 0x31, + 0xe4, 0x4a, 0x2b, 0x05, 0xe6, 0x4a, 0xa2, 0xf9, 0xfb, 0x0d, 0x13, 0x45, 0x6b, 0xfe, 0xa9, 0xc9, + 0x1e, 0xc2, 0xd9, 0x0d, 0x00, 0x99, 0xe7, 0xe3, 0x95, 0xdc, 0xe8, 0x18, 0x65, 0x0d, 0xca, 0xf8, + 0xbd, 0xfe, 0x23, 0xb4, 0xc6, 0x44, 0x3f, 0x5c, 0x69, 0x0b, 0x18, 0xea, 0xd2, 0x21, 0xa6, 0xc2, + 0xbc, 0xd3, 0x45, 0x72, 0xff, 0xb8, 0x3b, 0x33, 0x32, 0xea, 0xfd, 0xe6, 0xe2, 0x5b, 0x37, 0xff, + 0x3a, 0xc6, 0xda, 0x0c, 0x3c, 0xc6, 0x97, 0xb9, 0x96, 0x26, 0x5c, 0xaa, 0x5a, 0x53, 0xce, 0x44, + 0x57, 0x03, 0x03, 0xd7, 0xd1, 0x11, 0xf4, 0x4c, 0x63, 0x51, 0x19, 0x59, 0x5c, 0x24, 0x7e, 0x86, + 0xa3, 0x20, 0x83, 0xf2, 0x86, 0x55, 0x01, 0x75, 0x2f, 0x93, 0xe3, 0x02, 0x4b, 0x2e, 0x2b, 0x6d, + 0x82, 0xd0, 0xc0, 0x3b, 0x74, 0x5b, 0xfd, 0x80, 0x9a, 0xf7, 0xe8, 0xe1, 0x34, 0x9d, 0x1a, 0x79, + 0xbe, 0xd5, 0x1b, 0xba, 0x41, 0x50, 0x64, 0x70, 0x1a, 0x2a, 0x78, 0x90, 0xe8, 0xf3, 0x99, 0x37, + 0xc6, 0xd2, 0xf5, 0x63, 0xb0, 0x74, 0x7b, 0xd9, 0x4f, 0x1b, 0x69, 0x86, 0x24, 0xb4, 0xfd, 0x17, + 0xdf, 0xdf, 0x68, 0xff, 0xdc, 0x04, 0x50, 0xc2, 0x6d, 0x77, 0x1f, 0x8f, 0xf4, 0xfb, 0x01, 0xa2, + 0x6f, 0xf8, 0xf6, 0x4e, 0xb5, 0xb6, 0xd9, 0x15, 0x3f, 0x5c, 0xe2, 0x9d, 0x9d, 0xfc, 0xf8, 0x4c, + 0xa2, 0x30, 0xa4, 0xc2, 0x12, 0x40, 0x1b, 0x43, 0x7d, 0x11, 0x37, 0xf8, 0x3a, 0x44, 0xf7, 0xa9, + 0x8a, 0x9f, 0xd1, 0xbc, 0x3d, 0x88, 0x3e, 0x62, 0x27, 0xce, 0x36, 0x9e, 0xd3, 0x2a, 0x96, 0x05, + 0x50, 0xaa, 0x86, 0x3f, 0x3d, 0x01, 0x4d, 0xe7, 0x49, 0x4d, 0xea, 0xd3, 0x4f, 0xce, 0xd1, 0xd7, + 0xb4, 0xea, 0xb6, 0x51, 0xd4, 0x99, 0x03, 0x35, 0x89, 0x44, 0x6f, 0xb5, 0xa1, 0x56, 0x45, 0x57, + 0xd6, 0x3e, 0x72, 0x49, 0x41, 0xe7, 0x7a, 0xe3, 0xf4, 0x6b, 0x79, 0x70, 0x3d, 0x06, 0x27, 0x7d, + 0x87, 0x35, 0x69, 0x99, 0xb5, 0x1f, 0x61, 0x89, 0x3d, 0x31, 0xc7, 0x23, 0x1b, 0x0c, 0x63, 0x5f, + 0x1d, 0x83, 0xab, 0x38, 0xa0, 0xdc, 0xe5, 0x44, 0xf5, 0xf6, 0x80, 0x38, 0x61, 0xd6, 0xe3, 0xd7, + 0xe7, 0x0d, 0x61, 0x7e, 0xcc, 0x59, 0x39, 0x20, 0xb1, 0xab, 0x90, 0x06, 0xbd, 0xc7, 0xbf, 0xf3, + 0x4a, 0x8b, 0x36, 0xa7, 0x60, 0x1e, 0xb1, 0x70, 0xa0, 0x40, 0x15, 0x6b, 0x45, 0x67, 0xab, 0x37, + 0xf5, 0x5f, 0xdf, 0x2d, 0x46, 0x6f, 0xca, 0x93, 0x74, 0x27, 0x73, 0x22, 0xf2, 0x18, 0x11, 0xd0, + 0x2c, 0x7b, 0xc5, 0x99, 0xc9, 0xed, 0x5c, 0x2b, 0x1f, 0xe7, 0xb6, 0xba, 0xa1, 0x9b, 0x1b, 0x0a, + 0x30, 0xf7, 0x9f, 0x86, 0x41, 0xb9, 0x7b, 0xf6, 0x64, 0x91, 0xdc, 0xa0, 0xb4, 0xc0, 0x34, 0x13, + 0x67, 0xaa, 0x5a, 0xce, 0xc1, 0x39, 0x8b, 0xb3, 0x7c, 0x03, 0x7d, 0x81, 0xac, 0x23, 0x68, 0xdb, + 0x49, 0xc5, 0xd5, 0x72, 0x0b, 0xbf, 0xb7, 0x46, 0x6b, 0xa6, 0x16, 0xc7, 0x0c, 0x7d, 0x83, 0x42, + 0x86, 0x30, 0x30, 0x47, 0x35, 0x7d, 0xa0, 0xe9, 0xa3, 0x4f, 0xc1, 0x4b, 0x00, 0xc1, 0x7a, 0x0a, + 0x02, 0xf6, 0xa6, 0x2a, 0x5b, 0x52, 0x97, 0x6b, 0x00, 0xed, 0x67, 0xbb, 0x2d, 0x0a, 0xa1, 0xb4, + 0xa8, 0xa9, 0x31, 0x00, 0xb7, 0x99, 0xe1, 0x83, 0x96, 0x95, 0xbd, 0xae, 0x9b, 0x98, 0xe7, 0x5c, + 0x8d, 0xf5, 0xd8, 0x34, 0x0d, 0x15, 0x8b, 0xe6, 0x03, 0x79, 0xa6, 0xf6, 0x26, 0xaf, 0x05, 0x2a, + 0xd5, 0x5c, 0x5c, 0xea, 0x01, 0xf8, 0x06, 0x04, 0x8e, 0x93, 0x7f, 0x87, 0xe0, 0x1e, 0x72, 0x5e, + 0x67, 0x62, 0x03, 0x64, 0xe5, 0x11, 0xaf, 0xd2, 0x88, 0xb2, 0x59, 0x53, 0xe9, 0xad, 0xe3, 0x43, + 0xb5, 0x96, 0x06, 0x86, 0x08, 0x19, 0x0f, 0xa5, 0xc4, 0xdf, 0x11, 0x4c, 0x93, 0xd3, 0xc8, 0xde, + 0xca, 0x92, 0x9c, 0x06, 0x6d, 0x8b, 0xae, 0x5a, 0xc2, 0xd6, 0x07, 0xe3, 0xf9, 0x4d, 0x68, 0xa5, + 0xd3, 0x55, 0x48, 0x27, 0xa6, 0x47, 0x35, 0xa4, 0x3c, 0x46, 0x2b, 0xc3, 0x68, 0x2c, 0xc1, 0x66, + 0x44, 0x11, 0xf5, 0x92, 0xc9, 0x45, 0x6f, 0x53, 0xda, 0x10, 0x26, 0xf5, 0x14, 0x59, 0xa0, 0xcf, + 0x20, 0xcc, 0x17, 0x1b, 0x9b, 0x6b, 0xed, 0xe4, 0x7c, 0xe5, 0x7d, 0x84, 0x5d, 0xff, 0xe1, 0x02, + 0x5c, 0x6e, 0xb2, 0x40, 0x61, 0x5d, 0xa1, 0x51, 0x10, 0x6a, 0x56, 0x01, 0xb7, 0x5c, 0x24, 0xc6, + 0x73, 0xd6, 0xea, 0x81, 0x8d, 0x60, 0xc3, 0x1f, 0x41, 0x4a, 0xea, 0xa5, 0x55, 0x97, 0xb4, 0x0c, + 0xc4, 0xf2, 0xed, 0x2b, 0x38, 0x50, 0xd3, 0x66, 0x08, 0x4a, 0x52, 0x51, 0x34, 0x20, 0xb0, 0x13, + 0x69, 0x5e, 0x2b, 0xfc, 0xb0, 0xdb, 0xfa, 0xd0, 0x01, 0x49, 0x75, 0xc6, 0x74, 0x71, 0xa3, 0x80, + 0x75, 0x28, 0xd1, 0x57, 0x30, 0x80, 0x2a, 0x44, 0x28, 0x84, 0x2c, 0x63, 0x68, 0xc7, 0x26, 0x50, + 0xb3, 0x16, 0x12, 0x65, 0xd6, 0xb8, 0x60, 0x07, 0x26, 0x4c, 0xf0, 0x93, 0xa3, 0x17, 0xfe, 0xe4, + 0xee, 0x38, 0x8e, 0x77, 0x21, 0xa0, 0x24, 0x34, 0xc5, 0x14, 0x32, 0x4c, 0xbf, 0x85, 0xcb, 0x57, + 0xf7, 0x09, 0xb5, 0x3f, 0xdf, 0x69, 0x62, 0x4a, 0xdc, 0x29, 0xb8, 0x55, 0x18, 0xf1, 0xa0, 0x51, + 0xf2, 0x47, 0x3a, 0xd9, 0x38, 0x4d, 0x7a, 0xc5, 0x7c, 0x2a, 0x78, 0x0a, 0xb7, 0x25, 0x06, 0xba, + 0x92, 0x5f, 0xa3, 0x99, 0x92, 0xdd, 0x2d, 0x0b, 0x00, 0xff, 0xd8, 0xc3, 0x86, 0x45, 0xd5, 0x5c, + 0x2c, 0xa2, 0xae, 0x94, 0xcf, 0x4f, 0xfa, 0x37, 0x22, 0x84, 0xa2, 0x8a, 0x13, 0x79, 0x7e, 0x25, + 0xeb, 0x0d, 0x95, 0x0c, 0x08, 0x37, 0x16, 0x56, 0xa8, 0x89, 0xe6, 0x18, 0x9f, 0x83, 0xb9, 0xc0, + 0xc8, 0xe0, 0x69, 0x52, 0xb3, 0x4f, 0xe1, 0x3c, 0xcb, 0x5c, 0x3b, 0x2c, 0x82, 0xf2, 0xd9, 0x88, + 0xf6, 0xd9, 0xa2, 0x33, 0xf1, 0xa9, 0xe6, 0x4d, 0xe9, 0x72, 0x18, 0xbe, 0x12, 0xee, 0x7a, 0x8e, + 0x84, 0x63, 0xd8, 0x21, 0x31, 0x62, 0x4c, 0xe1, 0x67, 0xe7, 0x44, 0xa3, 0xca, 0x39, 0x15, 0xc7, + 0x8e, 0x6e, 0x76, 0x36, 0x2d, 0x06, 0x09, 0x0e, 0x2a, 0x7e, 0xd6, 0x0e, 0xa7, 0x43, 0x7c, 0x84, + 0x83, 0x8d, 0x8a, 0xa7, 0x5f, 0x09, 0x6c, 0x9a, 0x92, 0x8e, 0x40, 0x3e, 0x24, 0x28, 0x55, 0x0c, + 0x98, 0xde, 0x8c, 0x43, 0xbd, 0x25, 0xe2, 0x45, 0x20, 0xae, 0xf6, 0xee, 0x53, 0x4e, 0xe1, 0xd4, + 0x70, 0x21, 0x75, 0xe6, 0xb2, 0x5d, 0x03, 0x0b, 0x87, 0x94, 0x18, 0x45, 0xce, 0xfc, 0x1d, 0xc2, + 0x89, 0xce, 0xe3, 0x3c, 0x72, 0x13, 0x9f, 0x29, 0x83, 0x9a, 0xf8, 0x1c, 0xb6, 0xa0, 0x97, 0xd1, + 0x14, 0x31, 0x1a, 0x01, 0x73, 0x6f, 0x47, 0x9b, 0xda, 0xe3, 0x2a, 0x59, 0x39, 0x8f, 0xc4, 0xa7, + 0x49, 0x4d, 0x03, 0x4f, 0xc8, 0xdc, 0x5f, 0x2b, 0xa8, 0xaf, 0x93, 0xfc, 0x4c, 0x57, 0x6b, 0x70, + 0x39, 0x67, 0xae, 0x59, 0x37, 0x80, 0x41, 0x3b, 0x44, 0xb9, 0x8f, 0x4b, 0xab, 0xa9, 0xd3, 0xfd, + 0x7b, 0x55, 0x71, 0x5a, 0xd5, 0xe5, 0xc4, 0x1f, 0x93, 0x61, 0xa4, 0x2a, 0x75, 0x7d, 0x9a, 0x6d, + 0x72, 0x20, 0xa9, 0x46, 0x7e, 0x19, 0xf7, 0x39, 0x87, 0x70, 0x76, 0x16, 0x4c, 0x14, 0x2d, 0x40, + 0xbb, 0xae, 0x95, 0x01, 0x31, 0x2c, 0x39, 0x4d, 0xc0, 0x23, 0x3d, 0xc5, 0x86, 0x88, 0x14, 0x16, + 0x2b, 0xfc, 0x1f, 0x10, 0xbd, 0x46, 0x63, 0xb2, 0x85, 0xdd, 0x2d, 0x00, 0x5f, 0x3b, 0xc3, 0xda, + 0xd2, 0xff, 0x02, 0x3f, 0x7e, 0x81, 0xb7, 0x99, 0xb1, 0xb3, 0x23, 0xb3, 0x7e, 0x82, 0xfc, 0x99, + 0xdc, 0x81, 0x29, 0x1c, 0xf9, 0x3c, 0xc0, 0x4a, 0x0e, 0x05, 0xaa, 0x67, 0x4b, 0xcf, 0xd3, 0xbc, + 0x0d, 0x93, 0x0a, 0x10, 0xd0, 0x95, 0x7e, 0xc7, 0x71, 0x2b, 0x8c, 0xc7, 0x83, 0x75, 0xdd, 0x90, + 0x4e, 0xb5, 0xa4, 0x68, 0x29, 0x60, 0x15, 0xda, 0xb1, 0xba, 0xbb, 0x07, 0x67, 0x86, 0xf3, 0x05, + 0xc8, 0xad, 0x90, 0xca, 0x39, 0x47, 0xb1, 0x50, 0xda, 0x79, 0xcb, 0x94, 0x03, 0x7e, 0x97, 0x0e, + 0x91, 0x80, 0x43, 0x7e, 0xa3, 0x4c, 0x72, 0x77, 0x1d, 0x67, 0x30, 0x00, 0x82, 0x67, 0x41, 0xfe, + 0x75, 0x9f, 0xcd, 0xc2, 0xb0, 0x35, 0x58, 0x33, 0x1f, 0xdf, 0x5b, 0x89, 0xd6, 0xe3, 0xf2, 0x5a, + 0x05, 0x24, 0x1f, 0x32, 0xf1, 0x39, 0xe8, 0x98, 0x12, 0x6a, 0xec, 0x8b, 0x17, 0x15, 0xca, 0xc0, + 0x20, 0x88, 0x31, 0xfb, 0x12, 0x05, 0xf9, 0xef, 0xb7, 0x55, 0x38, 0x75, 0x5b, 0x2d, 0x83, 0x93, + 0x1c, 0x7a, 0xd9, 0xe2, 0x52, 0xc8, 0x8c, 0x8a, 0xf3, 0xc5, 0xdf, 0x62, 0xfb, 0x99, 0x65, 0x3a, + 0xff, 0x99, 0xe6, 0xc6, 0xc0, 0x51, 0xa9, 0xa1, 0x24, 0x13, 0x81, 0xcd, 0x5c, 0xe1, 0x30, 0x72, + 0x61, 0xf8, 0x66, 0x57, 0x5c, 0xae, 0xa0, 0xa3, 0xe8, 0x47, 0x28, 0x6e, 0xcc, 0x67, 0xd7, 0xd9, + 0xaa, 0x18, 0xf4, 0x8e, 0xf2, 0xa5, 0xe5, 0xf1, 0x83, 0x28, 0x61, 0x27, 0xf8, 0xb9, 0xaa, 0x2c, + 0xaa, 0x08, 0x69, 0xec, 0x5e, 0x47, 0x4a, 0x70, 0xe5, 0x42, 0x7d, 0xc2, 0xf0, 0x48, 0x8b, 0x13, + 0x4d, 0x20, 0x12, 0x41, 0xda, 0xe6, 0x8e, 0xd3, 0x99, 0x68, 0x69, 0x45, 0x32, 0x47, 0xbb, 0x50, + 0xd8, 0xbc, 0x3d, 0x3c, 0x90, 0x99, 0x51, 0xe5, 0xa4, 0x7b, 0x1e, 0x89, 0x96, 0x10, 0x34, 0x7e, + 0xa8, 0xd7, 0x19, 0x33, 0x46, 0xbf, 0xe7, 0x54, 0xc2, 0x89, 0xad, 0x1c, 0xa4, 0x54, 0xb9, 0xc9, + 0x2a, 0x07, 0x52, 0x7b, 0x95, 0xa1, 0xfe, 0x50, 0x8d, 0x0b, 0x7c, 0x8d, 0xa5, 0xb9, 0x04, 0x7d, + 0x27, 0x75, 0x08, 0xff, 0x61, 0x5c, 0x9d, 0xc9, 0xab, 0x11, 0x59, 0x6a, 0xa8, 0x8d, 0x0c, 0x97, + 0x34, 0xa4, 0x5d, 0x81, 0xf0, 0x39, 0x32, 0x19, 0xbe, 0xad, 0x58, 0x7d, 0x3a, 0x6f, 0x9d, 0x07, + 0xc4, 0x70, 0xf2, 0xab, 0xf8, 0xd7, 0xc6, 0x99, 0x22, 0x28, 0xbf, 0x0a, 0xb6, 0xef, 0x79, 0xe4, + 0x65, 0x99, 0xbb, 0x0a, 0x60, +}; diff --git a/libfprint/drivers/validity/validity_fwext.c b/libfprint/drivers/validity/validity_fwext.c new file mode 100644 index 00000000..54dacc52 --- /dev/null +++ b/libfprint/drivers/validity/validity_fwext.c @@ -0,0 +1,693 @@ +/* + * Validity/Synaptics VCSFW firmware extension upload + * + * Copyright (C) 2024 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "validity" + +#include "drivers_api.h" +#include "fpi-byte-utils.h" +#include "validity.h" +#include "validity_fwext.h" +#include "vcsfw_protocol.h" + +#include +#include + +/* ---- Constants ---- */ + +#define FWEXT_CHUNK_SIZE 0x1000 /* 4 KB per write_flash chunk */ +#define FWEXT_SIGNATURE_SIZE 256 /* RSA signature length */ +#define FWEXT_HEADER_DELIMITER 0x1A /* .xpfwext header end marker */ + +#define FWEXT_HW_REG_WRITE_ADDR 0x8000205C +#define FWEXT_HW_REG_WRITE_VALUE 7 +#define FWEXT_HW_REG_READ_ADDR 0x80002080 + +/* Firmware partition */ +#define FWEXT_PARTITION 2 + +/* Reboot command: 0x05 0x02 0x00 */ +#define VCSFW_CMD_REBOOT 0x05 +#define VCSFW_REBOOT_SUBCMD 0x02 + +/* Cleanup command (call_cleanups): 0x1a */ +#define VCSFW_CMD_CLEANUP 0x1A + +/* ---- Firmware file search paths ---- */ + +static const gchar *firmware_search_paths[] = { + "/usr/share/libfprint/validity", + "/var/lib/python-validity", + "/var/run/python-validity", + "/usr/share/python-validity", + NULL, +}; + +/* ---- db_write_enable blob for 06cb:009a (3621 bytes) ---- + * Opaque encrypted blob sent before each flash write. + * The sensor firmware decrypts this internally. + * Extracted from python-validity blobs_9a.py. + * TODO: Iteration 6 (HAL) will consolidate blobs for all PIDs. */ +#include "validity_blob_dbe_009a.inc" + +/* ================================================================ + * Firmware info parsing + * ================================================================ */ + +gboolean +validity_fwext_parse_fw_info (const guint8 *data, + gsize data_len, + guint16 status, + ValidityFwInfo *info) +{ + memset (info, 0, sizeof (*info)); + + /* Status 0xB004 (bytes: 0xb0 0x04) means no firmware loaded. + * python-validity checks: rsp[0]==0xb0 and rsp[1]==4 + * Our vcsfw_cmd_send reads status as uint16 LE from the first 2 bytes, + * so wire bytes {0xb0, 0x04} -> status = 0x04B0 in LE. */ + if (status == VCSFW_STATUS_NO_FW) + { + info->loaded = FALSE; + return TRUE; + } + + if (status != VCSFW_STATUS_OK) + { + fp_warn ("GET_FW_INFO unexpected status: 0x%04x", status); + info->loaded = FALSE; + return FALSE; + } + + /* Response data (after 2-byte status stripped by vcsfw_cmd_send): + * major(2) + minor(2) + modcnt(2) + buildtime(4) = 10 bytes header + * + 12 bytes per module */ + if (data_len < 10) + { + fp_warn ("GET_FW_INFO response too short: %zu", data_len); + info->loaded = FALSE; + return FALSE; + } + + info->loaded = TRUE; + info->major = FP_READ_UINT16_LE (data); + info->minor = FP_READ_UINT16_LE (data + 2); + info->module_count = FP_READ_UINT16_LE (data + 4); + info->buildtime = FP_READ_UINT32_LE (data + 6); + + if (info->module_count > 32) + info->module_count = 32; + + for (guint16 i = 0; i < info->module_count && (10 + (i + 1) * 12) <= data_len; i++) + { + const guint8 *m = data + 10 + i * 12; + info->modules[i].type = FP_READ_UINT16_LE (m); + info->modules[i].subtype = FP_READ_UINT16_LE (m + 2); + info->modules[i].major = FP_READ_UINT16_LE (m + 4); + info->modules[i].minor = FP_READ_UINT16_LE (m + 6); + info->modules[i].size = FP_READ_UINT32_LE (m + 8); + } + + return TRUE; +} + +/* ================================================================ + * Firmware filename mapping + * ================================================================ */ + +const gchar * +validity_fwext_get_firmware_name (guint16 vid, + guint16 pid) +{ + if (vid == 0x138a && pid == 0x0090) + return "6_07f_Lenovo.xpfwext"; + + /* All other supported PIDs use the same firmware */ + if ((vid == 0x138a && (pid == 0x0097 || pid == 0x009d)) || + (vid == 0x06cb && pid == 0x009a)) + return "6_07f_lenovo_mis_qm.xpfwext"; + + return NULL; +} + +gchar * +validity_fwext_find_firmware (guint16 vid, + guint16 pid, + GError **error) +{ + const gchar *filename = validity_fwext_get_firmware_name (vid, pid); + + if (filename == NULL) + { + g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_NOT_SUPPORTED, + "No firmware filename known for %04x:%04x", vid, pid); + return NULL; + } + + for (const gchar **path = firmware_search_paths; *path != NULL; path++) + { + g_autofree gchar *full_path = g_build_filename (*path, filename, NULL); + + if (g_file_test (full_path, G_FILE_TEST_IS_REGULAR)) + { + fp_info ("Found firmware file: %s", full_path); + return g_steal_pointer (&full_path); + } + } + + g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_NOT_FOUND, + "Firmware file '%s' not found. " + "Search paths: /usr/share/libfprint/validity/, " + "/var/lib/python-validity/, /var/run/python-validity/. " + "Extract from Lenovo driver installer (nz3gf09w.exe).", + filename); + return NULL; +} + +/* ================================================================ + * .xpfwext file parser + * ================================================================ */ + +gboolean +validity_fwext_load_file (const gchar *path, + ValidityFwextFile *fwext, + GError **error) +{ + g_autofree guint8 *contents = NULL; + gsize length = 0; + + memset (fwext, 0, sizeof (*fwext)); + + if (!g_file_get_contents (path, (gchar **) &contents, &length, error)) + return FALSE; + + /* Find 0x1A delimiter that ends the header */ + const guint8 *delim = memchr (contents, FWEXT_HEADER_DELIMITER, length); + + if (delim == NULL) + { + g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID, + "Firmware file has no 0x1A header delimiter"); + return FALSE; + } + + gsize header_len = (delim - contents) + 1; /* includes 0x1A itself */ + gsize data_len = length - header_len; + + if (data_len < FWEXT_SIGNATURE_SIZE + 1) + { + g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID, + "Firmware file too short after header (%zu bytes)", data_len); + return FALSE; + } + + /* Split: payload = data[:-256], signature = data[-256:] */ + fwext->payload_len = data_len - FWEXT_SIGNATURE_SIZE; + fwext->payload = g_memdup2 (contents + header_len, fwext->payload_len); + memcpy (fwext->signature, + contents + header_len + fwext->payload_len, + FWEXT_SIGNATURE_SIZE); + + fp_info ("Firmware file loaded: %zu bytes payload, %d bytes signature", + fwext->payload_len, FWEXT_SIGNATURE_SIZE); + + return TRUE; +} + +void +validity_fwext_file_clear (ValidityFwextFile *fwext) +{ + g_clear_pointer (&fwext->payload, g_free); + fwext->payload_len = 0; +} + +/* ================================================================ + * Command builders + * ================================================================ */ + +void +validity_fwext_build_write_hw_reg32 (guint32 addr, + guint32 value, + guint8 *cmd, + gsize *cmd_len) +{ + /* pack('cmd_response_status and self->cmd_response_data. + * ================================================================ */ + +/* SSM data for the upload state machine */ +typedef struct +{ + ValidityFwextFile fwext; + gsize write_offset; /* current offset into payload */ + guint16 vid; + guint16 pid; +} FwextUploadData; + +static void +fwext_upload_data_free (gpointer data) +{ + FwextUploadData *ud = data; + + validity_fwext_file_clear (&ud->fwext); + g_free (ud); +} + +void +validity_fwext_upload_run_state (FpiSsm *ssm, + FpDevice *dev) +{ + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + FwextUploadData *ud = fpi_ssm_get_data (ssm); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case FWEXT_SEND_WRITE_HW_REG: + { + guint8 cmd[10]; + gsize cmd_len; + + validity_fwext_build_write_hw_reg32 (FWEXT_HW_REG_WRITE_ADDR, + FWEXT_HW_REG_WRITE_VALUE, + cmd, &cmd_len); + vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL); + } + break; + + case FWEXT_RECV_WRITE_HW_REG: + if (self->cmd_response_status != VCSFW_STATUS_OK) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "WRITE_HW_REG failed: status=0x%04x", + self->cmd_response_status)); + return; + } + fpi_ssm_next_state (ssm); + break; + + case FWEXT_SEND_READ_HW_REG: + { + guint8 cmd[6]; + gsize cmd_len; + + validity_fwext_build_read_hw_reg32 (FWEXT_HW_REG_READ_ADDR, + cmd, &cmd_len); + vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL); + } + break; + + case FWEXT_RECV_READ_HW_REG: + { + if (self->cmd_response_status != VCSFW_STATUS_OK) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "READ_HW_REG failed: status=0x%04x", + self->cmd_response_status)); + return; + } + + guint32 value; + + if (!validity_fwext_parse_read_hw_reg32 (self->cmd_response_data, + self->cmd_response_len, + &value)) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "READ_HW_REG response too short")); + return; + } + + if (value != 2 && value != 3) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Unexpected HW register value: %u " + "(expected 2 or 3)", value)); + return; + } + + fp_dbg ("FWEXT: HW register 0x%08x = %u (OK)", FWEXT_HW_REG_READ_ADDR, value); + fpi_ssm_next_state (ssm); + } + break; + + case FWEXT_LOAD_FILE: + { + GError *error = NULL; + GUsbDevice *usb_dev = fpi_device_get_usb_device (dev); + guint16 vid = g_usb_device_get_vid (usb_dev); + guint16 pid = g_usb_device_get_pid (usb_dev); + + ud->vid = vid; + ud->pid = pid; + + g_autofree gchar *fw_path = validity_fwext_find_firmware (vid, pid, &error); + + if (fw_path == NULL) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + if (!validity_fwext_load_file (fw_path, &ud->fwext, &error)) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + ud->write_offset = 0; + fp_info ("FWEXT: Loaded firmware file, %zu bytes to write", + ud->fwext.payload_len); + fpi_ssm_next_state (ssm); + } + break; + + case FWEXT_SEND_DB_WRITE_ENABLE: + { + gsize dbe_len; + const guint8 *dbe = validity_fwext_get_db_write_enable (ud->vid, + ud->pid, + &dbe_len); + + if (dbe == NULL || dbe_len == 0) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED, + "No db_write_enable blob for " + "%04x:%04x", ud->vid, ud->pid)); + return; + } + + vcsfw_cmd_send (self, ssm, dbe, dbe_len, NULL); + } + break; + + case FWEXT_RECV_DB_WRITE_ENABLE: + if (self->cmd_response_status != VCSFW_STATUS_OK) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "db_write_enable failed: " + "status=0x%04x", + self->cmd_response_status)); + return; + } + fpi_ssm_next_state (ssm); + break; + + case FWEXT_SEND_WRITE_CHUNK: + { + gsize remaining = ud->fwext.payload_len - ud->write_offset; + gsize chunk_size = MIN (remaining, FWEXT_CHUNK_SIZE); + + /* cmd buffer: 13-byte header + payload */ + g_autofree guint8 *cmd = g_malloc (13 + chunk_size); + gsize cmd_len; + + validity_fwext_build_write_flash (FWEXT_PARTITION, + (guint32) ud->write_offset, + ud->fwext.payload + ud->write_offset, + chunk_size, + cmd, &cmd_len); + + fp_dbg ("FWEXT: Writing chunk at offset 0x%zx (%zu/%zu bytes)", + ud->write_offset, ud->write_offset + chunk_size, + ud->fwext.payload_len); + + ud->write_offset += chunk_size; + + vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL); + } + break; + + case FWEXT_RECV_WRITE_CHUNK: + if (self->cmd_response_status != VCSFW_STATUS_OK) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "WRITE_FLASH chunk failed: " + "status=0x%04x", + self->cmd_response_status)); + return; + } + fpi_ssm_next_state (ssm); + break; + + case FWEXT_SEND_CLEANUP: + { + guint8 cmd[] = { VCSFW_CMD_CLEANUP }; + + vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL); + } + break; + + case FWEXT_RECV_CLEANUP: + /* Status 0x0491 means "nothing to commit" -- not an error */ + if (self->cmd_response_status != VCSFW_STATUS_OK && + self->cmd_response_status != 0x0491) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Cleanup cmd failed: " + "status=0x%04x", + self->cmd_response_status)); + return; + } + + if (ud->write_offset < ud->fwext.payload_len) + { + /* More chunks to write -- loop back to db_write_enable */ + fpi_ssm_jump_to_state (ssm, FWEXT_SEND_DB_WRITE_ENABLE); + } + else + { + /* All chunks written -- proceed to signature */ + fpi_ssm_next_state (ssm); + } + break; + + case FWEXT_SEND_WRITE_SIGNATURE: + { + guint8 cmd[5 + FWEXT_SIGNATURE_SIZE]; + gsize cmd_len; + + validity_fwext_build_write_fw_sig (FWEXT_PARTITION, + ud->fwext.signature, + FWEXT_SIGNATURE_SIZE, + cmd, &cmd_len); + + fp_info ("FWEXT: Writing firmware signature (%d bytes)", + FWEXT_SIGNATURE_SIZE); + + vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL); + } + break; + + case FWEXT_RECV_WRITE_SIGNATURE: + if (self->cmd_response_status != VCSFW_STATUS_OK) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "WRITE_FW_SIG failed: " + "status=0x%04x", + self->cmd_response_status)); + return; + } + fpi_ssm_next_state (ssm); + break; + + case FWEXT_SEND_VERIFY: + { + guint8 cmd[] = { VCSFW_CMD_GET_FW_INFO, FWEXT_PARTITION }; + + vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL); + } + break; + + case FWEXT_RECV_VERIFY: + { + ValidityFwInfo info; + + if (!validity_fwext_parse_fw_info (self->cmd_response_data, + self->cmd_response_len, + self->cmd_response_status, + &info) || + !info.loaded) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Firmware not detected after upload")); + return; + } + + fp_info ("FWEXT: Upload verified -- firmware v%d.%d, %d modules", + info.major, info.minor, info.module_count); + fpi_ssm_next_state (ssm); + } + break; + + case FWEXT_SEND_REBOOT: + { + guint8 cmd[3]; + gsize cmd_len; + + validity_fwext_build_reboot (cmd, &cmd_len); + + fp_info ("FWEXT: Rebooting sensor to activate new firmware"); + + vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL); + } + break; + + case FWEXT_RECV_REBOOT: + /* Sensor will disconnect and re-enumerate on USB. + * We mark SSM completed -- the caller (open sequence) + * handles the post-reboot re-init. */ + fp_info ("FWEXT: Reboot sent. Device will re-enumerate."); + fpi_ssm_mark_completed (ssm); + break; + } +} + +/* ---- SSM factory ---- */ + +FpiSsm * +validity_fwext_upload_ssm_new (FpDevice *dev) +{ + FpiSsm *ssm; + FwextUploadData *ud; + + ssm = fpi_ssm_new (dev, validity_fwext_upload_run_state, + FWEXT_NUM_STATES); + ud = g_new0 (FwextUploadData, 1); + fpi_ssm_set_data (ssm, ud, fwext_upload_data_free); + + return ssm; +} diff --git a/libfprint/drivers/validity/validity_fwext.h b/libfprint/drivers/validity/validity_fwext.h new file mode 100644 index 00000000..e6ee31b8 --- /dev/null +++ b/libfprint/drivers/validity/validity_fwext.h @@ -0,0 +1,144 @@ +/* + * Validity/Synaptics VCSFW firmware extension upload + * + * Copyright (C) 2024 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include + +/* ---- Firmware info from GET_FW_INFO response ---- */ + +typedef struct +{ + guint16 type; + guint16 subtype; + guint16 major; + guint16 minor; + guint32 size; +} ValidityFwModule; + +typedef struct +{ + gboolean loaded; + guint16 major; + guint16 minor; + guint32 buildtime; + guint16 module_count; + ValidityFwModule modules[32]; +} ValidityFwInfo; + +/* ---- Parsed firmware extension file ---- */ + +typedef struct +{ + guint8 *payload; /* firmware data (without header, without signature) */ + gsize payload_len; + guint8 signature[256]; /* 256-byte RSA signature */ +} ValidityFwextFile; + +/* ---- Firmware upload SSM states ---- + * + * Each command has a SEND and RECV pair: + * - SEND: calls vcsfw_cmd_send(self, ssm, cmd, len, NULL) + * which starts a child SSM (subsm). The parent is paused. + * - RECV: entered automatically when child completes. Checks + * self->cmd_response_status and self->cmd_response_data. + * Advances or loops as needed. + */ +typedef enum { + FWEXT_SEND_WRITE_HW_REG = 0, + FWEXT_RECV_WRITE_HW_REG, + FWEXT_SEND_READ_HW_REG, + FWEXT_RECV_READ_HW_REG, + FWEXT_LOAD_FILE, + FWEXT_SEND_DB_WRITE_ENABLE, + FWEXT_RECV_DB_WRITE_ENABLE, + FWEXT_SEND_WRITE_CHUNK, + FWEXT_RECV_WRITE_CHUNK, + FWEXT_SEND_CLEANUP, + FWEXT_RECV_CLEANUP, + FWEXT_SEND_WRITE_SIGNATURE, + FWEXT_RECV_WRITE_SIGNATURE, + FWEXT_SEND_VERIFY, + FWEXT_RECV_VERIFY, + FWEXT_SEND_REBOOT, + FWEXT_RECV_REBOOT, + FWEXT_NUM_STATES, +} ValidityFwextSsmState; + +/* ---- API ---- */ + +gboolean validity_fwext_parse_fw_info (const guint8 *data, + gsize data_len, + guint16 status, + ValidityFwInfo *info); + +gboolean validity_fwext_load_file (const gchar *filename, + ValidityFwextFile *fwext, + GError **error); + +void validity_fwext_file_clear (ValidityFwextFile *fwext); + +const gchar *validity_fwext_get_firmware_name (guint16 vid, + guint16 pid); + +gchar *validity_fwext_find_firmware (guint16 vid, + guint16 pid, + GError **error); + +void validity_fwext_build_write_hw_reg32 (guint32 addr, + guint32 value, + guint8 *cmd, + gsize *cmd_len); + +void validity_fwext_build_read_hw_reg32 (guint32 addr, + guint8 *cmd, + gsize *cmd_len); + +gboolean validity_fwext_parse_read_hw_reg32 (const guint8 *data, + gsize data_len, + guint32 *value); + +void validity_fwext_build_write_flash (guint8 partition, + guint32 offset, + const guint8 *data, + gsize data_len, + guint8 *cmd, + gsize *cmd_len); + +void validity_fwext_build_write_fw_sig (guint8 partition, + const guint8 *signature, + gsize sig_len, + guint8 *cmd, + gsize *cmd_len); + +void validity_fwext_build_reboot (guint8 *cmd, + gsize *cmd_len); + +const guint8 *validity_fwext_get_db_write_enable (guint16 vid, + guint16 pid, + gsize *len); + +/* SSM entry point for upload state machine */ +void validity_fwext_upload_run_state (FpiSsm *ssm, + FpDevice *dev); + +/* Create the fwext upload SSM with data attached. + * Caller starts it via fpi_ssm_start(). */ +FpiSsm *validity_fwext_upload_ssm_new (FpDevice *dev); diff --git a/libfprint/drivers/validity/vcsfw_protocol.c b/libfprint/drivers/validity/vcsfw_protocol.c index 22b8ae20..ca4faaa4 100644 --- a/libfprint/drivers/validity/vcsfw_protocol.c +++ b/libfprint/drivers/validity/vcsfw_protocol.c @@ -90,7 +90,8 @@ cmd_receive_cb (FpiUsbTransfer *transfer, fp_dbg ("VCSFW response: status=0x%04x, len=%" G_GSSIZE_FORMAT, status, transfer->actual_length - 2); - /* Stash raw response for direct access if needed */ + /* Stash status and data for direct access in RECV states */ + self->cmd_response_status = status; g_clear_pointer (&self->cmd_response_data, g_free); if (transfer->actual_length > 2) { diff --git a/libfprint/meson.build b/libfprint/meson.build index 376bdf14..2f912138 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -156,7 +156,8 @@ driver_sources = { 'validity' : [ 'drivers/validity/validity.c', 'drivers/validity/vcsfw_protocol.c', - 'drivers/validity/validity_tls.c' ], + 'drivers/validity/validity_tls.c', + 'drivers/validity/validity_fwext.c' ], } helper_sources = { diff --git a/tests/meson.build b/tests/meson.build index 66b7d7af..551a36fd 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -339,6 +339,20 @@ if 'validity' in supported_drivers env: envs, ) endif + + # Validity fwext unit tests (no OpenSSL needed) + validity_fwext_test = executable('test-validity-fwext', + sources: 'test-validity-fwext.c', + dependencies: [ libfprint_private_dep ], + c_args: common_cflags, + link_with: libfprint_drivers, + install: false, + ) + test('validity-fwext', + validity_fwext_test, + suite: ['unit-tests'], + env: envs, + ) endif # Run udev rule generator with fatal warnings diff --git a/tests/test-validity-fwext.c b/tests/test-validity-fwext.c new file mode 100644 index 00000000..12d8bcf4 --- /dev/null +++ b/tests/test-validity-fwext.c @@ -0,0 +1,643 @@ +/* + * Unit tests for validity firmware extension upload functions + * + * Copyright (C) 2024 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + */ + +#include +#include +#include + +#include "fpi-device.h" +#include "fpi-ssm.h" +#include "fpi-byte-utils.h" + +#include "drivers/validity/validity_fwext.h" +#include "drivers/validity/vcsfw_protocol.h" + +/* ================================================================ + * T3.1: test_fw_info_parse_present + * + * Verify that a valid GET_FW_INFO response (status=OK) with 1 module + * is parsed correctly into ValidityFwInfo. + * ================================================================ */ +static void +test_fw_info_parse_present (void) +{ + ValidityFwInfo info; + + /* Build a synthetic GET_FW_INFO response: + * major(2) + minor(2) + modcnt(2) + buildtime(4) = 10 header bytes + * + 1 module * 12 bytes = 12 + * Total = 22 bytes (data after 2-byte status, which is stripped) */ + guint8 data[22]; + + memset (data, 0, sizeof (data)); + + /* major = 6 */ + data[0] = 6; + data[1] = 0; + /* minor = 7 */ + data[2] = 7; + data[3] = 0; + /* module_count = 1 */ + data[4] = 1; + data[5] = 0; + /* buildtime = 0x12345678 LE */ + data[6] = 0x78; + data[7] = 0x56; + data[8] = 0x34; + data[9] = 0x12; + /* Module 0: type=3, subtype=4, major=1, minor=2, size=0x1000 */ + data[10] = 3; + data[11] = 0; + data[12] = 4; + data[13] = 0; + data[14] = 1; + data[15] = 0; + data[16] = 2; + data[17] = 0; + data[18] = 0x00; + data[19] = 0x10; + data[20] = 0x00; + data[21] = 0x00; + + gboolean ok = validity_fwext_parse_fw_info (data, sizeof (data), + VCSFW_STATUS_OK, &info); + + g_assert_true (ok); + g_assert_true (info.loaded); + g_assert_cmpuint (info.major, ==, 6); + g_assert_cmpuint (info.minor, ==, 7); + g_assert_cmpuint (info.module_count, ==, 1); + g_assert_cmpuint (info.buildtime, ==, 0x12345678); + g_assert_cmpuint (info.modules[0].type, ==, 3); + g_assert_cmpuint (info.modules[0].subtype, ==, 4); + g_assert_cmpuint (info.modules[0].major, ==, 1); + g_assert_cmpuint (info.modules[0].minor, ==, 2); + g_assert_cmpuint (info.modules[0].size, ==, 0x1000); +} + +/* ================================================================ + * T3.2: test_fw_info_parse_absent + * + * Verify that status VCSFW_STATUS_NO_FW sets loaded=FALSE. + * ================================================================ */ +static void +test_fw_info_parse_absent (void) +{ + ValidityFwInfo info; + + gboolean ok = validity_fwext_parse_fw_info (NULL, 0, + VCSFW_STATUS_NO_FW, &info); + + g_assert_true (ok); + g_assert_false (info.loaded); +} + +/* ================================================================ + * T3.2b: test_fw_info_parse_unknown_status + * + * Verify that an unexpected status returns FALSE. + * ================================================================ */ +static void +test_fw_info_parse_unknown_status (void) +{ + ValidityFwInfo info; + + g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_WARNING, + "*unexpected status*"); + + gboolean ok = validity_fwext_parse_fw_info (NULL, 0, 0x9999, &info); + + g_test_assert_expected_messages (); + + g_assert_false (ok); + g_assert_false (info.loaded); +} + +/* ================================================================ + * T3.2c: test_fw_info_parse_truncated + * + * Verify that status=OK but data too short returns FALSE. + * ================================================================ */ +static void +test_fw_info_parse_truncated (void) +{ + ValidityFwInfo info; + guint8 data[5] = { 0 }; + + g_test_expect_message ("libfprint-validity", G_LOG_LEVEL_WARNING, + "*too short*"); + + gboolean ok = validity_fwext_parse_fw_info (data, sizeof (data), + VCSFW_STATUS_OK, &info); + + g_test_assert_expected_messages (); + + g_assert_false (ok); +} + +/* ================================================================ + * T3.3: test_xpfwext_file_parse + * + * Create a synthetic .xpfwext file in a temp dir and verify parsing. + * Format: header + 0x1A + payload + 256-byte signature + * ================================================================ */ +static void +test_xpfwext_file_parse (void) +{ + g_autoptr (GError) error = NULL; + ValidityFwextFile fwext; + gchar *tmpdir; + g_autofree gchar *path = NULL; + + tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); + g_assert_no_error (error); + + path = g_build_filename (tmpdir, "test.xpfwext", NULL); + + /* Build file: "HDR" + 0x1A + 8 bytes payload + 256 bytes sig */ + gsize header_len = 4; /* "HDR" + 0x1A */ + gsize payload_len = 8; + gsize sig_len = 256; + gsize total = header_len + payload_len + sig_len; + + guint8 *content = g_malloc0 (total); + + content[0] = 'H'; + content[1] = 'D'; + content[2] = 'R'; + content[3] = 0x1A; + + /* Payload: 0x01..0x08 */ + for (gsize i = 0; i < payload_len; i++) + content[header_len + i] = (guint8) (i + 1); + + /* Signature: 0xAA repeated */ + memset (content + header_len + payload_len, 0xAA, sig_len); + + g_file_set_contents (path, (gchar *) content, total, &error); + g_assert_no_error (error); + + gboolean ok = validity_fwext_load_file (path, &fwext, &error); + g_assert_no_error (error); + g_assert_true (ok); + + g_assert_cmpuint (fwext.payload_len, ==, payload_len); + + /* Check payload content */ + for (gsize i = 0; i < payload_len; i++) + g_assert_cmpuint (fwext.payload[i], ==, i + 1); + + /* Check signature */ + for (gsize i = 0; i < sig_len; i++) + g_assert_cmpuint (fwext.signature[i], ==, 0xAA); + + validity_fwext_file_clear (&fwext); + g_assert_null (fwext.payload); + g_assert_cmpuint (fwext.payload_len, ==, 0); + + /* Cleanup */ + g_unlink (path); + g_rmdir (tmpdir); + g_free (content); + g_free (tmpdir); +} + +/* ================================================================ + * T3.3b: test_xpfwext_file_no_delimiter + * + * Verify that a file without 0x1A delimiter fails to parse. + * ================================================================ */ +static void +test_xpfwext_file_no_delimiter (void) +{ + g_autoptr (GError) error = NULL; + ValidityFwextFile fwext; + gchar *tmpdir; + g_autofree gchar *path = NULL; + + tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); + g_assert_no_error (error); + + path = g_build_filename (tmpdir, "bad.xpfwext", NULL); + + /* All 0xFF — no 0x1A delimiter */ + guint8 content[300]; + + memset (content, 0xFF, sizeof (content)); + + g_file_set_contents (path, (gchar *) content, sizeof (content), &error); + g_assert_no_error (error); + + gboolean ok = validity_fwext_load_file (path, &fwext, &error); + g_assert_false (ok); + g_assert_nonnull (error); + + g_unlink (path); + g_rmdir (tmpdir); + g_free (tmpdir); +} + +/* ================================================================ + * T3.3c: test_xpfwext_file_too_short + * + * Verify that a file with valid header but data shorter than + * signature size fails. + * ================================================================ */ +static void +test_xpfwext_file_too_short (void) +{ + g_autoptr (GError) error = NULL; + ValidityFwextFile fwext; + gchar *tmpdir; + g_autofree gchar *path = NULL; + + tmpdir = g_dir_make_tmp ("fwext-test-XXXXXX", &error); + g_assert_no_error (error); + + path = g_build_filename (tmpdir, "short.xpfwext", NULL); + + /* "X" + 0x1A + 10 bytes (< 257 needed for sig + 1 byte payload) */ + guint8 content[12]; + + content[0] = 'X'; + content[1] = 0x1A; + memset (content + 2, 0, 10); + + g_file_set_contents (path, (gchar *) content, sizeof (content), &error); + g_assert_no_error (error); + + gboolean ok = validity_fwext_load_file (path, &fwext, &error); + g_assert_false (ok); + g_assert_nonnull (error); + + g_unlink (path); + g_rmdir (tmpdir); + g_free (tmpdir); +} + +/* ================================================================ + * T3.4: test_flash_write_cmd_format + * + * Verify that build_write_flash produces the correct wire format: + * [0x41, partition, 1, 0, 0, offset_LE32, len_LE32, data...] + * ================================================================ */ +static void +test_flash_write_cmd_format (void) +{ + guint8 payload[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + guint8 cmd[13 + sizeof (payload)]; + gsize cmd_len; + + validity_fwext_build_write_flash (2, 0x1000, payload, sizeof (payload), + cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 13 + sizeof (payload)); + g_assert_cmpuint (cmd[0], ==, 0x41); /* WRITE_FLASH command */ + g_assert_cmpuint (cmd[1], ==, 2); /* partition */ + g_assert_cmpuint (cmd[2], ==, 1); /* flag */ + g_assert_cmpuint (cmd[3], ==, 0); /* reserved low */ + g_assert_cmpuint (cmd[4], ==, 0); /* reserved high */ + + /* offset = 0x1000 LE */ + g_assert_cmpuint (cmd[5], ==, 0x00); + g_assert_cmpuint (cmd[6], ==, 0x10); + g_assert_cmpuint (cmd[7], ==, 0x00); + g_assert_cmpuint (cmd[8], ==, 0x00); + + /* length = 4 LE */ + g_assert_cmpuint (cmd[9], ==, 0x04); + g_assert_cmpuint (cmd[10], ==, 0x00); + g_assert_cmpuint (cmd[11], ==, 0x00); + g_assert_cmpuint (cmd[12], ==, 0x00); + + /* data */ + g_assert_cmpmem (cmd + 13, sizeof (payload), payload, sizeof (payload)); +} + +/* ================================================================ + * T3.5: test_fw_sig_cmd_format + * + * Verify that build_write_fw_sig produces the correct wire format: + * [0x42, partition, 0, len_LE16, signature...] + * ================================================================ */ +static void +test_fw_sig_cmd_format (void) +{ + guint8 sig[256]; + guint8 cmd[5 + 256]; + gsize cmd_len; + + memset (sig, 0xBB, sizeof (sig)); + + validity_fwext_build_write_fw_sig (2, sig, sizeof (sig), cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 5 + 256); + g_assert_cmpuint (cmd[0], ==, 0x42); /* WRITE_FW_SIG command */ + g_assert_cmpuint (cmd[1], ==, 2); /* partition */ + g_assert_cmpuint (cmd[2], ==, 0); /* reserved */ + + /* sig length = 256 LE */ + g_assert_cmpuint (cmd[3], ==, 0x00); + g_assert_cmpuint (cmd[4], ==, 0x01); + + g_assert_cmpmem (cmd + 5, 256, sig, 256); +} + +/* ================================================================ + * T3.6: test_chunk_iteration + * + * Verify that building write_flash with increasing offsets covers + * the entire payload. Simulate the chunk loop used by the SSM. + * ================================================================ */ +static void +test_chunk_iteration (void) +{ + gsize payload_len = 0x2800; /* 10 KB = 2.5 chunks of 4 KB */ + gsize write_offset = 0; + guint chunk_count = 0; + const gsize CHUNK_SIZE = 0x1000; + + while (write_offset < payload_len) + { + gsize remaining = payload_len - write_offset; + gsize chunk_size = MIN (remaining, CHUNK_SIZE); + + g_assert_cmpuint (chunk_size, >, 0); + g_assert_cmpuint (chunk_size, <=, CHUNK_SIZE); + + write_offset += chunk_size; + chunk_count++; + } + + g_assert_cmpuint (write_offset, ==, payload_len); + g_assert_cmpuint (chunk_count, ==, 3); /* 4096 + 4096 + 2048 */ +} + +/* ================================================================ + * T3.7: test_hw_reg_cmd_format + * + * Verify WRITE_HW_REG32 and READ_HW_REG32 command formats. + * ================================================================ */ +static void +test_hw_reg_write_cmd_format (void) +{ + guint8 cmd[10]; + gsize cmd_len; + + validity_fwext_build_write_hw_reg32 (0x8000205C, 7, cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 10); + g_assert_cmpuint (cmd[0], ==, 0x08); /* WRITE_HW_REG32 command */ + + /* address = 0x8000205C LE */ + g_assert_cmpuint (cmd[1], ==, 0x5C); + g_assert_cmpuint (cmd[2], ==, 0x20); + g_assert_cmpuint (cmd[3], ==, 0x00); + g_assert_cmpuint (cmd[4], ==, 0x80); + + /* value = 7 LE */ + g_assert_cmpuint (cmd[5], ==, 0x07); + g_assert_cmpuint (cmd[6], ==, 0x00); + g_assert_cmpuint (cmd[7], ==, 0x00); + g_assert_cmpuint (cmd[8], ==, 0x00); + + /* size = 4 */ + g_assert_cmpuint (cmd[9], ==, 4); +} + +static void +test_hw_reg_read_cmd_format (void) +{ + guint8 cmd[6]; + gsize cmd_len; + + validity_fwext_build_read_hw_reg32 (0x80002080, cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 6); + g_assert_cmpuint (cmd[0], ==, 0x07); /* READ_HW_REG32 command */ + + /* address = 0x80002080 LE */ + g_assert_cmpuint (cmd[1], ==, 0x80); + g_assert_cmpuint (cmd[2], ==, 0x20); + g_assert_cmpuint (cmd[3], ==, 0x00); + g_assert_cmpuint (cmd[4], ==, 0x80); + + /* size = 4 */ + g_assert_cmpuint (cmd[5], ==, 4); +} + +static void +test_hw_reg_read_parse (void) +{ + guint32 value; + guint8 data[] = { 0x02, 0x00, 0x00, 0x00 }; + + gboolean ok = validity_fwext_parse_read_hw_reg32 (data, sizeof (data), + &value); + g_assert_true (ok); + g_assert_cmpuint (value, ==, 2); + + /* Too short should fail */ + ok = validity_fwext_parse_read_hw_reg32 (data, 3, &value); + g_assert_false (ok); +} + +/* ================================================================ + * T3.8: test_firmware_filename + * + * Verify firmware filename mapping for known PIDs. + * ================================================================ */ +static void +test_firmware_filename (void) +{ + const gchar *name; + + name = validity_fwext_get_firmware_name (0x06cb, 0x009a); + g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); + + name = validity_fwext_get_firmware_name (0x138a, 0x0090); + g_assert_cmpstr (name, ==, "6_07f_Lenovo.xpfwext"); + + name = validity_fwext_get_firmware_name (0x138a, 0x0097); + g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); + + name = validity_fwext_get_firmware_name (0x138a, 0x009d); + g_assert_cmpstr (name, ==, "6_07f_lenovo_mis_qm.xpfwext"); + + /* Unknown PID should return NULL */ + name = validity_fwext_get_firmware_name (0x1234, 0x5678); + g_assert_null (name); +} + +/* ================================================================ + * T3.9: test_missing_firmware_file + * + * Verify that find_firmware returns an error when the file is not + * found in any search path. + * ================================================================ */ +static void +test_missing_firmware_file (void) +{ + g_autoptr (GError) error = NULL; + + /* This should fail since firmware files aren't installed in CI */ + g_autofree gchar *path = validity_fwext_find_firmware (0x06cb, 0x009a, + &error); + + /* It either found a file or returned an error — both are valid. + * In CI, it should be an error. On a real system, it might succeed. */ + if (path == NULL) + { + g_assert_nonnull (error); + g_assert_true (g_error_matches (error, FP_DEVICE_ERROR, + FP_DEVICE_ERROR_DATA_NOT_FOUND)); + } + else + { + g_assert_no_error (error); + g_assert_true (g_file_test (path, G_FILE_TEST_IS_REGULAR)); + } +} + +/* ================================================================ + * T3.9b: test_unsupported_pid_firmware + * + * Verify that find_firmware for an unknown PID returns NOT_SUPPORTED. + * ================================================================ */ +static void +test_unsupported_pid_firmware (void) +{ + g_autoptr (GError) error = NULL; + g_autofree gchar *path = validity_fwext_find_firmware (0x1234, 0x5678, + &error); + + g_assert_null (path); + g_assert_nonnull (error); + g_assert_true (g_error_matches (error, FP_DEVICE_ERROR, + FP_DEVICE_ERROR_NOT_SUPPORTED)); +} + +/* ================================================================ + * T3.10: test_db_write_enable_blob + * + * Verify that db_write_enable blob is returned for supported PID + * and NULL for unsupported PID. + * ================================================================ */ +static void +test_db_write_enable_blob (void) +{ + gsize len; + const guint8 *blob; + + blob = validity_fwext_get_db_write_enable (0x06cb, 0x009a, &len); + g_assert_nonnull (blob); + g_assert_cmpuint (len, ==, 3621); + + blob = validity_fwext_get_db_write_enable (0x1234, 0x5678, &len); + g_assert_null (blob); + g_assert_cmpuint (len, ==, 0); +} + +/* ================================================================ + * T3.11: test_reboot_cmd_format + * + * Verify reboot command: 0x05, 0x02, 0x00 + * ================================================================ */ +static void +test_reboot_cmd_format (void) +{ + guint8 cmd[3]; + gsize cmd_len; + + validity_fwext_build_reboot (cmd, &cmd_len); + + g_assert_cmpuint (cmd_len, ==, 3); + g_assert_cmpuint (cmd[0], ==, 0x05); + g_assert_cmpuint (cmd[1], ==, 0x02); + g_assert_cmpuint (cmd[2], ==, 0x00); +} + +/* ================================================================ + * Regression: fwext_file_clear on already-cleared struct + * + * Double-free guard. + * ================================================================ */ +static void +test_file_clear_idempotent (void) +{ + ValidityFwextFile fwext = { 0 }; + + /* Clear an empty struct — should not crash */ + validity_fwext_file_clear (&fwext); + validity_fwext_file_clear (&fwext); + + g_assert_null (fwext.payload); + g_assert_cmpuint (fwext.payload_len, ==, 0); +} + +int +main (int argc, + char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* Firmware info parsing */ + g_test_add_func ("/validity/fwext/fw-info/parse-present", + test_fw_info_parse_present); + g_test_add_func ("/validity/fwext/fw-info/parse-absent", + test_fw_info_parse_absent); + g_test_add_func ("/validity/fwext/fw-info/parse-unknown-status", + test_fw_info_parse_unknown_status); + g_test_add_func ("/validity/fwext/fw-info/parse-truncated", + test_fw_info_parse_truncated); + + /* File parsing */ + g_test_add_func ("/validity/fwext/file/parse", + test_xpfwext_file_parse); + g_test_add_func ("/validity/fwext/file/no-delimiter", + test_xpfwext_file_no_delimiter); + g_test_add_func ("/validity/fwext/file/too-short", + test_xpfwext_file_too_short); + g_test_add_func ("/validity/fwext/file/clear-idempotent", + test_file_clear_idempotent); + + /* Command format */ + g_test_add_func ("/validity/fwext/cmd/write-flash", + test_flash_write_cmd_format); + g_test_add_func ("/validity/fwext/cmd/write-fw-sig", + test_fw_sig_cmd_format); + g_test_add_func ("/validity/fwext/cmd/write-hw-reg", + test_hw_reg_write_cmd_format); + g_test_add_func ("/validity/fwext/cmd/read-hw-reg", + test_hw_reg_read_cmd_format); + g_test_add_func ("/validity/fwext/cmd/read-hw-reg-parse", + test_hw_reg_read_parse); + g_test_add_func ("/validity/fwext/cmd/reboot", + test_reboot_cmd_format); + + /* Chunk iteration */ + g_test_add_func ("/validity/fwext/chunk-iteration", + test_chunk_iteration); + + /* Firmware filename mapping */ + g_test_add_func ("/validity/fwext/firmware-name", + test_firmware_filename); + g_test_add_func ("/validity/fwext/find-firmware/missing", + test_missing_firmware_file); + g_test_add_func ("/validity/fwext/find-firmware/unsupported-pid", + test_unsupported_pid_firmware); + + /* Blob lookup */ + g_test_add_func ("/validity/fwext/db-write-enable", + test_db_write_enable_blob); + + return g_test_run (); +}