diff --git a/libfprint/drivers/validity/validity.h b/libfprint/drivers/validity/validity.h index 74bd09e6..9e827a55 100644 --- a/libfprint/drivers/validity/validity.h +++ b/libfprint/drivers/validity/validity.h @@ -266,3 +266,7 @@ void validity_identify (FpDevice *device); void validity_list (FpDevice *device); void validity_delete (FpDevice *device); void validity_clear_storage (FpDevice *device); + +/* Gallery matching helper (validity_verify.c) */ +FpPrint *validity_find_gallery_match (GPtrArray *gallery, + guint16 subtype); diff --git a/libfprint/drivers/validity/validity_db.c b/libfprint/drivers/validity/validity_db.c index c97cef78..a5240709 100644 --- a/libfprint/drivers/validity/validity_db.c +++ b/libfprint/drivers/validity/validity_db.c @@ -54,6 +54,13 @@ validity_user_storage_clear (ValidityUserStorage *storage) memset (storage, 0, sizeof (*storage)); } +void +validity_match_result_clear (ValidityMatchResult *result) +{ + g_clear_pointer (&result->hash, g_free); + memset (result, 0, sizeof (*result)); +} + void validity_user_clear (ValidityUser *user) { diff --git a/libfprint/drivers/validity/validity_db.h b/libfprint/drivers/validity/validity_db.h index 754a2176..5b84e5e1 100644 --- a/libfprint/drivers/validity/validity_db.h +++ b/libfprint/drivers/validity/validity_db.h @@ -136,6 +136,24 @@ typedef struct ValidityRecordChild *children; /* owned array */ } ValidityRecordChildren; +/* ================================================================ + * Match result — parsed from cmd 0x60 (get_match_result) response + * ================================================================ */ +typedef struct +{ + gboolean matched; /* TRUE if user_dbid (tag 1) was found */ + guint32 user_dbid; /* tag 1: matched user record dbid */ + guint16 subtype; /* tag 3: matched finger subtype */ + guint8 *hash; /* tag 4: match hash (owned, g_free) */ + gsize hash_len; +} ValidityMatchResult; + +void validity_match_result_clear (ValidityMatchResult *result); + +gboolean validity_parse_match_result (const guint8 *data, + gsize data_len, + ValidityMatchResult *result); + /* ================================================================ * Command builders — produce binary TLS command payloads * diff --git a/libfprint/drivers/validity/validity_verify.c b/libfprint/drivers/validity/validity_verify.c index 5507ea68..a2a835a7 100644 --- a/libfprint/drivers/validity/validity_verify.c +++ b/libfprint/drivers/validity/validity_verify.c @@ -167,20 +167,13 @@ verify_start_interrupt_wait (FpiDeviceValidity *self, * tag 4 → hash (variable length) * ================================================================ */ -typedef struct -{ - gboolean matched; - guint32 user_dbid; - guint16 subtype; - guint8 *hash; - gsize hash_len; -} MatchResult; +/* MatchResult is now ValidityMatchResult in validity_db.h */ +typedef ValidityMatchResult MatchResult; static void match_result_clear (MatchResult *r) { - g_clear_pointer (&r->hash, g_free); - memset (r, 0, sizeof (*r)); + validity_match_result_clear (r); } /** @@ -203,10 +196,10 @@ match_result_clear (MatchResult *r) * Returns: %TRUE if parsing succeeded (result may still be !matched * if the dict was empty), %FALSE on malformed data. */ -static gboolean -parse_match_result (const guint8 *data, - gsize data_len, - MatchResult *result) +gboolean +validity_parse_match_result (const guint8 *data, + gsize data_len, + ValidityMatchResult *result) { FpiByteReader reader; guint16 total_len; @@ -276,6 +269,37 @@ parse_match_result (const guint8 *data, return TRUE; } +/** + * validity_find_gallery_match: + * @gallery: (element-type FpPrint): array of gallery prints + * @subtype: sensor finger subtype from match result + * + * Find the gallery print whose finger matches the given sensor subtype. + * Falls back to the first gallery entry if no subtype match is found + * (the sensor confirmed a match; we just can't correlate the subtype). + * + * Returns: (nullable): the matching FpPrint, or %NULL if gallery is empty + */ +FpPrint * +validity_find_gallery_match (GPtrArray *gallery, + guint16 subtype) +{ + if (!gallery || gallery->len == 0) + return NULL; + + gint matched_finger = validity_subtype_to_finger (subtype); + + for (guint i = 0; i < gallery->len; i++) + { + FpPrint *candidate = g_ptr_array_index (gallery, i); + if (fp_print_get_finger (candidate) == (FpFinger) matched_finger) + return candidate; + } + + /* Fallback: sensor confirmed a match but we can't correlate the subtype */ + return g_ptr_array_index (gallery, 0); +} + /* ================================================================ * Verify/Identify SSM * ================================================================ */ @@ -466,7 +490,7 @@ verify_ssm_done (FpiSsm *ssm, if (self->bulk_data && self->bulk_data_len > 0) { - if (parse_match_result (self->bulk_data, self->bulk_data_len, &match)) + if (validity_parse_match_result (self->bulk_data, self->bulk_data_len, &match)) have_match = match.matched; } @@ -481,31 +505,12 @@ verify_ssm_done (FpiSsm *ssm, * the finger subtype. The sensor does the actual 1:N match * internally; we just need to find which gallery FpPrint * corresponds to the matched subtype. */ - FpPrint *gallery_match = NULL; GPtrArray *gallery = NULL; fpi_device_get_identify_data (dev, &gallery); - if (gallery) - { - gint matched_finger = validity_subtype_to_finger (match.subtype); - - for (guint i = 0; i < gallery->len; i++) - { - FpPrint *candidate = g_ptr_array_index (gallery, i); - if (fp_print_get_finger (candidate) == (FpFinger) matched_finger) - { - gallery_match = candidate; - break; - } - } - - /* If no finger match, fall back to first gallery print — - * the sensor confirmed a match even if we can't correlate - * the subtype to a specific gallery entry. */ - if (!gallery_match && gallery->len > 0) - gallery_match = g_ptr_array_index (gallery, 0); - } + FpPrint *gallery_match = validity_find_gallery_match ( + gallery, match.subtype); fpi_device_identify_report (dev, gallery_match, NULL, NULL); } diff --git a/tests/meson.build b/tests/meson.build index d8152bfc..39171365 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -397,6 +397,21 @@ if 'validity' in supported_drivers suite: ['unit-tests'], env: envs, ) + + # Validity verify/identify/delete/clear regression tests + validity_verify_test = executable('test-validity-verify', + sources: 'test-validity-verify.c', + dependencies: [ libfprint_private_dep ], + c_args: common_cflags, + link_with: libfprint_drivers, + link_whole: test_utils, + install: false, + ) + test('validity-verify', + validity_verify_test, + suite: ['unit-tests'], + env: envs, + ) endif # Run udev rule generator with fatal warnings diff --git a/tests/test-validity-verify.c b/tests/test-validity-verify.c new file mode 100644 index 00000000..fb638cc9 --- /dev/null +++ b/tests/test-validity-verify.c @@ -0,0 +1,551 @@ +/* + * Regression tests for validity verify/identify/delete/clear operations. + * + * These tests cover issues found during the Iteration 6 code audit: + * 1. parse_match_result dead while loop — TLV dict not iterated + * 2. ENROLL_CREATE_USER NULL user_id — validity_db_build_identity(NULL) crash + * 3. Identify always returns first gallery print — subtype not matched + * 4. delete_storage_dbid field reused for enrollment — struct field abuse + * 5. Delete SSM non-functional — del_record never called + * 6. match_finger double allocation — 12 bytes freed then 13 allocated + * 7. clear_storage returned NOT_SUPPORTED + * 8. Stale TODOs + * + * 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 "fp-enums.h" +#include "fpi-device.h" +#include "fpi-byte-utils.h" +#include "fp-print.h" +#include "test-device-fake.h" + +#include "drivers/validity/validity.h" +#include "drivers/validity/validity_db.h" +#include "drivers/validity/validity_capture.h" +#include "drivers/validity/vcsfw_protocol.h" + +/* ================================================================ + * Helper: build a TLV dict entry tag(2LE) | len(2LE) | data[len] + * Returns bytes written. + * ================================================================ */ +static gsize +build_tlv_entry (guint8 *buf, guint16 tag, const guint8 *data, guint16 len) +{ + FP_WRITE_UINT16_LE (&buf[0], tag); + FP_WRITE_UINT16_LE (&buf[2], len); + if (len > 0) + memcpy (&buf[4], data, len); + return 4 + len; +} + +/* ================================================================ + * Helper: Build a complete match result payload: + * total_len(2LE) | TLV entries... + * ================================================================ */ +static guint8 * +build_match_payload (guint32 user_dbid, + guint16 subtype, + const guint8 *hash, + gsize hash_len, + gsize *out_len) +{ + /* Max size: 2 (total_len) + 3 entries × (4 header + max data) */ + guint8 *buf = g_new0 (guint8, 256); + gsize pos = 2; /* skip total_len placeholder */ + + /* Tag 1: user_dbid (4 bytes LE) */ + guint8 dbid_data[4]; + FP_WRITE_UINT32_LE (dbid_data, user_dbid); + pos += build_tlv_entry (&buf[pos], 1, dbid_data, 4); + + /* Tag 3: subtype (2 bytes LE) */ + guint8 sub_data[2]; + FP_WRITE_UINT16_LE (sub_data, subtype); + pos += build_tlv_entry (&buf[pos], 3, sub_data, 2); + + /* Tag 4: hash */ + if (hash && hash_len > 0) + pos += build_tlv_entry (&buf[pos], 4, hash, hash_len); + + /* Write total_len at offset 0 */ + FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); + + *out_len = pos; + return buf; +} + +/* ================================================================ + * R1: parse_match_result with valid TLV data + * + * Regression: Issue #1 — dead while loop would never extract fields. + * Verifies that user_dbid, subtype, and hash are correctly parsed + * from a TLV dictionary matching python-validity's parse_dict() format. + * ================================================================ */ +static void +test_parse_match_result_valid (void) +{ + guint8 hash[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; + gsize payload_len; + g_autofree guint8 *payload = build_match_payload ( + 0x00001234, 3, hash, sizeof (hash), &payload_len); + + ValidityMatchResult result = { 0 }; + gboolean ok = validity_parse_match_result (payload, payload_len, &result); + + g_assert_true (ok); + g_assert_true (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0x00001234); + g_assert_cmpuint (result.subtype, ==, 3); + g_assert_nonnull (result.hash); + g_assert_cmpuint (result.hash_len, ==, sizeof (hash)); + g_assert_cmpmem (result.hash, result.hash_len, hash, sizeof (hash)); + + validity_match_result_clear (&result); +} + +/* ================================================================ + * R1b: parse_match_result iterates ALL TLV entries + * + * Regression: The dead while loop would break after first entry. + * Build a dict with tag 3 (subtype) BEFORE tag 1 (user_dbid) to + * ensure the parser doesn't stop after the first entry. + * ================================================================ */ +static void +test_parse_match_result_multi_tags (void) +{ + /* Manually build: total_len(2) | tag3(2+2+2) | tag1(2+2+4) | tag4(2+2+3) */ + guint8 buf[256]; + gsize pos = 2; + + /* Tag 3 first: subtype = 7 */ + guint8 sub[2]; + FP_WRITE_UINT16_LE (sub, 7); + pos += build_tlv_entry (&buf[pos], 3, sub, 2); + + /* Tag 1 second: user_dbid = 0xDEADBEEF */ + guint8 dbid[4]; + FP_WRITE_UINT32_LE (dbid, 0xDEADBEEF); + pos += build_tlv_entry (&buf[pos], 1, dbid, 4); + + /* Tag 4 third: hash = {0x11, 0x22, 0x33} */ + guint8 hash[] = { 0x11, 0x22, 0x33 }; + pos += build_tlv_entry (&buf[pos], 4, hash, 3); + + FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); + + ValidityMatchResult result = { 0 }; + gboolean ok = validity_parse_match_result (buf, pos, &result); + + g_assert_true (ok); + g_assert_true (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0xDEADBEEF); + g_assert_cmpuint (result.subtype, ==, 7); + g_assert_nonnull (result.hash); + g_assert_cmpuint (result.hash_len, ==, 3); + g_assert_cmpmem (result.hash, result.hash_len, hash, 3); + + validity_match_result_clear (&result); +} + +/* ================================================================ + * R1c: parse_match_result with empty dict (no match) + * + * Regression: A no-match scenario should return ok=TRUE but matched=FALSE. + * ================================================================ */ +static void +test_parse_match_result_empty (void) +{ + /* total_len = 0, no TLV entries */ + guint8 buf[2] = { 0x00, 0x00 }; + + ValidityMatchResult result = { 0 }; + gboolean ok = validity_parse_match_result (buf, sizeof (buf), &result); + + g_assert_true (ok); + g_assert_false (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0); + g_assert_cmpuint (result.subtype, ==, 0); + g_assert_null (result.hash); +} + +/* ================================================================ + * R1d: parse_match_result with truncated data + * + * Ensure graceful handling of malformed/truncated payloads. + * ================================================================ */ +static void +test_parse_match_result_truncated (void) +{ + /* Only 1 byte — too short for total_len */ + guint8 buf1[1] = { 0x05 }; + ValidityMatchResult result = { 0 }; + g_assert_false (validity_parse_match_result (buf1, 1, &result)); + + /* total_len says 20 but only 6 bytes follow (partial TLV entry) */ + guint8 buf2[8]; + FP_WRITE_UINT16_LE (&buf2[0], 20); + FP_WRITE_UINT16_LE (&buf2[2], 1); /* tag = 1 */ + FP_WRITE_UINT16_LE (&buf2[4], 10); /* len = 10, but only 2 bytes remain */ + buf2[6] = 0xFF; + buf2[7] = 0xFF; + + memset (&result, 0, sizeof (result)); + gboolean ok = validity_parse_match_result (buf2, sizeof (buf2), &result); + /* Should return TRUE (parsing succeeded) but matched=FALSE (incomplete entry) */ + g_assert_true (ok); + g_assert_false (result.matched); +} + +/* ================================================================ + * R1e: parse_match_result ignores unknown tags + * + * Unknown tags should be skipped without error. + * ================================================================ */ +static void +test_parse_match_result_unknown_tags (void) +{ + guint8 buf[256]; + gsize pos = 2; + + /* Unknown tag 99 with 2 bytes of data */ + guint8 unk[] = { 0x42, 0x43 }; + pos += build_tlv_entry (&buf[pos], 99, unk, 2); + + /* Tag 1: user_dbid = 0x0042 */ + guint8 dbid[4]; + FP_WRITE_UINT32_LE (dbid, 0x0042); + pos += build_tlv_entry (&buf[pos], 1, dbid, 4); + + FP_WRITE_UINT16_LE (&buf[0], (guint16) (pos - 2)); + + ValidityMatchResult result = { 0 }; + gboolean ok = validity_parse_match_result (buf, pos, &result); + + g_assert_true (ok); + g_assert_true (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0x0042); + + validity_match_result_clear (&result); +} + +/* ================================================================ + * R2: validity_db_build_identity rejects NULL + * + * Regression: Issue #2 — NULL user_id was passed to build_identity + * which would then be passed to g_variant_new_string(NULL) → crash. + * The guard should return NULL. + * ================================================================ */ +static void +test_build_identity_null (void) +{ + gsize len = 999; + + /* The function uses g_return_val_if_fail which emits g_critical. + * With G_DEBUG=fatal-warnings the critical would be fatal, + * so we must expect the message. */ + g_test_expect_message ("libfprint-validity", + G_LOG_LEVEL_CRITICAL, + "*assertion*uuid_str*failed*"); + + guint8 *id = validity_db_build_identity (NULL, &len); + g_test_assert_expected_messages (); + + g_assert_null (id); +} + +/* ================================================================ + * R2b: validity_db_build_identity with valid UUID + * + * Regression: Ensures UUID → identity bytes works correctly + * (complementary to the NULL test above). + * ================================================================ */ +static void +test_build_identity_valid_uuid (void) +{ + const gchar *uuid = "12345678-1234-5678-1234-567812345678"; + gsize len; + g_autofree guint8 *id = validity_db_build_identity (uuid, &len); + + g_assert_nonnull (id); + g_assert_cmpuint (len, >=, VALIDITY_IDENTITY_MIN_SIZE); + + /* Type should be SID (3) */ + g_assert_cmpuint (FP_READ_UINT32_LE (&id[0]), ==, VALIDITY_IDENTITY_TYPE_SID); + + /* Length field should be UUID string length */ + g_assert_cmpuint (FP_READ_UINT32_LE (&id[4]), ==, strlen (uuid)); + + /* UUID payload should be present */ + g_assert_cmpmem (&id[8], strlen (uuid), uuid, strlen (uuid)); +} + +/* ================================================================ + * R3: Gallery matching by subtype + * + * Regression: Issue #3 — identify always returned first gallery print + * regardless of actual subtype. Now it should match by finger subtype. + * ================================================================ */ +static void +test_gallery_match_by_subtype (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_DEVICE_FAKE, NULL); + g_autoptr(GPtrArray) gallery = g_ptr_array_new_with_free_func (g_object_unref); + + /* Create 3 prints with fingers: LEFT_THUMB(1), LEFT_INDEX(2), RIGHT_MIDDLE(8) */ + FpPrint *p1 = fp_print_new (device); + fp_print_set_finger (p1, FP_FINGER_LEFT_THUMB); + g_ptr_array_add (gallery, g_object_ref_sink (p1)); + + FpPrint *p2 = fp_print_new (device); + fp_print_set_finger (p2, FP_FINGER_LEFT_INDEX); + g_ptr_array_add (gallery, g_object_ref_sink (p2)); + + FpPrint *p3 = fp_print_new (device); + fp_print_set_finger (p3, FP_FINGER_RIGHT_MIDDLE); + g_ptr_array_add (gallery, g_object_ref_sink (p3)); + + /* Subtype 2 = LEFT_INDEX → should match p2, not p1 */ + guint16 subtype_left_index = validity_finger_to_subtype (FP_FINGER_LEFT_INDEX); + FpPrint *match = validity_find_gallery_match (gallery, subtype_left_index); + g_assert_true (match == p2); + + /* Subtype 8 = RIGHT_MIDDLE → should match p3 */ + guint16 subtype_right_middle = validity_finger_to_subtype (FP_FINGER_RIGHT_MIDDLE); + match = validity_find_gallery_match (gallery, subtype_right_middle); + g_assert_true (match == p3); + + /* Subtype 1 = LEFT_THUMB → should match p1 */ + guint16 subtype_left_thumb = validity_finger_to_subtype (FP_FINGER_LEFT_THUMB); + match = validity_find_gallery_match (gallery, subtype_left_thumb); + g_assert_true (match == p1); +} + +/* ================================================================ + * R3b: Gallery match falls back to first when subtype doesn't match + * + * The sensor confirmed a match but the subtype can't be correlated + * to any gallery entry — should fall back to first. + * ================================================================ */ +static void +test_gallery_match_fallback (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_DEVICE_FAKE, NULL); + g_autoptr(GPtrArray) gallery = g_ptr_array_new_with_free_func (g_object_unref); + + FpPrint *p1 = fp_print_new (device); + fp_print_set_finger (p1, FP_FINGER_LEFT_THUMB); + g_ptr_array_add (gallery, g_object_ref_sink (p1)); + + /* Subtype 9 = RIGHT_RING, not in gallery → should fall back to p1 */ + guint16 subtype_right_ring = validity_finger_to_subtype (FP_FINGER_RIGHT_RING); + FpPrint *match = validity_find_gallery_match (gallery, subtype_right_ring); + g_assert_true (match == p1); +} + +/* ================================================================ + * R3c: Gallery match with NULL/empty gallery + * + * Should return NULL when gallery is empty or NULL. + * ================================================================ */ +static void +test_gallery_match_empty (void) +{ + g_autoptr(GPtrArray) empty = g_ptr_array_new_with_free_func (g_object_unref); + + g_assert_null (validity_find_gallery_match (NULL, 1)); + g_assert_null (validity_find_gallery_match (empty, 1)); +} + +/* ================================================================ + * R4: enroll_user_dbid field exists separately from delete_storage_dbid + * + * Regression: Issue #4 — delete_storage_dbid was abused for enrollment. + * This compile-time test verifies both fields exist independently. + * ================================================================ */ +static void +test_struct_separate_fields (void) +{ + /* Verify both fields exist and are at different offsets */ + g_assert_cmpuint ( + G_STRUCT_OFFSET (FpiDeviceValidity, enroll_user_dbid), !=, + G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); + + /* Also verify delete_finger_subtype and delete_finger_dbid exist + * (needed for the functional delete SSM) */ + g_assert_cmpuint ( + G_STRUCT_OFFSET (FpiDeviceValidity, delete_finger_subtype), !=, + G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); + g_assert_cmpuint ( + G_STRUCT_OFFSET (FpiDeviceValidity, delete_finger_dbid), !=, + G_STRUCT_OFFSET (FpiDeviceValidity, delete_storage_dbid)); +} + +/* ================================================================ + * R5: del_record command format + * + * Regression: Issue #5 — delete SSM never sent del_record cmd. + * Verify cmd 0x48 produces correct format: 0x48 | dbid(2LE). + * (This already exists in test-validity-db.c but we double-check + * the format critical for delete functionality.) + * ================================================================ */ +static void +test_del_record_format (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_del_record (0x4321, &len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 3); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DEL_RECORD); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x4321); +} + +/* ================================================================ + * R6: match_finger command is exactly 13 bytes (single allocation) + * + * Regression: Issue #6 — build_cmd_match_finger allocated 12 bytes, + * freed, then re-allocated 13 bytes. Now single allocation. + * ================================================================ */ +static void +test_match_finger_size (void) +{ + gsize len; + g_autofree guint8 *cmd = validity_db_build_cmd_match_finger (&len); + + g_assert_nonnull (cmd); + g_assert_cmpuint (len, ==, 13); + g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_FINGER); + g_assert_cmpuint (cmd[1], ==, 0x02); + g_assert_cmpuint (cmd[2], ==, 0xFF); + + /* Verify all 5 uint16_le fields */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); /* stg_id */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0); /* usr_id */ + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, 1); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[9]), ==, 0); + g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[11]), ==, 0); +} + +/* ================================================================ + * R7: Clear storage SSM states exist + * + * Regression: Issue #7 — clear_storage was a stub returning NOT_SUPPORTED. + * Verify the CLEAR_* enum states exist (compile-time regression test). + * ================================================================ */ +static void +test_clear_storage_states_exist (void) +{ + /* Verify clear SSM states exist and are ordered correctly */ + g_assert_cmpint (CLEAR_GET_STORAGE, ==, 0); + g_assert_cmpint (CLEAR_GET_STORAGE_RECV, ==, 1); + g_assert_cmpint (CLEAR_DEL_USER, ==, 2); + g_assert_cmpint (CLEAR_DEL_USER_RECV, ==, 3); + g_assert_cmpint (CLEAR_DONE, ==, 4); + g_assert_cmpint (CLEAR_NUM_STATES, ==, 5); +} + +/* ================================================================ + * R7b: Delete SSM states are complete + * + * Verify the delete SSM has all required states including + * DEL_RECORD and DEL_RECORD_RECV (which were previously dead code). + * ================================================================ */ +static void +test_delete_states_exist (void) +{ + g_assert_cmpint (DELETE_GET_STORAGE, ==, 0); + g_assert_cmpint (DELETE_GET_STORAGE_RECV, ==, 1); + g_assert_cmpint (DELETE_LOOKUP_USER, ==, 2); + g_assert_cmpint (DELETE_LOOKUP_USER_RECV, ==, 3); + g_assert_cmpint (DELETE_DEL_RECORD, ==, 4); + g_assert_cmpint (DELETE_DEL_RECORD_RECV, ==, 5); + g_assert_cmpint (DELETE_DONE, ==, 6); + g_assert_cmpint (DELETE_NUM_STATES, ==, 7); +} + +/* ================================================================ + * R1f: match_result_clear frees hash + * + * Ensure the clear function properly frees the hash allocation. + * ================================================================ */ +static void +test_match_result_clear (void) +{ + ValidityMatchResult result = { 0 }; + result.matched = TRUE; + result.user_dbid = 42; + result.subtype = 5; + result.hash = g_memdup2 ((guint8[]){0x01, 0x02}, 2); + result.hash_len = 2; + + validity_match_result_clear (&result); + + g_assert_false (result.matched); + g_assert_cmpuint (result.user_dbid, ==, 0); + g_assert_cmpuint (result.subtype, ==, 0); + g_assert_null (result.hash); + g_assert_cmpuint (result.hash_len, ==, 0); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* R1: parse_match_result regression tests (Issue #1: dead while loop) */ + g_test_add_func ("/validity/verify/parse_match_result_valid", + test_parse_match_result_valid); + g_test_add_func ("/validity/verify/parse_match_result_multi_tags", + test_parse_match_result_multi_tags); + g_test_add_func ("/validity/verify/parse_match_result_empty", + test_parse_match_result_empty); + g_test_add_func ("/validity/verify/parse_match_result_truncated", + test_parse_match_result_truncated); + g_test_add_func ("/validity/verify/parse_match_result_unknown_tags", + test_parse_match_result_unknown_tags); + g_test_add_func ("/validity/verify/match_result_clear", + test_match_result_clear); + + /* R2: identity builder NULL regression (Issue #2: NULL crash) */ + g_test_add_func ("/validity/verify/build_identity_null", + test_build_identity_null); + g_test_add_func ("/validity/verify/build_identity_valid_uuid", + test_build_identity_valid_uuid); + + /* R3: gallery matching by subtype (Issue #3: always returned first) */ + g_test_add_func ("/validity/verify/gallery_match_by_subtype", + test_gallery_match_by_subtype); + g_test_add_func ("/validity/verify/gallery_match_fallback", + test_gallery_match_fallback); + g_test_add_func ("/validity/verify/gallery_match_empty", + test_gallery_match_empty); + + /* R4: struct field separation (Issue #4: field abuse) */ + g_test_add_func ("/validity/verify/struct_separate_fields", + test_struct_separate_fields); + + /* R5: del_record command format (Issue #5: delete SSM non-functional) */ + g_test_add_func ("/validity/verify/del_record_format", + test_del_record_format); + + /* R6: match_finger single allocation (Issue #6: double alloc) */ + g_test_add_func ("/validity/verify/match_finger_size", + test_match_finger_size); + + /* R7: clear/delete storage SSM states (Issue #7: stub) */ + g_test_add_func ("/validity/verify/clear_storage_states", + test_clear_storage_states_exist); + g_test_add_func ("/validity/verify/delete_states", + test_delete_states_exist); + + return g_test_run (); +}