From 977e09da2d5961bc4d2970490efeaff954561357 Mon Sep 17 00:00:00 2001 From: Leonardo Francisco Date: Mon, 6 Apr 2026 00:09:42 -0400 Subject: [PATCH] validity: Add capture program infrastructure (Iteration 5) Implement the capture command building infrastructure ported from python-validity's sensor.py and timeslot.py. This provides all the algorithms needed to construct sensor capture commands for calibration, enrollment, and identification modes. New components: - TLV chunk parsing (split/merge) for capture programs - Timeslot DSP instruction decoder (16 opcodes, 1-3 bytes each) - Timeslot table patching (Call repeat multiplication, factory cal) - Line Update Type 1 algorithm for 0xb5-class sensors - build_cmd_02(): main capture command builder - Factory bits parsing (subtag 3 cal values, subtag 7 cal data) - Frame averaging with multi-line deinterlacing - Calibration data processing (scale/accumulate/clip) - Clean slate format with SHA256 verification - Bitpack compression for factory calibration values - Finger ID mapping (FpFinger <-> VCSFW subtype 1-10) - LED control commands (glow_start_scan, glow_end_scan) - CaptureProg lookup table for firmware 6.x type-1 devices - OPEN_CAPTURE_SETUP state in the open SSM 27 unit tests covering all components. Full test suite: 36 OK, 0 Fail, 2 Skipped. --- libfprint/drivers/validity/validity.c | 45 + libfprint/drivers/validity/validity.h | 4 + libfprint/drivers/validity/validity_capture.c | 1718 +++++++++++++++++ libfprint/drivers/validity/validity_capture.h | 395 ++++ libfprint/meson.build | 3 +- tests/meson.build | 16 + tests/test-validity-capture.c | 1019 ++++++++++ 7 files changed, 3199 insertions(+), 1 deletion(-) create mode 100644 libfprint/drivers/validity/validity_capture.c create mode 100644 libfprint/drivers/validity/validity_capture.h create mode 100644 tests/test-validity-capture.c diff --git a/libfprint/drivers/validity/validity.c b/libfprint/drivers/validity/validity.c index 0e78e2fd..ce8b1bdd 100644 --- a/libfprint/drivers/validity/validity.c +++ b/libfprint/drivers/validity/validity.c @@ -183,6 +183,7 @@ typedef enum { OPEN_SENSOR_IDENTIFY_RECV, OPEN_SENSOR_FACTORY_BITS, OPEN_SENSOR_FACTORY_BITS_RECV, + OPEN_CAPTURE_SETUP, OPEN_DONE, OPEN_NUM_STATES, } ValidityOpenSsmState; @@ -618,6 +619,48 @@ open_run_state (FpiSsm *ssm, } break; + case OPEN_CAPTURE_SETUP: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + /* Initialize capture state from sensor identification and factory bits. + * Requires: sensor.type_info and sensor.device_info from IDENTIFY, + * sensor.factory_bits from FACTORY_BITS. */ + if (!self->sensor.type_info || !self->sensor.device_info) + { + fp_info ("No sensor type info — skipping capture setup"); + fpi_ssm_next_state (ssm); + return; + } + + validity_capture_state_init (&self->capture); + + if (!validity_capture_state_setup (&self->capture, + self->sensor.type_info, + self->sensor.device_info->type, + self->version_info.version_major, + self->version_info.version_minor, + self->sensor.factory_bits, + self->sensor.factory_bits_len)) + { + fp_warn ("Capture state setup failed — " + "enrollment/verification will not be available"); + /* Non-fatal: device can still be used for identification + * if calibration data exists on flash */ + } + else + { + fp_info ("Capture state: %u bytes/line, %u lines/frame, " + "type1=%d", + self->capture.bytes_per_line, + self->capture.lines_per_frame, + self->capture.is_type1_device); + } + + fpi_ssm_next_state (ssm); + } + break; + case OPEN_DONE: /* All init commands sent. Mark open complete. */ fpi_ssm_mark_completed (ssm); @@ -657,6 +700,7 @@ dev_open (FpDevice *device) self->interrupt_cancellable = g_cancellable_new (); validity_tls_init (&self->tls); validity_sensor_state_init (&self->sensor); + validity_capture_state_init (&self->capture); if (!g_usb_device_claim_interface (fpi_device_get_usb_device (device), 0, 0, &error)) { @@ -685,6 +729,7 @@ dev_close (FpDevice *device) g_clear_pointer (&self->cmd_response_data, g_free); self->cmd_response_len = 0; + validity_capture_state_clear (&self->capture); validity_sensor_state_clear (&self->sensor); validity_tls_free (&self->tls); diff --git a/libfprint/drivers/validity/validity.h b/libfprint/drivers/validity/validity.h index 3ddb5a1c..6058d400 100644 --- a/libfprint/drivers/validity/validity.h +++ b/libfprint/drivers/validity/validity.h @@ -22,6 +22,7 @@ #include "fpi-device.h" #include "fpi-ssm.h" +#include "validity_capture.h" #include "validity_sensor.h" #include "validity_tls.h" @@ -102,6 +103,9 @@ struct _FpiDeviceValidity /* Sensor identification and HAL state (post-TLS) */ ValiditySensorState sensor; + /* Capture program infrastructure and calibration state */ + ValidityCaptureState capture; + /* Firmware extension status */ gboolean fwext_loaded; diff --git a/libfprint/drivers/validity/validity_capture.c b/libfprint/drivers/validity/validity_capture.c new file mode 100644 index 00000000..fb6df854 --- /dev/null +++ b/libfprint/drivers/validity/validity_capture.c @@ -0,0 +1,1718 @@ +/* + * Capture program infrastructure for Validity/Synaptics VCSFW sensors + * + * 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_capture.h" + +#include +#include +#include + +/* OpenSSL for SHA256 (clean slate) */ +#include + +/* ================================================================ + * Chunk parsing + * ================================================================ */ + +ValidityCaptureChunk * +validity_capture_split_chunks (const guint8 *data, + gsize data_len, + gsize *n_chunks) +{ + GArray *arr; + gsize offset = 0; + + g_return_val_if_fail (data != NULL || data_len == 0, NULL); + g_return_val_if_fail (n_chunks != NULL, NULL); + + arr = g_array_new (FALSE, TRUE, sizeof (ValidityCaptureChunk)); + + while (offset + 4 <= data_len) + { + ValidityCaptureChunk chunk; + + chunk.type = FP_READ_UINT16_LE (data + offset); + chunk.size = FP_READ_UINT16_LE (data + offset + 2); + offset += 4; + + if (offset + chunk.size > data_len) + { + /* Truncated chunk — free what we have and fail */ + for (gsize i = 0; i < arr->len; i++) + g_free (g_array_index (arr, ValidityCaptureChunk, i).data); + g_array_free (arr, TRUE); + *n_chunks = 0; + return NULL; + } + + chunk.data = g_memdup2 (data + offset, chunk.size); + offset += chunk.size; + + g_array_append_val (arr, chunk); + } + + *n_chunks = arr->len; + return (ValidityCaptureChunk *) g_array_free (arr, FALSE); +} + +guint8 * +validity_capture_merge_chunks (const ValidityCaptureChunk *chunks, + gsize n_chunks, + gsize *out_len) +{ + gsize total = 0; + guint8 *buf; + gsize offset = 0; + + g_return_val_if_fail (out_len != NULL, NULL); + + /* Calculate total size */ + for (gsize i = 0; i < n_chunks; i++) + total += 4 + chunks[i].size; + + buf = g_malloc (total); + + for (gsize i = 0; i < n_chunks; i++) + { + FP_WRITE_UINT16_LE (&buf[offset], chunks[i].type); + FP_WRITE_UINT16_LE (&buf[offset + 2], chunks[i].size); + offset += 4; + if (chunks[i].size > 0 && chunks[i].data) + { + memcpy (buf + offset, chunks[i].data, chunks[i].size); + offset += chunks[i].size; + } + } + + *out_len = total; + return buf; +} + +void +validity_capture_chunks_free (ValidityCaptureChunk *chunks, + gsize n_chunks) +{ + if (chunks == NULL) + return; + + for (gsize i = 0; i < n_chunks; i++) + g_free (chunks[i].data); + + g_free (chunks); +} + +/* ================================================================ + * Timeslot instruction decoder + * + * Reference: python-validity timeslot.py decode_insn() + * ================================================================ */ + +gboolean +validity_capture_decode_insn (const guint8 *data, + gsize data_len, + guint8 *opcode, + guint8 *insn_len, + guint32 operands[3], + guint8 *n_operands) +{ + g_return_val_if_fail (data != NULL && data_len > 0, FALSE); + g_return_val_if_fail (opcode != NULL && insn_len != NULL, FALSE); + g_return_val_if_fail (n_operands != NULL, FALSE); + + *n_operands = 0; + guint8 b0 = data[0]; + + /* Single-byte instructions: 0x00-0x04 */ + if (b0 <= 4) + { + *opcode = b0; + *insn_len = 1; + return TRUE; + } + + /* Two-byte instructions with one operand */ + if (b0 == 5) + { + if (data_len < 2) + return FALSE; + *opcode = TST_OP_MACRO; + *insn_len = 2; + operands[0] = data[1]; + *n_operands = 1; + return TRUE; + } + + if (b0 == 6) + { + if (data_len < 2) + return FALSE; + *opcode = TST_OP_ENABLE_RX; + *insn_len = 2; + operands[0] = data[1]; + *n_operands = 1; + return TRUE; + } + + if (b0 == 7) + { + if (data_len < 2) + return FALSE; + *opcode = TST_OP_IDLE_RX; + *insn_len = 2; + operands[0] = (data[1] == 0) ? 0x100 : data[1]; + *n_operands = 1; + return TRUE; + } + + /* Enable SO: 0x08-0x09 */ + if ((b0 & 0xfe) == 0x08) + { + if (data_len < 2) + return FALSE; + *opcode = TST_OP_ENABLE_SO; + *insn_len = 2; + operands[0] = ((guint32)(b0 & 1) << 8) | data[1]; + *n_operands = 1; + return TRUE; + } + + /* Disable SO: 0x0a-0x0b */ + if ((b0 & 0xfe) == 0x0a) + { + if (data_len < 2) + return FALSE; + *opcode = TST_OP_DISABLE_SO; + *insn_len = 2; + operands[0] = ((guint32)(b0 & 1) << 8) | data[1]; + *n_operands = 1; + return TRUE; + } + + /* Interrupt: 0x0c-0x0f */ + if ((b0 & 0xfc) == 0x0c) + { + *opcode = TST_OP_INTERRUPT; + *insn_len = 1; + operands[0] = b0 & 3; + *n_operands = 1; + return TRUE; + } + + /* Call: 0x10-0x17 (3 bytes) */ + if ((b0 & 0xf8) == 0x10) + { + if (data_len < 3) + return FALSE; + *opcode = TST_OP_CALL; + *insn_len = 3; + operands[0] = b0 & 7; /* rx_inc */ + operands[1] = (guint32) data[1] << 2; /* address */ + operands[2] = (data[2] == 0) ? 0x100 : data[2]; /* repeat */ + *n_operands = 3; + return TRUE; + } + + /* Features: 0x20-0x3f (1 byte) */ + if ((b0 & 0xe0) == 0x20) + { + *opcode = TST_OP_FEATURES; + *insn_len = 1; + operands[0] = b0 & 0x1f; + *n_operands = 1; + return TRUE; + } + + /* Register Write: 0x40-0x7f (3 bytes) */ + if ((b0 & 0xc0) == 0x40) + { + if (data_len < 3) + return FALSE; + *opcode = TST_OP_REG_WRITE; + *insn_len = 3; + operands[0] = (guint32)(b0 & 0x3f) * 4 + 0x80002000; /* register address */ + operands[1] = (guint32) data[1] | ((guint32) data[2] << 8); /* value */ + *n_operands = 2; + return TRUE; + } + + /* Sample: 0x80-0xbf (1 byte) */ + if ((b0 & 0xc0) == 0x80) + { + *opcode = TST_OP_SAMPLE; + *insn_len = 1; + operands[0] = (b0 & 0x38) >> 3; + operands[1] = b0 & 7; + *n_operands = 2; + return TRUE; + } + + /* Sample Repeat: 0xc0-0xff (2 bytes) */ + if ((b0 & 0xc0) == 0xc0) + { + if (data_len < 2) + return FALSE; + *opcode = TST_OP_SAMPLE_REPEAT; + *insn_len = 2; + operands[0] = (b0 & 0x38) >> 3; + operands[1] = b0 & 7; + operands[2] = (data[1] == 0) ? 0x100 : data[1]; + *n_operands = 3; + return TRUE; + } + + return FALSE; +} + +gssize +validity_capture_find_nth_insn (const guint8 *data, + gsize data_len, + guint8 target_opcode, + guint n) +{ + gsize pc = 0; + + while (pc < data_len) + { + guint8 opcode, len, n_ops; + guint32 operands[3]; + + if (!validity_capture_decode_insn (data + pc, data_len - pc, + &opcode, &len, operands, &n_ops)) + break; + + if (opcode == target_opcode) + { + n--; + if (n == 0) + return (gssize) pc; + } + + pc += len; + } + + return -1; +} + +gssize +validity_capture_find_nth_regwrite (const guint8 *data, + gsize data_len, + guint32 reg_addr, + guint n) +{ + gsize pc = 0; + + while (pc < data_len) + { + guint8 opcode, len, n_ops; + guint32 operands[3]; + + if (!validity_capture_decode_insn (data + pc, data_len - pc, + &opcode, &len, operands, &n_ops)) + break; + + if (opcode == TST_OP_REG_WRITE && n_ops >= 2 && operands[0] == reg_addr) + { + n--; + if (n == 0) + return (gssize) pc; + } + + pc += len; + } + + return -1; +} + +/* ================================================================ + * Timeslot table patching + * ================================================================ */ + +gboolean +validity_capture_patch_timeslot_table (guint8 *data, + gsize data_len, + gboolean inc_address, + guint8 mult) +{ + gsize i = 0; + + g_return_val_if_fail (data != NULL, FALSE); + + while (i + 3 <= data_len) + { + /* Call instruction: 0x10-0x17 */ + if ((data[i] & 0xf8) == 0x10) + { + if (data[i + 2] > 1) + { + data[i + 2] *= mult; + if (inc_address) + data[i + 1] += 1; + } + i += 3; + continue; + } + + /* NOOP: single byte 0x00 */ + if (data[i] == 0x00) + { + i += 1; + continue; + } + + /* Idle Rx: two bytes, opcode 0x07 */ + if (data[i] == 0x07) + { + i += 2; + continue; + } + + /* Unknown instruction — stop patching */ + break; + } + + return TRUE; +} + +gboolean +validity_capture_patch_timeslot_again (guint8 *data, + gsize data_len, + const guint8 *factory_calibration_values, + gsize factory_cal_len, + guint16 key_calibration_line) +{ + gssize call_target = -1; + gsize pc = 0; + gssize match = -1; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (factory_calibration_values != NULL, FALSE); + + /* First pass: find the last Call instruction's destination address */ + while (pc < data_len) + { + guint8 opcode, len, n_ops; + guint32 operands[3]; + + if (!validity_capture_decode_insn (data + pc, data_len - pc, + &opcode, &len, operands, &n_ops)) + break; + + /* End of Table, Return, End of Data — stop scanning */ + if (opcode == TST_OP_END_OF_TABLE || + opcode == TST_OP_RETURN || + opcode == TST_OP_END_OF_DATA) + break; + + /* Call — record its destination address */ + if (opcode == TST_OP_CALL && n_ops >= 2) + call_target = (gssize) operands[1]; + + pc += len; + } + + if (call_target < 0 || (gsize) call_target >= data_len) + return FALSE; + + /* Second pass: from the Call target, find the last Register Write to 0x8000203C */ + pc = (gsize) call_target; + while (pc < data_len) + { + guint8 opcode, len, n_ops; + guint32 operands[3]; + + if (!validity_capture_decode_insn (data + pc, data_len - pc, + &opcode, &len, operands, &n_ops)) + break; + + if (opcode == TST_OP_END_OF_TABLE || + opcode == TST_OP_RETURN || + opcode == TST_OP_END_OF_DATA) + break; + + /* Register Write to 0x8000203C */ + if (opcode == TST_OP_REG_WRITE && n_ops >= 2 && operands[0] == 0x8000203c) + match = (gssize) pc; + + pc += len; + } + + if (match < 0) + return FALSE; + + /* Patch the value byte with factory calibration value at key_calibration_line. + * The instruction is 3 bytes: [opcode_byte] [value_lo] [value_hi] + * We patch value_lo (byte at match+1). */ + if (key_calibration_line < factory_cal_len) + data[match + 1] = factory_calibration_values[key_calibration_line]; + + return TRUE; +} + +/* ================================================================ + * Helper functions for line update + * ================================================================ */ + +/* Clip a signed value to [-128, 127] and return as unsigned byte */ +static guint8 +clip_signed (gint x) +{ + if (x < -128) x = -128; + if (x > 127) x = 127; + return (guint8)(x & 0xff); +} + +/* Scale a byte value for calibration processing */ +static guint8 +scale_byte (guint8 x) +{ + gint val = (gint) x - 0x80; + val = val * 10 / 0x22; + return clip_signed (val); +} + +/* Add two unsigned bytes as signed, clip result */ +static guint8 +add_signed_bytes (guint8 l, guint8 r) +{ + gint8 sl = (gint8) l; + gint8 sr = (gint8) r; + return clip_signed ((gint) sl + (gint) sr); +} + +/* ================================================================ + * Bitpack — compress calibration values + * + * Reference: python-validity sensor.py bitpack() + * ================================================================ */ + +guint8 * +validity_capture_bitpack (const guint8 *values, + gsize values_len, + guint8 *out_v0, + guint8 *out_v1, + gsize *out_len) +{ + guint8 min_val = 0xff, max_val = 0; + guint8 useful_bits; + guint max_delta; + gsize total_bits; + gsize total_bytes; + guint8 *packed; + + g_return_val_if_fail (values != NULL && values_len > 0, NULL); + g_return_val_if_fail (out_v0 != NULL && out_v1 != NULL && out_len != NULL, NULL); + + /* Find min and max */ + for (gsize i = 0; i < values_len; i++) + { + if (values[i] < min_val) min_val = values[i]; + if (values[i] > max_val) max_val = values[i]; + } + + max_delta = max_val - min_val; + + /* Count useful bits */ + useful_bits = 0; + { + guint tmp = max_delta; + while (tmp > 0) + { + tmp >>= 1; + useful_bits++; + } + } + + /* Handle edge case: all values identical */ + if (useful_bits == 0) + { + *out_v0 = 0; + *out_v1 = min_val; + *out_len = 0; + return g_malloc0 (1); + } + + /* Pack values in reverse order into a bit stream. + * Each value is (value - min), stored in useful_bits bits. + * Values are packed starting from the last value (reverse). */ + total_bits = (gsize) useful_bits * values_len; + total_bytes = (total_bits + 7) / 8; + packed = g_malloc0 (total_bytes); + + /* Build by accumulating shifted deltas from the last value to the first */ + { + /* Use a big-endian bit accumulation approach: + * Process values from last to first. For each value, shift the + * accumulator left by useful_bits and OR in the delta. + * This matches python: int(''.join(bin(v - min)[-u:] for v reversed), 2) */ + guint bit_pos = 0; + for (gsize i = 0; i < values_len; i++) + { + guint delta = values[i] - min_val; + /* Write 'useful_bits' bits at bit_pos (little-endian byte order) */ + for (guint b = 0; b < useful_bits; b++) + { + if (delta & (1u << b)) + packed[bit_pos / 8] |= (1u << (bit_pos % 8)); + bit_pos++; + } + } + } + + *out_v0 = useful_bits; + *out_v1 = min_val; + *out_len = total_bytes; + return packed; +} + +/* ================================================================ + * get_key_line — extract the key calibration line from calib_data + * + * Reference: python-validity sensor.py get_key_line() + * ================================================================ */ + +static guint8 * +get_key_line (const guint8 *calib_data, + gsize calib_data_len, + guint16 lines_per_calibration_data, + guint16 key_calibration_line, + guint16 line_width) +{ + guint8 *key_line = g_malloc0 (line_width); + + if (calib_data != NULL && calib_data_len > 0 && lines_per_calibration_data > 0) + { + gsize bytes_per_cal_line = calib_data_len / lines_per_calibration_data; + gsize key_offset = 8 + bytes_per_cal_line * key_calibration_line; + + if (key_offset + line_width <= calib_data_len) + { + memcpy (key_line, calib_data + key_offset, line_width); + /* Replace value 5 with 4 (python: [i-1 if i == 5 else i for i in key_line]) */ + for (guint16 i = 0; i < line_width; i++) + { + if (key_line[i] == 5) + key_line[i] = 4; + } + } + } + + return key_line; +} + +/* ================================================================ + * Line Update Type 1 + * + * Modifies the chunk list for type-1 devices (includes 0xb5). + * Adds Reply Config, Finger Detect/Image Reconstruction, + * Interleave, Line Update, and Line Update Transform chunks. + * + * Reference: python-validity sensor.py line_update_type_1() + * ================================================================ */ + +/* Internal line entry for building Line Update chunks */ +typedef struct +{ + guint32 mask; + guint32 flags; + guint8 *data; + gsize data_len; + guint8 v0; /* bitpack: useful bits */ + guint8 v1; /* bitpack: minimum */ + guint16 v2; /* unused in type 1, always 0 */ +} LineEntry; + +/* + * Build the line update chunk list for type 1 devices. + * This is a complex function that: + * 1. Patches the timeslot table in existing chunks + * 2. Adds mode-specific chunks (Reply Config, Finger Detect, Image Recon) + * 3. Adds Interleave chunk + * 4. Builds Line Update and Line Update Transform from calibration data + * + * Returns a new array of chunks (caller frees with validity_capture_chunks_free). + * n_chunks is updated with the new count. + */ +static ValidityCaptureChunk * +build_line_update_type1 (const ValidityCaptureState *capture, + const ValiditySensorTypeInfo *type_info, + ValidityCaptureMode mode, + ValidityCaptureChunk *in_chunks, + gsize in_n_chunks, + gsize *out_n_chunks) +{ + GArray *chunks_arr; + GArray *lines_arr; + gsize cnt = 2; /* line counter starts at 2 per python-validity */ + + /* Copy input chunks, patching timeslot table in-place */ + chunks_arr = g_array_new (FALSE, TRUE, sizeof (ValidityCaptureChunk)); + + for (gsize i = 0; i < in_n_chunks; i++) + { + ValidityCaptureChunk c; + c.type = in_chunks[i].type; + c.size = in_chunks[i].size; + c.data = g_memdup2 (in_chunks[i].data, in_chunks[i].size); + + /* Patch Timeslot Table 2D */ + if (c.type == CAPT_CHUNK_TIMESLOT_2D && c.data && c.size > 0) + { + validity_capture_patch_timeslot_table (c.data, c.size, TRUE, + type_info->repeat_multiplier); + if (mode != VALIDITY_CAPTURE_CALIBRATE) + validity_capture_patch_timeslot_again (c.data, c.size, + capture->factory_calibration_values, + capture->factory_calibration_values_len, + capture->key_calibration_line); + + /* Prepend key line to the timeslot table. + * In type 1: c[1] = get_key_line() + tst[line_width:] */ + { + g_autofree guint8 *key_line = get_key_line ( + capture->calib_data, capture->calib_data_len, + type_info->lines_per_calibration_data, + capture->key_calibration_line, + type_info->line_width); + + if (c.size > type_info->line_width) + { + gsize new_size = type_info->line_width + (c.size - type_info->line_width); + guint8 *new_data = g_malloc (new_size); + memcpy (new_data, key_line, type_info->line_width); + memcpy (new_data + type_info->line_width, + c.data + type_info->line_width, + c.size - type_info->line_width); + g_free (c.data); + c.data = new_data; + c.size = new_size; + } + } + } + + g_array_append_val (chunks_arr, c); + } + + /* --- Reply Configuration --- */ + { + ValidityCaptureChunk rc = { .type = CAPT_CHUNK_REPLY_CONFIG, .size = 0, .data = NULL }; + g_array_append_val (chunks_arr, rc); + } + + /* --- Mode-specific chunks (Finger Detect / WTF / Image Reconstruction) --- */ + if (mode == VALIDITY_CAPTURE_IDENTIFY) + { + /* Finger Detect (chunk 0x4e) — hardcoded for type 1 devices */ + static const guint8 wtf_data[] = { + 0xfb, 0xb2, 0x0f, 0x00, 0x00, 0x00, 0x0f, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x87, 0x00, 0x02, 0x00, + 0x67, 0x00, 0x0a, 0x00, 0x01, 0x80, 0x00, 0x00, + 0x0a, 0x02, 0x00, 0x00, 0x0b, 0x19, 0x00, 0x00, + 0x88, 0x13, 0xb8, 0x0b, 0x01, 0x09, 0x10, 0x00, + }; + ValidityCaptureChunk fd = { + .type = CAPT_CHUNK_WTF, + .size = sizeof (wtf_data), + .data = g_memdup2 (wtf_data, sizeof (wtf_data)), + }; + g_array_append_val (chunks_arr, fd); + + /* Image Reconstruction (for identify mode) */ + static const guint8 recon_identify[] = { + 0x02, 0x00, 0x18, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x70, 0x00, 0x4d, 0x01, 0x00, 0x00, + 0xa0, 0x00, 0x8c, 0x00, 0x3c, 0x32, 0x32, 0x1e, + 0x3c, 0x0a, 0x02, 0x02, + }; + ValidityCaptureChunk ir = { + .type = CAPT_CHUNK_IMAGE_RECON, + .size = sizeof (recon_identify), + .data = g_memdup2 (recon_identify, sizeof (recon_identify)), + }; + g_array_append_val (chunks_arr, ir); + } + else if (mode == VALIDITY_CAPTURE_ENROLL) + { + /* Finger Detect for enroll — uses chunk 0x26 */ + static const guint8 fd_enroll[] = { + 0xfb, 0xb2, 0x0f, 0x00, 0x00, 0x00, 0x0f, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x87, 0x00, 0x02, 0x00, + 0x67, 0x00, 0x0a, 0x00, 0x01, 0x80, 0x00, 0x00, + 0x0a, 0x02, 0x00, 0x00, 0x0b, 0x19, 0x00, 0x00, + 0x50, 0xc3, 0x60, 0xea, 0x01, 0x09, 0x10, 0x00, + }; + ValidityCaptureChunk fd = { + .type = CAPT_CHUNK_FINGER_DETECT, + .size = sizeof (fd_enroll), + .data = g_memdup2 (fd_enroll, sizeof (fd_enroll)), + }; + g_array_append_val (chunks_arr, fd); + + /* Image Reconstruction (for enroll mode — one byte different) */ + static const guint8 recon_enroll[] = { + 0x02, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x70, 0x00, 0x4d, 0x01, 0x00, 0x00, + 0xa0, 0x00, 0x8c, 0x00, 0x3c, 0x32, 0x32, 0x1e, + 0x3c, 0x0a, 0x02, 0x02, + }; + ValidityCaptureChunk ir = { + .type = CAPT_CHUNK_IMAGE_RECON, + .size = sizeof (recon_enroll), + .data = g_memdup2 (recon_enroll, sizeof (recon_enroll)), + }; + g_array_append_val (chunks_arr, ir); + } + /* CALIBRATE mode: no Finger Detect or Image Reconstruction */ + + /* --- Interleave --- */ + { + guint8 interleave_data[4]; + FP_WRITE_UINT32_LE (interleave_data, 1); + ValidityCaptureChunk il = { + .type = CAPT_CHUNK_INTERLEAVE, + .size = 4, + .data = g_memdup2 (interleave_data, 4), + }; + g_array_append_val (chunks_arr, il); + } + + /* --- Line Update and Line Update Transform --- + * Build line entries from calibration data for the timeslot table. */ + lines_arr = g_array_new (FALSE, TRUE, sizeof (LineEntry)); + + /* We need the patched timeslot table for instruction searches */ + { + const guint8 *tst_data = NULL; + gsize tst_len = 0; + + /* Find the Timeslot Table 2D chunk in our patched chunks */ + for (gsize i = 0; i < chunks_arr->len; i++) + { + ValidityCaptureChunk *ch = &g_array_index (chunks_arr, ValidityCaptureChunk, i); + if (ch->type == CAPT_CHUNK_TIMESLOT_2D) + { + tst_data = ch->data; + tst_len = ch->size; + break; + } + } + + if (tst_data && tst_len > 0) + { + /* Line 0: calibration blob at Enable Rx position */ + { + gssize pc = validity_capture_find_nth_insn (tst_data, tst_len, + TST_OP_ENABLE_RX, 2); + if (pc >= 0 && type_info->calibration_blob) + { + LineEntry le = { 0 }; + le.mask = 0xff; + le.flags = ((guint32)(pc + 1)) | ((guint32) cnt << 0x14) | 0x7000000; + le.data = g_memdup2 (type_info->calibration_blob, + type_info->calibration_blob_len); + le.data_len = type_info->calibration_blob_len; + le.v0 = 0x0f; + g_array_append_val (lines_arr, le); + cnt++; + } + } + + /* Line 1: factory calibration values at Register Write position */ + { + gssize pc = validity_capture_find_nth_regwrite (tst_data, tst_len, + 0x8000203C, 1); + if (pc >= 0 && capture->factory_calibration_values) + { + LineEntry le = { 0 }; + le.mask = 0xff; + le.flags = ((guint32)(pc + 1)) | ((guint32) cnt << 0x14) | 0x7000000; + + /* Bitpack the factory calibration values */ + le.data = validity_capture_bitpack ( + capture->factory_calibration_values, + capture->factory_calibration_values_len, + &le.v0, &le.v1, &le.data_len); + le.v0 = (le.v0 - 1) | 8; + cnt++; + + g_array_append_val (lines_arr, le); + } + } + + /* Calibration data lines (if we have calib_data) */ + if (capture->calib_data && capture->calib_data_len > 0) + { + gsize bytes_per_cal_line = capture->calib_data_len / + type_info->lines_per_calibration_data; + + for (guint i = 0; i < 112; i += 4) + { + LineEntry le = { 0 }; + le.mask = 0xffffffff; + le.flags = i | (0x85u << 24); + + /* Collect data from each calibration line at offset i */ + gsize row_data_len = 0; + GByteArray *row = g_byte_array_new (); + + for (guint j = 0; j < 112; j++) + { + gsize p = 8 + (gsize) j * bytes_per_cal_line + i; + if (p + 4 <= capture->calib_data_len) + g_byte_array_append (row, capture->calib_data + p, 4); + else + { + guint8 zeros[4] = { 0 }; + g_byte_array_append (row, zeros, 4); + } + } + + le.data_len = row->len; + le.data = g_byte_array_free (row, FALSE); + row_data_len = le.data_len; + + (void) row_data_len; + g_array_append_val (lines_arr, le); + } + } + } + } + + /* Align all line data to 4-byte boundary */ + for (gsize i = 0; i < lines_arr->len; i++) + { + LineEntry *le = &g_array_index (lines_arr, LineEntry, i); + gsize pad = le->data_len % 4; + if (pad > 0) + { + gsize new_len = le->data_len + (4 - pad); + le->data = g_realloc (le->data, new_len); + memset (le->data + le->data_len, 0, 4 - pad); + le->data_len = new_len; + } + } + + /* Build Line Update chunk (entries with (flags & 0x00f00000) >> 0x14 <= 1) */ + { + GByteArray *lu = g_byte_array_new (); + guint32 n_lines = lines_arr->len; + guint8 hdr[4]; + + FP_WRITE_UINT32_LE (hdr, n_lines); + g_byte_array_append (lu, hdr, 4); + + /* Mask + flags headers */ + for (gsize i = 0; i < lines_arr->len; i++) + { + LineEntry *le = &g_array_index (lines_arr, LineEntry, i); + guint8 entry[8]; + FP_WRITE_UINT32_LE (entry, le->mask); + FP_WRITE_UINT32_LE (entry + 4, le->flags); + g_byte_array_append (lu, entry, 8); + } + + /* Data for entries where (flags >> 20) & 0xf <= 1 */ + for (gsize i = 0; i < lines_arr->len; i++) + { + LineEntry *le = &g_array_index (lines_arr, LineEntry, i); + guint32 slot = (le->flags & 0x00f00000) >> 0x14; + if (slot <= 1 && le->data && le->data_len > 0) + g_byte_array_append (lu, le->data, le->data_len); + } + + ValidityCaptureChunk lu_chunk = { + .type = CAPT_CHUNK_LINE_UPDATE, + .size = lu->len, + .data = g_byte_array_free (lu, FALSE), + }; + g_array_append_val (chunks_arr, lu_chunk); + } + + /* Build Line Update Transform chunk (entries where (flags >> 20) & 0xf > 1) */ + { + GByteArray *lut = g_byte_array_new (); + + for (gsize i = 0; i < lines_arr->len; i++) + { + LineEntry *le = &g_array_index (lines_arr, LineEntry, i); + guint32 slot = (le->flags & 0x00f00000) >> 0x14; + if (slot > 1 && le->data && le->data_len > 0) + { + guint8 hdr[4] = { le->v0, le->v1, 0, 0 }; + FP_WRITE_UINT16_LE (hdr + 2, le->v2); + g_byte_array_append (lut, hdr, 4); + g_byte_array_append (lut, le->data, le->data_len); + } + } + + if (lut->len > 0) + { + ValidityCaptureChunk lut_chunk = { + .type = CAPT_CHUNK_LINE_UPDATE_XFORM, + .size = lut->len, + .data = g_byte_array_free (lut, FALSE), + }; + g_array_append_val (chunks_arr, lut_chunk); + } + else + { + g_byte_array_free (lut, TRUE); + } + } + + /* Free line entries */ + for (gsize i = 0; i < lines_arr->len; i++) + g_free (g_array_index (lines_arr, LineEntry, i).data); + g_array_free (lines_arr, TRUE); + + *out_n_chunks = chunks_arr->len; + return (ValidityCaptureChunk *) g_array_free (chunks_arr, FALSE); +} + +/* ================================================================ + * build_cmd_02 — assemble the final capture command + * + * Reference: python-validity sensor.py build_cmd_02() + * ================================================================ */ + +guint8 * +validity_capture_build_cmd_02 (const ValidityCaptureState *capture, + const ValiditySensorTypeInfo *type_info, + ValidityCaptureMode mode, + gsize *out_len) +{ + ValidityCaptureChunk *chunks = NULL; + ValidityCaptureChunk *patched = NULL; + gsize n_chunks = 0; + gsize n_patched = 0; + guint8 *merged = NULL; + gsize merged_len = 0; + guint8 *cmd = NULL; + guint16 req_lines; + + g_return_val_if_fail (capture != NULL, NULL); + g_return_val_if_fail (type_info != NULL, NULL); + g_return_val_if_fail (out_len != NULL, NULL); + g_return_val_if_fail (capture->capture_prog != NULL, NULL); + + *out_len = 0; + + /* Split the capture program into chunks */ + chunks = validity_capture_split_chunks (capture->capture_prog, + capture->capture_prog_len, + &n_chunks); + if (!chunks) + return NULL; + + /* Apply line update patching (type 1 for our target devices) */ + if (capture->is_type1_device) + { + patched = build_line_update_type1 (capture, type_info, mode, + chunks, n_chunks, &n_patched); + } + else + { + /* Type 2 devices — not yet implemented (0x9d, 0x97, etc.) */ + fp_warn ("Line update type 2 not yet implemented"); + validity_capture_chunks_free (chunks, n_chunks); + return NULL; + } + + validity_capture_chunks_free (chunks, n_chunks); + + if (!patched) + return NULL; + + /* Merge chunks back to binary */ + merged = validity_capture_merge_chunks (patched, n_patched, &merged_len); + validity_capture_chunks_free (patched, n_patched); + + if (!merged) + return NULL; + + /* Calculate requested lines */ + if (mode == VALIDITY_CAPTURE_CALIBRATE) + req_lines = (guint16)(capture->calibration_frames * capture->lines_per_frame + 1); + else + req_lines = 0; + + /* Build final command: cmd(1) | bytes_per_line(2LE) | req_lines(2LE) | chunks */ + *out_len = 5 + merged_len; + cmd = g_malloc (*out_len); + cmd[0] = 0x02; + FP_WRITE_UINT16_LE (cmd + 1, capture->bytes_per_line); + FP_WRITE_UINT16_LE (cmd + 3, req_lines); + memcpy (cmd + 5, merged, merged_len); + g_free (merged); + + return cmd; +} + +/* ================================================================ + * Factory bits parsing + * + * Reference: python-validity sensor.py get_factory_bits() + * ================================================================ */ + +gboolean +validity_capture_parse_factory_bits (const guint8 *data, + gsize data_len, + guint8 **cal_values, + gsize *cal_values_len, + guint8 **cal_data, + gsize *cal_data_len) +{ + guint32 wtf, entries; + gsize offset; + gboolean found_subtag3 = FALSE; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (cal_values != NULL && cal_values_len != NULL, FALSE); + + *cal_values = NULL; + *cal_values_len = 0; + if (cal_data) + { + *cal_data = NULL; + *cal_data_len = 0; + } + + if (data_len < 8) + return FALSE; + + wtf = FP_READ_UINT32_LE (data); + entries = FP_READ_UINT32_LE (data + 4); + offset = 8; + + (void) wtf; + + for (guint32 i = 0; i < entries; i++) + { + guint32 ptr; + guint16 length, tag, subtag, flags; + + if (offset + 12 > data_len) + break; + + ptr = FP_READ_UINT32_LE (data + offset); + length = FP_READ_UINT16_LE (data + offset + 4); + tag = FP_READ_UINT16_LE (data + offset + 6); + subtag = FP_READ_UINT16_LE (data + offset + 8); + flags = FP_READ_UINT16_LE (data + offset + 10); + offset += 12; + + (void) ptr; + (void) tag; + (void) flags; + + if (offset + length > data_len) + break; + + /* Subtag 3: factory calibration values. + * First 4 bytes are a header; actual values start at +4. */ + if (subtag == 3 && length > 4) + { + *cal_values_len = length - 4; + *cal_values = g_memdup2 (data + offset + 4, *cal_values_len); + found_subtag3 = TRUE; + } + + /* Subtag 7: optional factory calibration data. + * Also has a 4-byte header. */ + if (subtag == 7 && length > 4 && cal_data && cal_data_len) + { + *cal_data_len = length - 4; + *cal_data = g_memdup2 (data + offset + 4, *cal_data_len); + } + + offset += length; + } + + return found_subtag3; +} + +/* ================================================================ + * Frame averaging + * + * Reference: python-validity sensor.py average() + * ================================================================ */ + +guint8 * +validity_capture_average_frames (const guint8 *raw_data, + gsize raw_len, + guint16 lines_per_frame, + guint16 bytes_per_line, + guint16 lines_per_calibration_data, + guint8 calibration_frames, + gsize *out_len) +{ + gsize frame_size; + guint16 interleave_lines; + guint8 input_frames; + gsize base_address = 0; + guint8 *result; + gsize result_len; + + g_return_val_if_fail (raw_data != NULL, NULL); + g_return_val_if_fail (out_len != NULL, NULL); + g_return_val_if_fail (lines_per_calibration_data > 0, NULL); + + frame_size = (gsize) lines_per_frame * bytes_per_line; + interleave_lines = lines_per_frame / lines_per_calibration_data; + input_frames = calibration_frames; + + if (interleave_lines <= 0) + { + *out_len = 0; + return NULL; + } + + if (interleave_lines > 1) + { + const guint8 *frame; + + if (input_frames > 1) + { + /* Skip the first frame */ + input_frames--; + base_address = frame_size; + } + + if (base_address + frame_size > raw_len) + { + *out_len = 0; + return NULL; + } + + frame = raw_data + base_address; + + /* Result: one line per group of interleaved lines */ + result_len = (gsize) lines_per_calibration_data * bytes_per_line; + result = g_malloc0 (result_len); + + for (guint16 line = 0; line < lines_per_calibration_data; line++) + { + gsize group_offset = (gsize) line * interleave_lines * bytes_per_line; + + for (guint16 col = 0; col < bytes_per_line; col++) + { + guint sum = 0; + for (guint16 il = 0; il < interleave_lines; il++) + { + gsize idx = group_offset + (gsize) il * bytes_per_line + col; + if (idx < frame_size) + sum += frame[idx]; + } + result[(gsize) line * bytes_per_line + col] = + (guint8)(sum / interleave_lines); + } + } + + *out_len = result_len; + return result; + } + else + { + /* interleave_lines == 1: average multiple frames */ + if (input_frames > 1) + { + /* Average frames 1..N (skip frame 0) */ + result_len = frame_size; + result = g_malloc0 (result_len); + + for (gsize i = 0; i < frame_size; i++) + { + guint sum = 0; + for (guint8 f = 1; f <= input_frames; f++) + { + gsize idx = (gsize) f * frame_size + i; + if (idx < raw_len) + sum += raw_data[idx]; + } + result[i] = (guint8)(sum / input_frames); + } + + *out_len = result_len; + return result; + } + else + { + /* Single frame — just copy */ + if (frame_size > raw_len) + { + *out_len = 0; + return NULL; + } + result = g_memdup2 (raw_data, frame_size); + *out_len = frame_size; + return result; + } + } +} + +/* ================================================================ + * Calibration data processing + * + * Reference: python-validity sensor.py process_calibration_results() + * ================================================================ */ + +void +validity_capture_process_calibration (guint8 **calib_data, + gsize *calib_data_len, + const guint8 *averaged_frame, + gsize frame_len, + guint16 bytes_per_line) +{ + guint8 *frame_scaled; + + g_return_if_fail (calib_data != NULL && calib_data_len != NULL); + g_return_if_fail (averaged_frame != NULL); + + /* Apply scaling: leave first 8 bytes of each line, scale the rest */ + frame_scaled = g_memdup2 (averaged_frame, frame_len); + + { + gsize n_lines = frame_len / bytes_per_line; + for (gsize line = 0; line < n_lines; line++) + { + gsize line_start = line * bytes_per_line; + /* First 8 bytes: untouched */ + /* Bytes 8+: scale */ + for (gsize col = 8; col < bytes_per_line && line_start + col < frame_len; col++) + frame_scaled[line_start + col] = scale_byte (frame_scaled[line_start + col]); + } + } + + if (*calib_data != NULL && *calib_data_len > 0) + { + /* Combine with existing calibration data */ + gsize len = MIN (*calib_data_len, frame_len); + gsize n_lines = len / bytes_per_line; + + for (gsize line = 0; line < n_lines; line++) + { + gsize off = line * bytes_per_line; + /* First 8 bytes: keep as-is from previous */ + for (gsize col = 8; col < bytes_per_line && off + col < len; col++) + (*calib_data)[off + col] = add_signed_bytes ((*calib_data)[off + col], + frame_scaled[off + col]); + } + } + else + { + /* First calibration — use this frame */ + g_free (*calib_data); + *calib_data = g_memdup2 (frame_scaled, frame_len); + *calib_data_len = frame_len; + } + + g_free (frame_scaled); +} + +/* ================================================================ + * Clean slate + * + * Reference: python-validity sensor.py calibrate() (clean slate format) + * ================================================================ */ + +guint8 * +validity_capture_build_clean_slate (const guint8 *averaged_frame, + gsize frame_len, + gsize *out_len) +{ + /* Inner payload: data_len(2LE) | data | trailing_zero(2LE=0) */ + gsize inner_payload_len = 2 + frame_len + 2; + + /* Full inner: inner_len(2LE) | sha256(32) | zeroes(32) | inner_payload */ + gsize inner_len = 2 + 32 + 32 + inner_payload_len; + + /* Full blob: magic(2LE) | inner */ + gsize total_len = 2 + inner_len; + guint8 *buf; + guint8 hash[32]; + EVP_MD_CTX *ctx; + guint hash_len = 32; + + g_return_val_if_fail (averaged_frame != NULL, NULL); + g_return_val_if_fail (out_len != NULL, NULL); + + /* Build inner payload */ + guint8 *inner_payload = g_malloc0 (inner_payload_len); + FP_WRITE_UINT16_LE (inner_payload, (guint16) frame_len); + memcpy (inner_payload + 2, averaged_frame, frame_len); + /* trailing zero 2 bytes already zero from g_malloc0 */ + + /* SHA256 of inner_payload */ + ctx = EVP_MD_CTX_new (); + EVP_DigestInit_ex (ctx, EVP_sha256 (), NULL); + EVP_DigestUpdate (ctx, inner_payload, inner_payload_len); + EVP_DigestFinal_ex (ctx, hash, &hash_len); + EVP_MD_CTX_free (ctx); + + /* Build final buffer */ + buf = g_malloc0 (total_len); + gsize pos = 0; + + /* Magic */ + FP_WRITE_UINT16_LE (buf + pos, 0x5002); + pos += 2; + + /* Inner length */ + FP_WRITE_UINT16_LE (buf + pos, (guint16) inner_payload_len); + pos += 2; + + /* SHA256 hash */ + memcpy (buf + pos, hash, 32); + pos += 32; + + /* 32 bytes of zeroes */ + pos += 32; + + /* Inner payload */ + memcpy (buf + pos, inner_payload, inner_payload_len); + + g_free (inner_payload); + *out_len = total_len; + return buf; +} + +gboolean +validity_capture_verify_clean_slate (const guint8 *data, + gsize data_len) +{ + guint16 magic, inner_len; + const guint8 *hash_stored; + const guint8 *zeroes; + const guint8 *payload; + guint8 hash_computed[32]; + EVP_MD_CTX *ctx; + guint hash_len = 32; + + if (data_len < 68) /* 2+2+32+32 minimum */ + return FALSE; + + magic = FP_READ_UINT16_LE (data); + if (magic != 0x5002) + return FALSE; + + inner_len = FP_READ_UINT16_LE (data + 2); + hash_stored = data + 4; + zeroes = data + 36; + + /* Check zeroes block */ + for (int i = 0; i < 32; i++) + { + if (zeroes[i] != 0) + return FALSE; + } + + /* Verify hash */ + if (68 + inner_len > data_len) + return FALSE; + + payload = data + 68; + + ctx = EVP_MD_CTX_new (); + EVP_DigestInit_ex (ctx, EVP_sha256 (), NULL); + EVP_DigestUpdate (ctx, payload, inner_len); + EVP_DigestFinal_ex (ctx, hash_computed, &hash_len); + EVP_MD_CTX_free (ctx); + + return memcmp (hash_stored, hash_computed, 32) == 0; +} + +/* ================================================================ + * Finger ID mapping + * + * Maps FpFinger enum to VCSFW finger subtype (1-10). + * FpFinger: LEFT_THUMB=1, LEFT_INDEX=2, ..., RIGHT_LITTLE=10 + * VCSFW subtype: same 1-10 mapping (matches WINBIO_ANSI_381_POS) + * ================================================================ */ + +guint16 +validity_finger_to_subtype (guint finger) +{ + /* FpFinger values: FP_FINGER_LEFT_THUMB=1 through FP_FINGER_RIGHT_LITTLE=10 */ + if (finger >= 1 && finger <= 10) + return (guint16) finger; + return 0; +} + +gint +validity_subtype_to_finger (guint16 subtype) +{ + if (subtype >= 1 && subtype <= 10) + return (gint) subtype; + return -1; +} + +/* ================================================================ + * LED control commands + * + * Reference: python-validity sensor.py glow_start_scan(), glow_end_scan() + * These are sent via tls.app() (TLS application data). + * ================================================================ */ + +static const guint8 glow_start_data[] = { + 0x39, 0x20, 0xbf, 0x02, 0x00, 0xff, 0xff, 0x00, + 0x00, 0x01, 0x99, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x99, 0x99, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x99, 0x00, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const guint8 glow_end_data[] = { + 0x39, 0xf4, 0x01, 0x00, 0x00, 0xf4, 0x01, 0x00, + 0x00, 0x01, 0xff, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf4, 0x01, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const guint8 * +validity_capture_glow_start_cmd (gsize *out_len) +{ + *out_len = sizeof (glow_start_data); + return glow_start_data; +} + +const guint8 * +validity_capture_glow_end_cmd (gsize *out_len) +{ + *out_len = sizeof (glow_end_data); + return glow_end_data; +} + +/* ================================================================ + * CaptureProg table + * + * Hardcoded capture program for the target firmware/device combination. + * This is the generic capture program for firmware 6.x with line_update_type1 + * geometry (bytes_per_line=0x78, line_width=112). + * + * The blob is split into TLV chunks: + * 0x002a: ACM Config (8 bytes) + * 0x002c: CEM Config (40 bytes) + * 0x0034: Timeslot Table 2D (64 bytes) + * 0x002f: 2D Params (4 bytes) — lines per calibration data + * 0x0029: Timeslot Table Offset (4 bytes) + * 0x0035: Timeslot Table Offset for Finger Detect (4 bytes) + * + * Reference: python-validity generated_tables.py SensorCaptureProg entries + * ================================================================ */ + +/* Generic capture program for firmware 6.x, type1 devices (incl. 0xb5). + * The Timeslot Table 2D and 2D Params chunks are the key data. */ +static const guint8 capture_prog_type1_b5[] = { + /* ACM Config (0x2a, 8 bytes) */ + 0x2a, 0x00, 0x08, 0x00, + 0x20, 0x01, 0x01, 0x00, 0x10, 0x01, 0x00, 0x00, + + /* CEM Config (0x2c, 40 bytes) */ + 0x2c, 0x00, 0x28, 0x00, + 0x80, 0x20, 0x80, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x3f, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0f, 0x08, 0x0f, 0x00, 0x00, 0x00, 0x00, + 0x27, 0x9c, 0x10, 0x00, 0x27, 0x9c, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + /* Timeslot Table 2D (0x34, 64 bytes) */ + 0x34, 0x00, 0x40, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x07, 0x16, 0x00, 0x00, + 0x24, 0x0a, 0x59, 0x08, 0x5a, 0x07, 0x01, 0xc9, + 0x50, 0x0a, 0xaa, 0x07, 0x01, 0x0a, 0xda, 0x08, + 0xdb, 0x07, 0x01, 0xc9, 0x46, 0x0b, 0x21, 0x07, + 0x01, 0x08, 0x00, 0x80, 0x0a, 0x00, 0x88, 0xc9, + 0x59, 0x0a, 0x5a, 0x07, 0x01, 0x0a, 0xa9, 0x08, + 0xaa, 0x07, 0x01, 0xc9, 0x1f, 0x0a, 0xc9, 0x00, + 0x00, 0x0c, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, + + /* 2D Params (0x2f, 4 bytes) — lines_per_calibration_data = 112 = 0x70 */ + 0x2f, 0x00, 0x04, 0x00, + 0x70, 0x00, 0x00, 0x00, + + /* Timeslot Table Offset (0x29, 4 bytes) */ + 0x29, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, + + /* Timeslot Table Offset for Finger Detect (0x35, 4 bytes) */ + 0x35, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +/* Device types that use line_update_type_1 */ +static const guint16 line_update_type1_devices[] = { + 0x00B5, 0x0885, 0x00B3, 0x143B, 0x1055, + 0x00E1, 0x08B1, 0x00EA, 0x00E4, 0x00ED, + 0x1825, 0x1FF5, 0x0199, +}; + +const guint8 * +validity_capture_prog_lookup (guint8 rom_major, + guint8 rom_minor, + guint16 dev_type, + gsize *out_len) +{ + g_return_val_if_fail (out_len != NULL, NULL); + + /* Currently we only have capture program data for firmware 6.x, + * type-1 devices with 0x78 bytes/line geometry. */ + if (rom_major == 6) + { + for (gsize i = 0; i < G_N_ELEMENTS (line_update_type1_devices); i++) + { + if (line_update_type1_devices[i] == dev_type) + { + *out_len = sizeof (capture_prog_type1_b5); + return capture_prog_type1_b5; + } + } + } + + *out_len = 0; + return NULL; + + (void) rom_minor; +} + +/* ================================================================ + * Capture state lifecycle + * ================================================================ */ + +void +validity_capture_state_init (ValidityCaptureState *state) +{ + memset (state, 0, sizeof (*state)); +} + +void +validity_capture_state_clear (ValidityCaptureState *state) +{ + g_clear_pointer (&state->factory_calibration_values, g_free); + g_clear_pointer (&state->factory_calib_data, g_free); + g_clear_pointer (&state->calib_data, g_free); + memset (state, 0, sizeof (*state)); +} + +gboolean +validity_capture_state_setup (ValidityCaptureState *state, + const ValiditySensorTypeInfo *type_info, + guint16 dev_type, + guint8 rom_major, + guint8 rom_minor, + const guint8 *factory_bits, + gsize factory_bits_len) +{ + const guint8 *prog; + gsize prog_len; + + g_return_val_if_fail (state != NULL, FALSE); + g_return_val_if_fail (type_info != NULL, FALSE); + + /* Look up capture program */ + prog = validity_capture_prog_lookup (rom_major, rom_minor, dev_type, &prog_len); + if (!prog) + { + fp_warn ("No capture program for rom %d.%d, dev_type 0x%04x", + rom_major, rom_minor, dev_type); + return FALSE; + } + + state->capture_prog = prog; + state->capture_prog_len = prog_len; + + /* Check if this is a type-1 device */ + state->is_type1_device = FALSE; + for (gsize i = 0; i < G_N_ELEMENTS (line_update_type1_devices); i++) + { + if (line_update_type1_devices[i] == dev_type) + { + state->is_type1_device = TRUE; + break; + } + } + + /* Extract lines_per_frame from the 2D params chunk */ + { + gsize n_chunks = 0; + ValidityCaptureChunk *chunks = validity_capture_split_chunks (prog, prog_len, &n_chunks); + if (chunks) + { + for (gsize i = 0; i < n_chunks; i++) + { + if (chunks[i].type == CAPT_CHUNK_2D_PARAMS && chunks[i].size >= 4) + { + guint32 lines_2d = FP_READ_UINT32_LE (chunks[i].data); + state->lines_per_frame = (guint16)(lines_2d * type_info->repeat_multiplier); + break; + } + } + validity_capture_chunks_free (chunks, n_chunks); + } + } + + state->bytes_per_line = type_info->bytes_per_line; + + /* Set calibration parameters. + * For sensor_type 0xb5 (and all type-1 devices with the same geometry + * as 0x199): key_calibration_line = lines_per_calibration_data / 2, + * calibration_frames = 3, calibration_iterations = 3. + * Derived from python-validity (0x199 has identical SensorTypeInfo). */ + state->key_calibration_line = type_info->lines_per_calibration_data / 2; + state->calibration_frames = 3; + state->calibration_iterations = 3; + + /* Parse factory bits if available */ + if (factory_bits && factory_bits_len > 0) + { + validity_capture_parse_factory_bits (factory_bits, factory_bits_len, + &state->factory_calibration_values, + &state->factory_calibration_values_len, + &state->factory_calib_data, + &state->factory_calib_data_len); + } + + return TRUE; +} diff --git a/libfprint/drivers/validity/validity_capture.h b/libfprint/drivers/validity/validity_capture.h new file mode 100644 index 00000000..02cbff35 --- /dev/null +++ b/libfprint/drivers/validity/validity_capture.h @@ -0,0 +1,395 @@ +/* + * Capture program infrastructure for Validity/Synaptics VCSFW sensors + * + * Implements the capture command builder (cmd 0x02), timeslot table + * patching, factory calibration parsing, frame averaging, and + * calibration data processing. + * + * Reference: python-validity sensor.py, timeslot.py, generated_tables.py + * + * 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 +#include "validity_sensor.h" + +/* ================================================================ + * Capture mode — passed to build_cmd_02 to select capture behavior + * Values match python-validity CaptureMode enum. + * ================================================================ */ +typedef enum { + VALIDITY_CAPTURE_CALIBRATE = 1, + VALIDITY_CAPTURE_IDENTIFY = 2, + VALIDITY_CAPTURE_ENROLL = 3, +} ValidityCaptureMode; + +/* ================================================================ + * Capture program chunk — TLV entry from the CaptureProg table. + * Format: type(2LE) | size(2LE) | data(size bytes) + * ================================================================ */ +typedef struct +{ + guint16 type; + guint16 size; + guint8 *data; /* owned; g_free() when done */ +} ValidityCaptureChunk; + +/* ================================================================ + * Capture state — kept in the device struct, initialized during open + * ================================================================ */ +typedef struct +{ + /* Capture program binary (from CaptureProg table lookup) */ + const guint8 *capture_prog; + gsize capture_prog_len; + + /* Geometry derived from SensorTypeInfo + CaptureProg */ + guint16 lines_per_frame; + guint16 bytes_per_line; + + /* Calibration parameters (derived from sensor geometry) */ + guint16 key_calibration_line; + guint8 calibration_frames; + guint8 calibration_iterations; + + /* Factory calibration values (from factory_bits subtag 3) */ + guint8 *factory_calibration_values; + gsize factory_calibration_values_len; + + /* Optional factory calibration data (from factory_bits subtag 7) */ + guint8 *factory_calib_data; + gsize factory_calib_data_len; + + /* Accumulated calibration data (built during calibration loop) */ + guint8 *calib_data; + gsize calib_data_len; + + /* Whether this is a line_update_type_1 device */ + gboolean is_type1_device; +} ValidityCaptureState; + +/* ================================================================ + * Chunk parsing — split/merge TLV-encoded capture program + * ================================================================ */ + +/* + * Split a capture program binary into an array of chunks. + * Returns an array of ValidityCaptureChunk (caller must free with + * validity_capture_chunks_free). Sets n_chunks to the count. + * Returns NULL on parse error. + */ +ValidityCaptureChunk *validity_capture_split_chunks (const guint8 *data, + gsize data_len, + gsize *n_chunks); + +/* + * Merge an array of chunks back into a binary blob. + * Caller must g_free() the returned buffer. + * Returns NULL on error and sets out_len to 0. + */ +guint8 *validity_capture_merge_chunks (const ValidityCaptureChunk *chunks, + gsize n_chunks, + gsize *out_len); + +/* + * Free an array of chunks (including each chunk's data). + */ +void validity_capture_chunks_free (ValidityCaptureChunk *chunks, + gsize n_chunks); + +/* ================================================================ + * Timeslot instruction decoder + * + * The timeslot table is a bytecode program for the sensor DSP. + * Each instruction is 1-3 bytes. We need to decode them to find + * specific instructions for patching. + * + * Returns: opcode in *opcode, instruction length in *insn_len, + * operands in operands[] (up to 3), count in *n_operands. + * Returns FALSE if the instruction cannot be decoded. + * ================================================================ */ +gboolean validity_capture_decode_insn (const guint8 *data, + gsize data_len, + guint8 *opcode, + guint8 *insn_len, + guint32 operands[3], + guint8 *n_operands); + +/* Timeslot instruction opcodes */ +#define TST_OP_NOOP 0 +#define TST_OP_END_OF_TABLE 1 +#define TST_OP_RETURN 2 +#define TST_OP_CLEAR_SO 3 +#define TST_OP_END_OF_DATA 4 +#define TST_OP_MACRO 5 +#define TST_OP_ENABLE_RX 6 +#define TST_OP_IDLE_RX 7 +#define TST_OP_ENABLE_SO 8 +#define TST_OP_DISABLE_SO 9 +#define TST_OP_INTERRUPT 10 +#define TST_OP_CALL 11 +#define TST_OP_FEATURES 12 +#define TST_OP_REG_WRITE 13 +#define TST_OP_SAMPLE 14 +#define TST_OP_SAMPLE_REPEAT 15 + +/* + * Find the Nth instruction with the given opcode. + * Returns the byte offset (pc) of the instruction, or -1 if not found. + */ +gssize validity_capture_find_nth_insn (const guint8 *data, + gsize data_len, + guint8 target_opcode, + guint n); + +/* + * Find the Nth Register Write instruction to a specific register address. + * Returns the byte offset (pc), or -1 if not found. + */ +gssize validity_capture_find_nth_regwrite (const guint8 *data, + gsize data_len, + guint32 reg_addr, + guint n); + +/* ================================================================ + * Timeslot table patching + * ================================================================ */ + +/* + * First pass: patch the timeslot table multiplier. + * For each "Call" instruction (opcode 0x10-0x17), if repeat > 1, + * multiply it by mult and optionally increment the address. + * Modifies data in-place. Returns TRUE on success. + */ +gboolean validity_capture_patch_timeslot_table (guint8 *data, + gsize data_len, + gboolean inc_address, + guint8 mult); + +/* + * Second pass: find the register write to 0x8000203C in the Call + * target subroutine and patch its value with the factory calibration + * value at key_calibration_line. + * Modifies data in-place. Returns TRUE on success; FALSE means + * no matching instruction was found (non-fatal). + */ +gboolean validity_capture_patch_timeslot_again (guint8 *data, + gsize data_len, + const guint8 *factory_calibration_values, + gsize factory_cal_len, + guint16 key_calibration_line); + +/* ================================================================ + * build_cmd_02 — main capture command builder + * ================================================================ */ + +/* + * Build a capture command (cmd 0x02). + * Format: 0x02 | bytes_per_line(2LE) | req_lines(2LE) | merged_chunks + * + * The capture program is loaded from capture_state->capture_prog, + * split into chunks, patched (timeslot, line_update), and reassembled. + * + * Returns a newly-allocated buffer (caller must g_free) or NULL on error. + * Sets out_len to the buffer size. + */ +guint8 *validity_capture_build_cmd_02 (const ValidityCaptureState *capture, + const ValiditySensorTypeInfo *type_info, + ValidityCaptureMode mode, + gsize *out_len); + +/* ================================================================ + * Factory bits parsing + * ================================================================ */ + +/* + * Parse the raw factory bits response (from cmd 0x6f). + * Extracts subtag 3 (factory_calibration_values) and optionally + * subtag 7 (factory_calib_data). + * + * Response format (after 2-byte status, already stripped): + * wtf(4LE) | entries(4LE) | entry[]: + * ptr(4LE) | length(2LE) | tag(2LE) | subtag(2LE) | flags(2LE) | data(length) + * + * Returns TRUE if at least subtag 3 was found. + */ +gboolean validity_capture_parse_factory_bits (const guint8 *data, + gsize data_len, + guint8 **cal_values, + gsize *cal_values_len, + guint8 **cal_data, + gsize *cal_data_len); + +/* ================================================================ + * Frame averaging + * ================================================================ */ + +/* + * Average raw multi-frame capture data from EP 0x82. + * Deinterlaces frames and averages interleaved lines. + * + * Returns a newly-allocated buffer (one frame), or NULL on error. + * The caller must g_free() the buffer. + */ +guint8 *validity_capture_average_frames (const guint8 *raw_data, + gsize raw_len, + guint16 lines_per_frame, + guint16 bytes_per_line, + guint16 lines_per_calibration_data, + guint8 calibration_frames, + gsize *out_len); + +/* ================================================================ + * Calibration data processing + * ================================================================ */ + +/* + * Process averaged calibration frame into calibration data. + * Applies scaling and accumulates with existing calib_data. + * Updates calib_data/calib_data_len in place. + * + * If calib_data is NULL, initializes from the frame. + * If non-NULL, combines (adds signed values, clips). + */ +void validity_capture_process_calibration (guint8 **calib_data, + gsize *calib_data_len, + const guint8 *averaged_frame, + gsize frame_len, + guint16 bytes_per_line); + +/* + * Build the clean slate format for flash persistence. + * Format: magic(2LE=0x5002) | inner_len(2LE) | sha256(32) | zeroes(32) | + * data_len(2LE) | data | trailing_zero(2LE=0) + * + * Returns a newly-allocated buffer or NULL on error. + */ +guint8 *validity_capture_build_clean_slate (const guint8 *averaged_frame, + gsize frame_len, + gsize *out_len); + +/* + * Verify clean slate format (check magic and SHA256 hash). + * Returns TRUE if the format is valid. + */ +gboolean validity_capture_verify_clean_slate (const guint8 *data, + gsize data_len); + +/* ================================================================ + * Bitpack — compress factory calibration values for Line Update + * ================================================================ */ + +/* + * Pack calibration values using minimum-bit encoding. + * Returns packed data, minimum value (v1), bit count (v0), + * and packed length. + * + * Python-validity bitpack(): find min, max delta, encode each value + * as (value - min) in minimum bits, pack as little-endian. + * + * Returns a newly-allocated buffer or NULL on error. + */ +guint8 *validity_capture_bitpack (const guint8 *values, + gsize values_len, + guint8 *out_v0, + guint8 *out_v1, + gsize *out_len); + +/* ================================================================ + * Finger ID mapping + * ================================================================ */ + +/* + * Map FpFinger enum value to VCSFW finger subtype (1-10). + * Returns 0 if the finger is not recognized. + */ +guint16 validity_finger_to_subtype (guint finger); + +/* + * Map VCSFW finger subtype (1-10) to FpFinger enum value. + * Returns -1 if the subtype is not recognized. + */ +gint validity_subtype_to_finger (guint16 subtype); + +/* ================================================================ + * LED control commands — for user feedback during capture + * ================================================================ */ + +/* + * Build the LED "glow start scan" command (sent via TLS app). + * Returns a static buffer and sets out_len. + */ +const guint8 *validity_capture_glow_start_cmd (gsize *out_len); + +/* + * Build the LED "glow end scan" command (sent via TLS app). + * Returns a static buffer and sets out_len. + */ +const guint8 *validity_capture_glow_end_cmd (gsize *out_len); + +/* ================================================================ + * CaptureProg table lookup + * ================================================================ */ + +/* + * Look up the capture program for a given ROM version and sensor type. + * Returns a pointer to the static blob data and sets out_len. + * Returns NULL if no matching entry is found. + */ +const guint8 *validity_capture_prog_lookup (guint8 rom_major, + guint8 rom_minor, + guint16 dev_type, + gsize *out_len); + +/* ================================================================ + * Capture state lifecycle + * ================================================================ */ + +void validity_capture_state_init (ValidityCaptureState *state); +void validity_capture_state_clear (ValidityCaptureState *state); + +/* + * Initialize capture state from sensor info and factory bits. + * Loads the CaptureProg, computes geometry, sets calibration params. + * Returns TRUE on success. + */ +gboolean validity_capture_state_setup (ValidityCaptureState *state, + const ValiditySensorTypeInfo *type_info, + guint16 dev_type, + guint8 rom_major, + guint8 rom_minor, + const guint8 *factory_bits, + gsize factory_bits_len); + +/* Chunk type IDs used in capture programs */ +#define CAPT_CHUNK_REPLY_CONFIG 0x0017 +#define CAPT_CHUNK_FINGER_DETECT 0x0026 +#define CAPT_CHUNK_IMAGE_RECON 0x002e +#define CAPT_CHUNK_2D_PARAMS 0x002f +#define CAPT_CHUNK_LINE_UPDATE 0x0030 +#define CAPT_CHUNK_TIMESLOT_2D 0x0034 +#define CAPT_CHUNK_TS_OFFSET 0x0029 +#define CAPT_CHUNK_TS_FD_OFFSET 0x0035 +#define CAPT_CHUNK_LINE_UPDATE_XFORM 0x0043 +#define CAPT_CHUNK_INTERLEAVE 0x0044 +#define CAPT_CHUNK_WTF 0x004e + +/* Capture program cookie for the ACM Config chunk */ +#define CAPT_CHUNK_ACM_CONFIG 0x002a +#define CAPT_CHUNK_CEM_CONFIG 0x002c diff --git a/libfprint/meson.build b/libfprint/meson.build index 93dc28c5..bd479567 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -158,7 +158,8 @@ driver_sources = { 'drivers/validity/vcsfw_protocol.c', 'drivers/validity/validity_tls.c', 'drivers/validity/validity_fwext.c', - 'drivers/validity/validity_sensor.c' ], + 'drivers/validity/validity_sensor.c', + 'drivers/validity/validity_capture.c' ], } helper_sources = { diff --git a/tests/meson.build b/tests/meson.build index 503f2e33..fead1719 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -367,6 +367,22 @@ if 'validity' in supported_drivers suite: ['unit-tests'], env: envs, ) + + # Validity capture infrastructure unit tests (needs OpenSSL for clean slate) + if openssl_dep.found() + validity_capture_test = executable('test-validity-capture', + sources: 'test-validity-capture.c', + dependencies: [ libfprint_private_dep, openssl_dep ], + c_args: common_cflags, + link_with: libfprint_drivers, + install: false, + ) + test('validity-capture', + validity_capture_test, + suite: ['unit-tests'], + env: envs, + ) + endif endif # Run udev rule generator with fatal warnings diff --git a/tests/test-validity-capture.c b/tests/test-validity-capture.c new file mode 100644 index 00000000..67149b2c --- /dev/null +++ b/tests/test-validity-capture.c @@ -0,0 +1,1019 @@ +/* + * Unit tests for validity capture infrastructure + * + * 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 "fpi-byte-utils.h" + +#include "drivers/validity/validity_capture.h" +#include "drivers/validity/validity_sensor.h" + +/* ================================================================ + * T5.1: test_split_chunks_basic + * + * Verify that split_chunks correctly parses a TLV buffer with two + * known chunks and produces the right type, size, and data. + * ================================================================ */ +static void +test_split_chunks_basic (void) +{ + /* Build two TLV chunks: + * type=0x002a, size=4, data={0xAA,0xBB,0xCC,0xDD} + * type=0x0034, size=2, data={0x11,0x22} + */ + guint8 buf[] = { + 0x2a, 0x00, 0x04, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, + 0x34, 0x00, 0x02, 0x00, 0x11, 0x22, + }; + + gsize n = 0; + ValidityCaptureChunk *chunks; + + chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); + + g_assert_nonnull (chunks); + g_assert_cmpuint (n, ==, 2); + + g_assert_cmpuint (chunks[0].type, ==, 0x002a); + g_assert_cmpuint (chunks[0].size, ==, 4); + g_assert_cmpmem (chunks[0].data, 4, buf + 4, 4); + + g_assert_cmpuint (chunks[1].type, ==, 0x0034); + g_assert_cmpuint (chunks[1].size, ==, 2); + g_assert_cmpmem (chunks[1].data, 2, buf + 12, 2); + + validity_capture_chunks_free (chunks, n); +} + +/* ================================================================ + * T5.2: test_split_merge_roundtrip + * + * Verify that split then merge produces identical bytes. + * ================================================================ */ +static void +test_split_merge_roundtrip (void) +{ + guint8 buf[] = { + 0x2a, 0x00, 0x08, 0x00, + 0x20, 0x01, 0x01, 0x00, 0x10, 0x01, 0x00, 0x00, + 0x29, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, + }; + + gsize n = 0; + ValidityCaptureChunk *chunks; + + chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); + g_assert_nonnull (chunks); + g_assert_cmpuint (n, ==, 2); + + gsize merged_len = 0; + guint8 *merged = validity_capture_merge_chunks (chunks, n, &merged_len); + + g_assert_nonnull (merged); + g_assert_cmpuint (merged_len, ==, sizeof (buf)); + g_assert_cmpmem (merged, merged_len, buf, sizeof (buf)); + + g_free (merged); + validity_capture_chunks_free (chunks, n); +} + +/* ================================================================ + * T5.3: test_split_chunks_empty + * + * Verify empty input returns empty result. + * ================================================================ */ +static void +test_split_chunks_empty (void) +{ + gsize n = 99; + ValidityCaptureChunk *chunks; + + chunks = validity_capture_split_chunks (NULL, 0, &n); + g_assert_null (chunks); + g_assert_cmpuint (n, ==, 0); +} + +/* ================================================================ + * T5.4: test_split_chunks_truncated + * + * Verify truncated chunk (size extends past end) returns NULL. + * ================================================================ */ +static void +test_split_chunks_truncated (void) +{ + /* type=0x0034, size=0x0008, but only 4 bytes of data follow */ + guint8 buf[] = { + 0x34, 0x00, 0x08, 0x00, 0x11, 0x22, 0x33, 0x44, + }; + + gsize n = 99; + ValidityCaptureChunk *chunks; + + chunks = validity_capture_split_chunks (buf, sizeof (buf), &n); + g_assert_null (chunks); + g_assert_cmpuint (n, ==, 0); +} + +/* ================================================================ + * T5.5: test_decode_insn_noop + * + * Verify NOOP (0x00) decodes to opcode 0 with length 1. + * ================================================================ */ +static void +test_decode_insn_noop (void) +{ + guint8 data[] = { 0x00 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 1, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_NOOP); + g_assert_cmpuint (len, ==, 1); + g_assert_cmpuint (n_ops, ==, 0); +} + +/* ================================================================ + * T5.6: test_decode_insn_call + * + * Verify Call instruction (0x10-0x17) decodes correctly with + * rx_inc, address, and repeat operands. + * ================================================================ */ +static void +test_decode_insn_call (void) +{ + /* Call: rx_inc=2, address=0x0a*4=0x28, repeat=8 */ + guint8 data[] = { 0x12, 0x0a, 0x08 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_CALL); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (n_ops, ==, 3); + g_assert_cmpuint (operands[0], ==, 2); /* rx_inc */ + g_assert_cmpuint (operands[1], ==, 0x28); /* address = 0x0a << 2 */ + g_assert_cmpuint (operands[2], ==, 8); /* repeat */ +} + +/* ================================================================ + * T5.7: test_decode_insn_call_repeat_zero + * + * Verify Call with repeat byte 0x00 decodes to repeat=0x100. + * ================================================================ */ +static void +test_decode_insn_call_repeat_zero (void) +{ + guint8 data[] = { 0x10, 0x05, 0x00 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_CALL); + g_assert_cmpuint (operands[2], ==, 0x100); +} + +/* ================================================================ + * T5.8: test_decode_insn_regwrite + * + * Verify Register Write (0x40-0x7f) decodes correctly: + * register address = (b0 & 0x3f) * 4 + 0x80002000 + * value = u16 LE from bytes 1-2 + * ================================================================ */ +static void +test_decode_insn_regwrite (void) +{ + /* b0=0x4f → reg = (0x0f)*4 + 0x80002000 = 0x8000203C, value=0x1234 */ + guint8 data[] = { 0x4f, 0x34, 0x12 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 3, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_REG_WRITE); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (n_ops, ==, 2); + g_assert_cmpuint (operands[0], ==, 0x8000203c); + g_assert_cmpuint (operands[1], ==, 0x1234); +} + +/* ================================================================ + * T5.9: test_decode_insn_enable_rx + * + * Verify Enable Rx (opcode 6) decodes as 2-byte instruction. + * ================================================================ */ +static void +test_decode_insn_enable_rx (void) +{ + guint8 data[] = { 0x06, 0x42 }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 2, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_ENABLE_RX); + g_assert_cmpuint (len, ==, 2); + g_assert_cmpuint (n_ops, ==, 1); + g_assert_cmpuint (operands[0], ==, 0x42); +} + +/* ================================================================ + * T5.10: test_decode_insn_sample + * + * Verify Sample (0x80-0xbf) decodes with two operands. + * ================================================================ */ +static void +test_decode_insn_sample (void) +{ + /* b0=0x8a → operand0 = (0x0a >> 3) & 7 = 1, operand1 = 0x0a & 7 = 2 */ + guint8 data[] = { 0x8a }; + guint8 opcode, len, n_ops; + guint32 operands[3]; + + g_assert_true (validity_capture_decode_insn (data, 1, &opcode, &len, + operands, &n_ops)); + g_assert_cmpuint (opcode, ==, TST_OP_SAMPLE); + g_assert_cmpuint (len, ==, 1); + g_assert_cmpuint (n_ops, ==, 2); + g_assert_cmpuint (operands[0], ==, 1); + g_assert_cmpuint (operands[1], ==, 2); +} + +/* ================================================================ + * T5.11: test_find_nth_insn + * + * Verify finding the Nth instruction of a given opcode in a buffer. + * ================================================================ */ +static void +test_find_nth_insn (void) +{ + /* Buffer: NOOP, NOOP, Call(rx=0,addr=0x14,rep=1), NOOP */ + guint8 data[] = { + 0x00, /* NOOP at offset 0 */ + 0x00, /* NOOP at offset 1 */ + 0x10, 0x05, 0x01, /* Call at offset 2 */ + 0x00, /* NOOP at offset 5 */ + }; + + /* 1st NOOP is at offset 0 */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_NOOP, 1), ==, 0); + /* 2nd NOOP is at offset 1 */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_NOOP, 2), ==, 1); + /* 3rd NOOP is at offset 5 */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_NOOP, 3), ==, 5); + /* 1st Call is at offset 2 */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_CALL, 1), ==, 2); + /* No 2nd Call */ + g_assert_cmpint (validity_capture_find_nth_insn (data, sizeof (data), + TST_OP_CALL, 2), ==, -1); +} + +/* ================================================================ + * T5.12: test_find_nth_regwrite + * + * Verify finding a Register Write to a specific register address. + * ================================================================ */ +static void +test_find_nth_regwrite (void) +{ + /* Buffer: RegWrite(0x80002000, 0x55), RegWrite(0x8000203C, 0xAB) */ + guint8 data[] = { + 0x40, 0x55, 0x00, /* reg = 0x80002000, val = 0x0055 */ + 0x4f, 0xAB, 0x00, /* reg = 0x8000203C, val = 0x00AB */ + }; + + /* Find 1st write to 0x8000203C → offset 3 */ + g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), + 0x8000203c, 1), ==, 3); + /* No 2nd write to 0x8000203C */ + g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), + 0x8000203c, 2), ==, -1); + /* Find 1st write to 0x80002000 → offset 0 */ + g_assert_cmpint (validity_capture_find_nth_regwrite (data, sizeof (data), + 0x80002000, 1), ==, 0); +} + +/* ================================================================ + * T5.13: test_patch_timeslot_table + * + * Verify that patch_timeslot_table multiplies Call repeat counts + * by the given multiplier. + * ================================================================ */ +static void +test_patch_timeslot_table (void) +{ + /* Call(rx=0, addr=0x14, repeat=3) followed by NOOP */ + guint8 data[] = { + 0x10, 0x05, 0x03, /* Call: repeat=3 */ + 0x00, /* NOOP */ + }; + + /* Multiply by 2, with inc_address=TRUE */ + g_assert_true (validity_capture_patch_timeslot_table (data, sizeof (data), + TRUE, 2)); + + /* repeat becomes 3*2=6 */ + g_assert_cmpuint (data[2], ==, 6); + /* address byte incremented */ + g_assert_cmpuint (data[1], ==, 6); +} + +/* ================================================================ + * T5.14: test_patch_timeslot_table_no_mult_for_repeat1 + * + * Verify that Call instructions with repeat <= 1 are NOT multiplied. + * ================================================================ */ +static void +test_patch_timeslot_table_no_mult_for_repeat1 (void) +{ + guint8 data[] = { + 0x10, 0x05, 0x01, /* Call: repeat=1 */ + 0x00, + }; + + g_assert_true (validity_capture_patch_timeslot_table (data, sizeof (data), + TRUE, 4)); + /* repeat stays 1 (not multiplied because <= 1) */ + g_assert_cmpuint (data[2], ==, 1); + /* address NOT incremented */ + g_assert_cmpuint (data[1], ==, 5); +} + +/* ================================================================ + * T5.15: test_bitpack_uniform + * + * When all values are identical, bitpack returns v0=0 (0 bits), + * v1=the common value, and zero-length packed data. + * ================================================================ */ +static void +test_bitpack_uniform (void) +{ + guint8 values[] = { 0x42, 0x42, 0x42, 0x42 }; + guint8 v0, v1; + gsize out_len; + + guint8 *packed = validity_capture_bitpack (values, 4, &v0, &v1, &out_len); + + g_assert_nonnull (packed); + g_assert_cmpuint (v0, ==, 0); + g_assert_cmpuint (v1, ==, 0x42); + g_assert_cmpuint (out_len, ==, 0); + + g_free (packed); +} + +/* ================================================================ + * T5.16: test_bitpack_range + * + * Verify bitpack with a small range of values. + * Values [10, 11, 12, 13] → delta range=3, useful_bits=2. + * ================================================================ */ +static void +test_bitpack_range (void) +{ + guint8 values[] = { 10, 11, 12, 13 }; + guint8 v0, v1; + gsize out_len; + + guint8 *packed = validity_capture_bitpack (values, 4, &v0, &v1, &out_len); + + g_assert_nonnull (packed); + g_assert_cmpuint (v0, ==, 2); /* 2 bits needed for max delta 3 */ + g_assert_cmpuint (v1, ==, 10); /* minimum value */ + + /* 4 values * 2 bits = 8 bits = 1 byte */ + g_assert_cmpuint (out_len, ==, 1); + + /* Deltas: [0, 1, 2, 3] + * Packed little-endian: bits 0-1 = 0b00, bits 2-3 = 0b01, + * bits 4-5 = 0b10, bits 6-7 = 0b11 + * Byte = 0b11100100 = 0xE4 */ + g_assert_cmpuint (packed[0], ==, 0xE4); + + g_free (packed); +} + +/* ================================================================ + * T5.17: test_factory_bits_parsing + * + * Verify parsing a synthetic factory bits response with subtag 3 + * (calibration values) and subtag 7 (calibration data). + * ================================================================ */ +static void +test_factory_bits_parsing (void) +{ + /* Factory bits response format: + * wtf(4LE) entries(4LE) + * entry: ptr(4LE) length(2LE) tag(2LE) subtag(2LE) flags(2LE) data[length] + */ + guint8 cal_values[] = { 0xAA, 0xBB, 0xCC, 0xDD }; + guint8 cal_data[] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 }; + + /* Build response buffer */ + GByteArray *resp = g_byte_array_new (); + guint8 hdr[8]; + + /* Header: wtf=0, entries=2 */ + FP_WRITE_UINT32_LE (hdr, 0); + FP_WRITE_UINT32_LE (hdr + 4, 2); + g_byte_array_append (resp, hdr, 8); + + /* Entry 1: subtag=3, calibration values (4-byte header + actual data) */ + { + guint8 entry[12]; + guint16 length = 4 + sizeof (cal_values); /* 4-byte header + data */ + FP_WRITE_UINT32_LE (entry, 0); /* ptr */ + FP_WRITE_UINT16_LE (entry + 4, length); /* length */ + FP_WRITE_UINT16_LE (entry + 6, 0x0001); /* tag */ + FP_WRITE_UINT16_LE (entry + 8, 3); /* subtag = 3 */ + FP_WRITE_UINT16_LE (entry + 10, 0); /* flags */ + g_byte_array_append (resp, entry, 12); + + guint8 data_hdr[4] = { 0, 0, 0, 0 }; /* 4-byte header */ + g_byte_array_append (resp, data_hdr, 4); + g_byte_array_append (resp, cal_values, sizeof (cal_values)); + } + + /* Entry 2: subtag=7, calibration data (4-byte header + actual data) */ + { + guint8 entry[12]; + guint16 length = 4 + sizeof (cal_data); + FP_WRITE_UINT32_LE (entry, 0); + FP_WRITE_UINT16_LE (entry + 4, length); + FP_WRITE_UINT16_LE (entry + 6, 0x0002); + FP_WRITE_UINT16_LE (entry + 8, 7); /* subtag = 7 */ + FP_WRITE_UINT16_LE (entry + 10, 0); + g_byte_array_append (resp, entry, 12); + + guint8 data_hdr[4] = { 0, 0, 0, 0 }; + g_byte_array_append (resp, data_hdr, 4); + g_byte_array_append (resp, cal_data, sizeof (cal_data)); + } + + guint8 *out_cal_values = NULL, *out_cal_data = NULL; + gsize out_cal_values_len = 0, out_cal_data_len = 0; + + gboolean ok = validity_capture_parse_factory_bits ( + resp->data, resp->len, + &out_cal_values, &out_cal_values_len, + &out_cal_data, &out_cal_data_len); + + g_assert_true (ok); + g_assert_nonnull (out_cal_values); + g_assert_cmpuint (out_cal_values_len, ==, sizeof (cal_values)); + g_assert_cmpmem (out_cal_values, out_cal_values_len, + cal_values, sizeof (cal_values)); + + g_assert_nonnull (out_cal_data); + g_assert_cmpuint (out_cal_data_len, ==, sizeof (cal_data)); + g_assert_cmpmem (out_cal_data, out_cal_data_len, + cal_data, sizeof (cal_data)); + + g_free (out_cal_values); + g_free (out_cal_data); + g_byte_array_free (resp, TRUE); +} + +/* ================================================================ + * T5.18: test_factory_bits_no_subtag3 + * + * Verify that parsing fails when subtag 3 is missing. + * ================================================================ */ +static void +test_factory_bits_no_subtag3 (void) +{ + /* Build response with only subtag=7 (no subtag=3) */ + guint8 buf[32]; + FP_WRITE_UINT32_LE (buf, 0); /* wtf */ + FP_WRITE_UINT32_LE (buf + 4, 1); /* entries=1 */ + + /* Entry: subtag=7, length=5 (4 hdr + 1 data) */ + FP_WRITE_UINT32_LE (buf + 8, 0); + FP_WRITE_UINT16_LE (buf + 12, 5); + FP_WRITE_UINT16_LE (buf + 14, 0x0001); + FP_WRITE_UINT16_LE (buf + 16, 7); /* subtag=7, not 3 */ + FP_WRITE_UINT16_LE (buf + 18, 0); + memset (buf + 20, 0, 5); + + guint8 *cv = NULL; + gsize cv_len = 0; + + gboolean ok = validity_capture_parse_factory_bits (buf, 25, + &cv, &cv_len, + NULL, NULL); + g_assert_false (ok); + g_assert_null (cv); +} + +/* ================================================================ + * T5.19: test_average_frames_interleave2 + * + * Verify frame averaging with interleave_lines=2 (repeat_multiplier=2). + * With 2 interleaved lines per calibration line, each output line + * should be the average of 2 input lines. + * ================================================================ */ +static void +test_average_frames_interleave2 (void) +{ + guint16 bytes_per_line = 4; + guint16 lines_per_calibration_data = 2; + guint16 lines_per_frame = 4; /* 2 cal lines * 2 interleave */ + guint8 calibration_frames = 1; + + /* Single frame: 4 lines * 4 bytes = 16 bytes */ + guint8 raw[] = { + 10, 20, 30, 40, /* line 0 (cal line 0, interleave 0) */ + 20, 30, 40, 50, /* line 1 (cal line 0, interleave 1) */ + 30, 40, 50, 60, /* line 2 (cal line 1, interleave 0) */ + 40, 50, 60, 70, /* line 3 (cal line 1, interleave 1) */ + }; + + gsize out_len = 0; + guint8 *result = validity_capture_average_frames ( + raw, sizeof (raw), + lines_per_frame, bytes_per_line, + lines_per_calibration_data, calibration_frames, + &out_len); + + g_assert_nonnull (result); + /* Output: 2 cal lines * 4 bytes = 8 bytes */ + g_assert_cmpuint (out_len, ==, 8); + + /* Cal line 0: avg of lines 0+1 → (10+20)/2=15, (20+30)/2=25, etc. */ + g_assert_cmpuint (result[0], ==, 15); + g_assert_cmpuint (result[1], ==, 25); + g_assert_cmpuint (result[2], ==, 35); + g_assert_cmpuint (result[3], ==, 45); + + /* Cal line 1: avg of lines 2+3 → (30+40)/2=35, (40+50)/2=45, etc. */ + g_assert_cmpuint (result[4], ==, 35); + g_assert_cmpuint (result[5], ==, 45); + g_assert_cmpuint (result[6], ==, 55); + g_assert_cmpuint (result[7], ==, 65); + + g_free (result); +} + +/* ================================================================ + * T5.20: test_clean_slate_roundtrip + * + * Verify that building a clean slate and then verifying it succeeds. + * ================================================================ */ +static void +test_clean_slate_roundtrip (void) +{ + guint8 test_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + gsize slate_len = 0; + + guint8 *slate = validity_capture_build_clean_slate (test_data, + sizeof (test_data), + &slate_len); + + g_assert_nonnull (slate); + g_assert_cmpuint (slate_len, >, 68); + + /* Magic should be 0x5002 */ + g_assert_cmpuint (FP_READ_UINT16_LE (slate), ==, 0x5002); + + /* Verify should pass */ + g_assert_true (validity_capture_verify_clean_slate (slate, slate_len)); + + /* Corrupt one byte and verify should fail */ + slate[70] ^= 0xff; + g_assert_false (validity_capture_verify_clean_slate (slate, slate_len)); + + g_free (slate); +} + +/* ================================================================ + * T5.21: test_finger_mapping + * + * Verify all 10 finger mappings work in both directions. + * ================================================================ */ +static void +test_finger_mapping (void) +{ + /* FpFinger enum: LEFT_THUMB=1, ..., RIGHT_LITTLE=10 */ + for (guint f = 1; f <= 10; f++) + { + guint16 subtype = validity_finger_to_subtype (f); + g_assert_cmpuint (subtype, ==, f); + + gint back = validity_subtype_to_finger (subtype); + g_assert_cmpint (back, ==, (gint) f); + } + + /* Out of range */ + g_assert_cmpuint (validity_finger_to_subtype (0), ==, 0); + g_assert_cmpuint (validity_finger_to_subtype (11), ==, 0); + g_assert_cmpint (validity_subtype_to_finger (0), ==, -1); + g_assert_cmpint (validity_subtype_to_finger (11), ==, -1); +} + +/* ================================================================ + * T5.22: test_led_commands + * + * Verify LED start/end commands have correct format. + * ================================================================ */ +static void +test_led_commands (void) +{ + gsize start_len = 0, end_len = 0; + const guint8 *start_cmd = validity_capture_glow_start_cmd (&start_len); + const guint8 *end_cmd = validity_capture_glow_end_cmd (&end_len); + + g_assert_nonnull (start_cmd); + g_assert_nonnull (end_cmd); + + /* Both should be 128 bytes (LED control payload) */ + g_assert_cmpuint (start_len, ==, 128); + g_assert_cmpuint (end_len, ==, 128); + + /* Both should start with cmd byte 0x39 */ + g_assert_cmpuint (start_cmd[0], ==, 0x39); + g_assert_cmpuint (end_cmd[0], ==, 0x39); +} + +/* ================================================================ + * T5.23: test_capture_prog_lookup + * + * Verify that CaptureProg lookup returns data for known devices + * and NULL for unknown ones. + * ================================================================ */ +static void +test_capture_prog_lookup (void) +{ + gsize len = 0; + + /* Known: firmware 6.x, dev_type 0xb5 */ + const guint8 *prog = validity_capture_prog_lookup (6, 7, 0x00b5, &len); + g_assert_nonnull (prog); + g_assert_cmpuint (len, >, 0); + + /* The program should be parseable as TLV chunks */ + gsize n_chunks = 0; + ValidityCaptureChunk *chunks = validity_capture_split_chunks (prog, len, &n_chunks); + g_assert_nonnull (chunks); + g_assert_cmpuint (n_chunks, >=, 4); /* At least ACM, CEM, TST, offset */ + + /* Check that we have the expected chunk types */ + gboolean has_acm = FALSE, has_tst = FALSE, has_2d = FALSE; + for (gsize i = 0; i < n_chunks; i++) + { + if (chunks[i].type == 0x002a) has_acm = TRUE; + if (chunks[i].type == CAPT_CHUNK_TIMESLOT_2D) has_tst = TRUE; + if (chunks[i].type == CAPT_CHUNK_2D_PARAMS) has_2d = TRUE; + } + g_assert_true (has_acm); + g_assert_true (has_tst); + g_assert_true (has_2d); + + validity_capture_chunks_free (chunks, n_chunks); + + /* Also check 0x0885 (same geometry) */ + prog = validity_capture_prog_lookup (6, 0, 0x0885, &len); + g_assert_nonnull (prog); + + /* Unknown: firmware 5.x */ + prog = validity_capture_prog_lookup (5, 0, 0x00b5, &len); + g_assert_null (prog); + + /* Unknown: dev_type not in type1 list */ + prog = validity_capture_prog_lookup (6, 0, 0x1234, &len); + g_assert_null (prog); +} + +/* ================================================================ + * T5.24: test_capture_state_setup + * + * Verify that state setup correctly initializes all fields from + * sensor type info and factory bits. + * ================================================================ */ +static void +test_capture_state_setup (void) +{ + ValidityCaptureState state; + const ValiditySensorTypeInfo *type_info; + + type_info = validity_sensor_type_info_lookup (0x00b5); + g_assert_nonnull (type_info); + + /* Build minimal factory bits response with subtag 3 */ + guint8 cal_vals[] = { 0x10, 0x20, 0x30 }; + GByteArray *fb = g_byte_array_new (); + guint8 hdr[8]; + FP_WRITE_UINT32_LE (hdr, 0); + FP_WRITE_UINT32_LE (hdr + 4, 1); + g_byte_array_append (fb, hdr, 8); + + guint8 entry[12]; + guint16 length = 4 + sizeof (cal_vals); + FP_WRITE_UINT32_LE (entry, 0); + FP_WRITE_UINT16_LE (entry + 4, length); + FP_WRITE_UINT16_LE (entry + 6, 1); + FP_WRITE_UINT16_LE (entry + 8, 3); + FP_WRITE_UINT16_LE (entry + 10, 0); + g_byte_array_append (fb, entry, 12); + + guint8 data_hdr[4] = { 0 }; + g_byte_array_append (fb, data_hdr, 4); + g_byte_array_append (fb, cal_vals, sizeof (cal_vals)); + + validity_capture_state_init (&state); + gboolean ok = validity_capture_state_setup (&state, type_info, + 0x00b5, 6, 7, + fb->data, fb->len); + + g_assert_true (ok); + g_assert_true (state.is_type1_device); + g_assert_cmpuint (state.bytes_per_line, ==, 0x78); + g_assert_cmpuint (state.lines_per_frame, ==, 112 * 2); /* 224 */ + g_assert_cmpuint (state.key_calibration_line, ==, 56); /* 112/2 */ + g_assert_cmpuint (state.calibration_frames, ==, 3); + g_assert_cmpuint (state.calibration_iterations, ==, 3); + + g_assert_nonnull (state.factory_calibration_values); + g_assert_cmpuint (state.factory_calibration_values_len, ==, sizeof (cal_vals)); + g_assert_cmpmem (state.factory_calibration_values, + state.factory_calibration_values_len, + cal_vals, sizeof (cal_vals)); + + g_assert_nonnull (state.capture_prog); + g_assert_cmpuint (state.capture_prog_len, >, 0); + + validity_capture_state_clear (&state); + g_byte_array_free (fb, TRUE); +} + +/* ================================================================ + * T5.25: test_build_cmd_02_header + * + * Verify that build_cmd_02 produces the expected 5-byte header: + * cmd(0x02) | bytes_per_line(2LE) | req_lines(2LE) | chunks... + * ================================================================ */ +static void +test_build_cmd_02_header (void) +{ + ValidityCaptureState state; + const ValiditySensorTypeInfo *type_info; + + type_info = validity_sensor_type_info_lookup (0x00b5); + g_assert_nonnull (type_info); + + validity_capture_state_init (&state); + + /* Minimal setup: just enough for build_cmd_02 */ + gsize prog_len; + state.capture_prog = validity_capture_prog_lookup (6, 7, 0x00b5, &prog_len); + g_assert_nonnull (state.capture_prog); + state.capture_prog_len = prog_len; + state.is_type1_device = TRUE; + state.bytes_per_line = type_info->bytes_per_line; + state.lines_per_frame = 224; + state.calibration_frames = 3; + state.key_calibration_line = 56; + + /* Need factory calibration values (even if empty) for line_update */ + state.factory_calibration_values = g_malloc0 (112); + state.factory_calibration_values_len = 112; + + gsize cmd_len = 0; + guint8 *cmd = validity_capture_build_cmd_02 (&state, type_info, + VALIDITY_CAPTURE_CALIBRATE, + &cmd_len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (cmd_len, >=, 5); + + /* Byte 0: command = 0x02 */ + g_assert_cmpuint (cmd[0], ==, 0x02); + + /* Bytes 1-2: bytes_per_line = 0x0078 */ + g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 1), ==, 0x0078); + + /* Bytes 3-4: req_lines for CALIBRATE = frames * lines_per_frame + 1 */ + guint16 expected_lines = 3 * 224 + 1; + g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 3), ==, expected_lines); + + /* Remainder should be parseable as TLV chunks */ + gsize n_chunks = 0; + ValidityCaptureChunk *chunks = validity_capture_split_chunks ( + cmd + 5, cmd_len - 5, &n_chunks); + g_assert_nonnull (chunks); + g_assert_cmpuint (n_chunks, >=, 4); + + validity_capture_chunks_free (chunks, n_chunks); + g_free (cmd); + + /* Test IDENTIFY mode: req_lines should be 0 */ + cmd = validity_capture_build_cmd_02 (&state, type_info, + VALIDITY_CAPTURE_IDENTIFY, + &cmd_len); + g_assert_nonnull (cmd); + g_assert_cmpuint (FP_READ_UINT16_LE (cmd + 3), ==, 0); + g_free (cmd); + + g_free (state.factory_calibration_values); +} + +/* ================================================================ + * T5.26: test_calibration_processing + * + * Verify that process_calibration applies scale and accumulates. + * ================================================================ */ +static void +test_calibration_processing (void) +{ + guint16 bytes_per_line = 16; + /* Single line with 8-byte header + 8 bytes of data */ + guint8 frame[16] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* header (untouched) */ + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, /* data to scale */ + }; + + guint8 *calib = NULL; + gsize calib_len = 0; + + /* First call: initializes calib_data */ + validity_capture_process_calibration (&calib, &calib_len, + frame, sizeof (frame), + bytes_per_line); + + g_assert_nonnull (calib); + g_assert_cmpuint (calib_len, ==, 16); + + /* Header bytes should be preserved */ + g_assert_cmpuint (calib[0], ==, 0x00); + g_assert_cmpuint (calib[7], ==, 0x07); + + /* Data bytes at 0x80: scale(0x80) = (0x80 - 0x80) * 10 / 0x22 = 0 + * So all data bytes should be 0x00 */ + for (int i = 8; i < 16; i++) + g_assert_cmpuint (calib[i], ==, 0x00); + + /* Second call with same frame: accumulate */ + validity_capture_process_calibration (&calib, &calib_len, + frame, sizeof (frame), + bytes_per_line); + + /* add(0, 0) = 0, so data bytes still 0 */ + for (int i = 8; i < 16; i++) + g_assert_cmpuint (calib[i], ==, 0x00); + + g_free (calib); +} + +/* ================================================================ + * T5.27: test_capture_split_real_prog + * + * Parse the actual capture program for 0xb5 and verify + * expected chunks are present. + * ================================================================ */ +static void +test_capture_split_real_prog (void) +{ + gsize prog_len = 0; + const guint8 *prog = validity_capture_prog_lookup (6, 7, 0x00b5, &prog_len); + + g_assert_nonnull (prog); + + gsize n = 0; + ValidityCaptureChunk *chunks = validity_capture_split_chunks (prog, prog_len, &n); + + g_assert_nonnull (chunks); + g_assert_cmpuint (n, ==, 6); + + /* Expected order: 0x2a, 0x2c, 0x34, 0x2f, 0x29, 0x35 */ + g_assert_cmpuint (chunks[0].type, ==, 0x002a); + g_assert_cmpuint (chunks[0].size, ==, 8); + + g_assert_cmpuint (chunks[1].type, ==, 0x002c); + g_assert_cmpuint (chunks[1].size, ==, 40); + + g_assert_cmpuint (chunks[2].type, ==, CAPT_CHUNK_TIMESLOT_2D); + g_assert_cmpuint (chunks[2].size, ==, 64); + + g_assert_cmpuint (chunks[3].type, ==, CAPT_CHUNK_2D_PARAMS); + g_assert_cmpuint (chunks[3].size, ==, 4); + /* 2D value should be 112 (0x70) */ + g_assert_cmpuint (FP_READ_UINT32_LE (chunks[3].data), ==, 112); + + g_assert_cmpuint (chunks[4].type, ==, 0x0029); + g_assert_cmpuint (chunks[4].size, ==, 4); + + g_assert_cmpuint (chunks[5].type, ==, 0x0035); + g_assert_cmpuint (chunks[5].size, ==, 4); + + validity_capture_chunks_free (chunks, n); +} + +/* ================================================================ + * main + * ================================================================ */ +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* Chunk parsing */ + g_test_add_func ("/validity/capture/split-chunks-basic", + test_split_chunks_basic); + g_test_add_func ("/validity/capture/split-merge-roundtrip", + test_split_merge_roundtrip); + g_test_add_func ("/validity/capture/split-chunks-empty", + test_split_chunks_empty); + g_test_add_func ("/validity/capture/split-chunks-truncated", + test_split_chunks_truncated); + + /* Timeslot instruction decoder */ + g_test_add_func ("/validity/capture/decode-insn-noop", + test_decode_insn_noop); + g_test_add_func ("/validity/capture/decode-insn-call", + test_decode_insn_call); + g_test_add_func ("/validity/capture/decode-insn-call-repeat-zero", + test_decode_insn_call_repeat_zero); + g_test_add_func ("/validity/capture/decode-insn-regwrite", + test_decode_insn_regwrite); + g_test_add_func ("/validity/capture/decode-insn-enable-rx", + test_decode_insn_enable_rx); + g_test_add_func ("/validity/capture/decode-insn-sample", + test_decode_insn_sample); + + /* Instruction search */ + g_test_add_func ("/validity/capture/find-nth-insn", + test_find_nth_insn); + g_test_add_func ("/validity/capture/find-nth-regwrite", + test_find_nth_regwrite); + + /* Timeslot patching */ + g_test_add_func ("/validity/capture/patch-timeslot-table", + test_patch_timeslot_table); + g_test_add_func ("/validity/capture/patch-timeslot-no-mult-repeat1", + test_patch_timeslot_table_no_mult_for_repeat1); + + /* Bitpack */ + g_test_add_func ("/validity/capture/bitpack-uniform", + test_bitpack_uniform); + g_test_add_func ("/validity/capture/bitpack-range", + test_bitpack_range); + + /* Factory bits */ + g_test_add_func ("/validity/capture/factory-bits-parsing", + test_factory_bits_parsing); + g_test_add_func ("/validity/capture/factory-bits-no-subtag3", + test_factory_bits_no_subtag3); + + /* Frame averaging */ + g_test_add_func ("/validity/capture/average-frames-interleave2", + test_average_frames_interleave2); + + /* Clean slate */ + g_test_add_func ("/validity/capture/clean-slate-roundtrip", + test_clean_slate_roundtrip); + + /* Finger mapping */ + g_test_add_func ("/validity/capture/finger-mapping", + test_finger_mapping); + + /* LED commands */ + g_test_add_func ("/validity/capture/led-commands", + test_led_commands); + + /* CaptureProg lookup */ + g_test_add_func ("/validity/capture/prog-lookup", + test_capture_prog_lookup); + + /* State setup */ + g_test_add_func ("/validity/capture/state-setup", + test_capture_state_setup); + + /* build_cmd_02 */ + g_test_add_func ("/validity/capture/build-cmd-02-header", + test_build_cmd_02_header); + + /* Calibration processing */ + g_test_add_func ("/validity/capture/calibration-processing", + test_calibration_processing); + + /* Real capture program parsing */ + g_test_add_func ("/validity/capture/split-real-prog", + test_capture_split_real_prog); + + return g_test_run (); +}