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
This commit is contained in:
Leonardo Francisco 2026-04-04 23:40:40 -04:00 committed by Leonardo
parent 5cee45025a
commit 4ade76ea90
9 changed files with 1788 additions and 4 deletions

View file

@ -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);

View file

@ -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;
};

View file

@ -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,
};

View file

@ -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 <gio/gio.h>
#include <string.h>
/* ---- 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('<BLLB', 0x08, addr, val, 4) = 10 bytes */
cmd[0] = VCSFW_CMD_WRITE_HW_REG32;
FP_WRITE_UINT32_LE (cmd + 1, addr);
FP_WRITE_UINT32_LE (cmd + 5, value);
cmd[9] = 4;
*cmd_len = 10;
}
void
validity_fwext_build_read_hw_reg32 (guint32 addr,
guint8 *cmd,
gsize *cmd_len)
{
/* pack('<BLB', 0x07, addr, 4) = 6 bytes */
cmd[0] = VCSFW_CMD_READ_HW_REG32;
FP_WRITE_UINT32_LE (cmd + 1, addr);
cmd[5] = 4;
*cmd_len = 6;
}
gboolean
validity_fwext_parse_read_hw_reg32 (const guint8 *data,
gsize data_len,
guint32 *value)
{
/* Response data (after 2-byte status): uint32 LE value */
if (data_len < 4)
return FALSE;
*value = FP_READ_UINT32_LE (data);
return TRUE;
}
void
validity_fwext_build_write_flash (guint8 partition,
guint32 offset,
const guint8 *data,
gsize data_len,
guint8 *cmd,
gsize *cmd_len)
{
/* pack('<BBBHLL', 0x41, partition, 1, 0, addr, len) + data = 13 + data bytes */
cmd[0] = VCSFW_CMD_WRITE_FLASH;
cmd[1] = partition;
cmd[2] = 1; /* flag */
cmd[3] = 0; /* reserved LE low */
cmd[4] = 0; /* reserved LE high */
FP_WRITE_UINT32_LE (cmd + 5, offset);
FP_WRITE_UINT32_LE (cmd + 9, (guint32) data_len);
memcpy (cmd + 13, data, data_len);
*cmd_len = 13 + data_len;
}
void
validity_fwext_build_write_fw_sig (guint8 partition,
const guint8 *signature,
gsize sig_len,
guint8 *cmd,
gsize *cmd_len)
{
/* pack('<BBxH', 0x42, partition, len) + signature = 5 + sig bytes */
cmd[0] = VCSFW_CMD_WRITE_FW_SIG;
cmd[1] = partition;
cmd[2] = 0; /* reserved byte (the 'x' in pack format) */
FP_WRITE_UINT16_LE (cmd + 3, (guint16) sig_len);
memcpy (cmd + 5, signature, sig_len);
*cmd_len = 5 + sig_len;
}
void
validity_fwext_build_reboot (guint8 *cmd,
gsize *cmd_len)
{
/* b'\x05\x02\x00' */
cmd[0] = VCSFW_CMD_REBOOT;
cmd[1] = VCSFW_REBOOT_SUBCMD;
cmd[2] = 0x00;
*cmd_len = 3;
}
/* ================================================================
* db_write_enable blob lookup
* ================================================================ */
const guint8 *
validity_fwext_get_db_write_enable (guint16 vid,
guint16 pid,
gsize *len)
{
/* Currently only 06cb:009a is supported.
* Iteration 6 (HAL) will add blobs for all PIDs. */
if (vid == 0x06cb && pid == 0x009a)
{
*len = sizeof (db_write_enable_009a);
return db_write_enable_009a;
}
*len = 0;
return NULL;
}
/* ================================================================
* Upload SSM -- firmware extension upload state machine
*
* This SSM is started as a standalone child from the open sequence
* when fwext_loaded == FALSE. It uses the subsm pattern:
* SEND states call vcsfw_cmd_send(self, ssm, ..., NULL) with a
* NULL callback. The child SSM auto-advances the parent to the
* RECV state on completion. RECV states read
* self->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;
}

View file

@ -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 <glib.h>
/* ---- 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);

View file

@ -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)
{

View file

@ -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 = {

View file

@ -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

643
tests/test-validity-fwext.c Normal file
View file

@ -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 <glib.h>
#include <glib/gstdio.h>
#include <string.h>
#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 ();
}