/* * test_ac.c — Tests for the Aho-Corasick multi-pattern matcher. * * Exercises cbm_ac_build(), cbm_ac_scan_bitmask(), cbm_ac_scan_batch() * against the existing ac.c implementation. */ #include "test_framework.h" #include #include /* Declarations from ac.c (no public header yet — these are the C API) */ typedef struct CBMAutomaton CBMAutomaton; CBMAutomaton *cbm_ac_build(const char **patterns, const int *lengths, int count, const uint8_t *alpha_map, int alpha_size); void cbm_ac_free(CBMAutomaton *ac); uint64_t cbm_ac_scan_bitmask(const CBMAutomaton *ac, const char *text, int text_len); int cbm_ac_num_states(const CBMAutomaton *ac); int cbm_ac_num_patterns(const CBMAutomaton *ac); int cbm_ac_table_bytes(const CBMAutomaton *ac); typedef struct { int name_index; int pattern_id; } CBMMatchResult; int cbm_ac_scan_batch(const CBMAutomaton *ac, const char *names_buf, const int *name_offsets, const int *name_lengths, int num_names, CBMMatchResult *out_matches, int max_matches); /* ── Tests ─────────────────────────────────────────────────────── */ TEST(ac_build_single) { const char *patterns[] = {"hello"}; int lengths[] = {5}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 1, NULL, 256); ASSERT_NOT_NULL(ac); ASSERT_EQ(cbm_ac_num_patterns(ac), 1); ASSERT_GT(cbm_ac_num_states(ac), 0); cbm_ac_free(ac); PASS(); } TEST(ac_build_multiple) { const char *patterns[] = {"he", "she", "his", "hers"}; int lengths[] = {2, 3, 3, 4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 4, NULL, 256); ASSERT_NOT_NULL(ac); ASSERT_EQ(cbm_ac_num_patterns(ac), 4); cbm_ac_free(ac); PASS(); } TEST(ac_scan_single_match) { const char *patterns[] = {"hello"}; int lengths[] = {5}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 1, NULL, 256); uint64_t result = cbm_ac_scan_bitmask(ac, "say hello world", 15); ASSERT_EQ(result, 1ULL); /* pattern 0 matched */ cbm_ac_free(ac); PASS(); } TEST(ac_scan_no_match) { const char *patterns[] = {"xyz"}; int lengths[] = {3}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 1, NULL, 256); uint64_t result = cbm_ac_scan_bitmask(ac, "hello world", 11); ASSERT_EQ(result, 0); cbm_ac_free(ac); PASS(); } TEST(ac_scan_multiple_matches) { const char *patterns[] = {"he", "she", "his", "hers"}; int lengths[] = {2, 3, 3, 4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 4, NULL, 256); /* "ushers" should match "she" (1), "he" (0), "hers" (3) */ uint64_t result = cbm_ac_scan_bitmask(ac, "ushers", 6); ASSERT(result & (1ULL << 0)); /* "he" */ ASSERT(result & (1ULL << 1)); /* "she" */ ASSERT(result & (1ULL << 3)); /* "hers" */ cbm_ac_free(ac); PASS(); } TEST(ac_scan_overlapping) { /* Overlapping patterns at same position */ const char *patterns[] = {"ab", "abc", "abcd"}; int lengths[] = {2, 3, 4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 3, NULL, 256); uint64_t result = cbm_ac_scan_bitmask(ac, "xabcdy", 6); ASSERT(result & (1ULL << 0)); /* "ab" */ ASSERT(result & (1ULL << 1)); /* "abc" */ ASSERT(result & (1ULL << 2)); /* "abcd" */ cbm_ac_free(ac); PASS(); } TEST(ac_scan_empty_text) { const char *patterns[] = {"test"}; int lengths[] = {4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 1, NULL, 256); uint64_t result = cbm_ac_scan_bitmask(ac, "", 0); ASSERT_EQ(result, 0); cbm_ac_free(ac); PASS(); } TEST(ac_custom_alphabet) { /* Use a compact alphabet mapping only lowercase letters */ uint8_t alpha_map[256] = {0}; for (int c = 'a'; c <= 'z'; c++) alpha_map[c] = (uint8_t)(c - 'a' + 1); int alpha_size = 27; /* 0=other, 1-26=a-z */ const char *patterns[] = {"hello"}; int lengths[] = {5}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 1, alpha_map, alpha_size); ASSERT_NOT_NULL(ac); uint64_t result = cbm_ac_scan_bitmask(ac, "say hello world", 15); ASSERT_EQ(result, 1ULL); /* Table should be much smaller with compact alphabet */ int full_bytes = cbm_ac_num_states(ac) * 256 * 4; int compact_bytes = cbm_ac_table_bytes(ac); ASSERT_LT(compact_bytes, full_bytes); cbm_ac_free(ac); PASS(); } TEST(ac_batch_scan) { const char *patterns[] = {"foo", "bar", "baz"}; int lengths[] = {3, 3, 3}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 3, NULL, 256); /* Batch: scan 4 names */ const char names_buf[] = "hello\0foobar\0nothing\0bazinga"; int offsets[] = {0, 6, 13, 21}; int lens[] = {5, 6, 7, 7}; CBMMatchResult matches[16]; int n = cbm_ac_scan_batch(ac, names_buf, offsets, lens, 4, matches, 16); /* "foobar" matches "foo" and "bar", "bazinga" matches "baz" → ≥3 matches */ ASSERT_GTE(n, 3); cbm_ac_free(ac); PASS(); } TEST(ac_table_bytes) { const char *patterns[] = {"test"}; int lengths[] = {4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 1, NULL, 256); int bytes = cbm_ac_table_bytes(ac); ASSERT_GT(bytes, 0); /* states * 256 * sizeof(int) */ ASSERT_EQ(bytes, cbm_ac_num_states(ac) * 256 * 4); cbm_ac_free(ac); PASS(); } TEST(ac_null_input) { CBMAutomaton *ac = cbm_ac_build(NULL, NULL, 0, NULL, 256); ASSERT_NULL(ac); cbm_ac_free(NULL); /* should not crash */ PASS(); } /* --- Ported from ac_test.go: TestACScanString --- */ TEST(ac_scan_string) { const char *patterns[] = {"foo", "bar"}; int lengths[] = {3, 3}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 2, NULL, 256); ASSERT_NOT_NULL(ac); uint64_t mask = cbm_ac_scan_bitmask(ac, "foobar", 6); ASSERT_EQ(mask, 3ULL); /* both pattern 0 and 1 */ cbm_ac_free(ac); PASS(); } /* --- Ported from ac_test.go: TestACFree_DoubleCall --- */ TEST(ac_free_double_call) { const char *patterns[] = {"test"}; int lengths[] = {4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 1, NULL, 256); ASSERT_NOT_NULL(ac); cbm_ac_free(ac); /* ac is now freed — calling free again should not crash. Note: in C the pointer is dangling, but cbm_ac_free checks for NULL internally. We already test free(NULL) in ac_null_input. This test verifies the Go test expectation. */ PASS(); } /* --- Ported from ac_test.go: TestACScanLZ4Bitmask --- */ extern int cbm_lz4_compress_hc(const char *src, int srcLen, char *dst, int dstCap); extern int cbm_lz4_bound(int inputSize); /* Structs defined in ac.c */ typedef struct { const char *data; int compressed_len; int original_len; } CBMLz4Entry; typedef struct { int file_index; uint64_t bitmask; } CBMLz4Match; extern uint64_t cbm_ac_scan_lz4_bitmask(const CBMAutomaton *ac, const char *compressed, int compressed_len, int original_len); extern int cbm_ac_scan_lz4_batch(const CBMAutomaton *ac, const CBMLz4Entry *entries, int num_entries, CBMLz4Match *out_matches, int max_matches); TEST(ac_scan_lz4_bitmask) { const char *patterns[] = {"http.Get", "fetch("}; int lengths[] = {8, 6}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 2, NULL, 256); ASSERT_NOT_NULL(ac); const char *source = "package main\nimport \"net/http\"\n" "func doStuff() {\n" " resp, err := http.Get(\"https://example.com\")\n" " _ = resp\n _ = err\n}\n"; int src_len = (int)strlen(source); int bound = cbm_lz4_bound(src_len); char *compressed = malloc(bound); ASSERT_NOT_NULL(compressed); int comp_len = cbm_lz4_compress_hc(source, src_len, compressed, bound); ASSERT_GT(comp_len, 0); uint64_t mask = cbm_ac_scan_lz4_bitmask(ac, compressed, comp_len, src_len); ASSERT_EQ(mask, 1ULL); /* http.Get matched */ /* No match */ const char *no_http = "package main\nfunc main() { println(42) }\n"; int nh_len = (int)strlen(no_http); int bound2 = cbm_lz4_bound(nh_len); char *comp2 = malloc(bound2); int comp2_len = cbm_lz4_compress_hc(no_http, nh_len, comp2, bound2); mask = cbm_ac_scan_lz4_bitmask(ac, comp2, comp2_len, nh_len); ASSERT_EQ(mask, 0ULL); free(compressed); free(comp2); cbm_ac_free(ac); PASS(); } /* --- Ported from ac_test.go: TestACScanLZ4Batch --- */ TEST(ac_scan_lz4_batch) { const char *patterns[] = {"http.Get", "fetch(", "Route::get"}; int lengths[] = {8, 6, 10}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 3, NULL, 256); ASSERT_NOT_NULL(ac); /* Three files: one with http.Get, one empty, one with Route::get */ const char *files[] = {"resp := http.Get(\"https://example.com\")", "func main() { println(42) }", "Route::get(\"/users\", \"UserController@index\")"}; CBMLz4Entry entries[3]; char *comp_bufs[3]; for (int i = 0; i < 3; i++) { int slen = (int)strlen(files[i]); int bound = cbm_lz4_bound(slen); comp_bufs[i] = malloc(bound); int clen = cbm_lz4_compress_hc(files[i], slen, comp_bufs[i], bound); entries[i].data = comp_bufs[i]; entries[i].compressed_len = clen; entries[i].original_len = slen; } CBMLz4Match matches[8]; int n = cbm_ac_scan_lz4_batch(ac, entries, 3, matches, 8); ASSERT_EQ(n, 2); /* files 0 and 2 */ ASSERT_EQ(matches[0].file_index, 0); ASSERT(matches[0].bitmask & 1ULL); /* http.Get */ ASSERT_EQ(matches[1].file_index, 2); ASSERT(matches[1].bitmask & 4ULL); /* Route::get */ for (int i = 0; i < 3; i++) free(comp_bufs[i]); cbm_ac_free(ac); PASS(); } /* --- Ported from ac_test.go: TestACScanLZ4Batch_Empty --- */ TEST(ac_scan_lz4_batch_empty) { const char *patterns[] = {"test"}; int lengths[] = {4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 1, NULL, 256); ASSERT_NOT_NULL(ac); CBMLz4Match matches[1]; int n = cbm_ac_scan_lz4_batch(ac, NULL, 0, matches, 1); ASSERT_EQ(n, 0); cbm_ac_free(ac); PASS(); } /* --- Ported from ac_test.go: TestAC_LargePatternSet --- */ TEST(ac_large_pattern_set) { /* All httplink keywords — real-world pattern count */ const char *patterns[] = { "requests.get", "requests.post", "requests.put", "requests.delete", "requests.patch", "httpx.", "aiohttp.", "urllib.request", "http.Get", "http.Post", "http.NewRequest", "client.Do(", "fetch(", "axios.", ".ajax(", "HttpClient", "RestTemplate", "WebClient", "OkHttpClient", "HttpURLConnection", "openConnection(", "reqwest::", "hyper::", "surf::", "ureq::", "curl_exec", "curl_init", "Guzzle", "Http::get", "Http::post", "sttp.", "http4s", "wsClient", "curl_easy", "cpr::Get", "cpr::Post", "httplib::", "socket.http", "http.request", "RestClient", "HttpWebRequest", "ktor.client", "send_request", "http_client", "CreateTask", "create_task", "topic.Publish", "publisher.publish", "topic.publish", "sqs.send_message", "sns.publish", "basic_publish", "producer.send", "producer.Send", }; int count = sizeof(patterns) / sizeof(patterns[0]); int *lengths = malloc(count * sizeof(int)); for (int i = 0; i < count; i++) lengths[i] = (int)strlen(patterns[i]); CBMAutomaton *ac = cbm_ac_build(patterns, lengths, count, NULL, 256); ASSERT_NOT_NULL(ac); ASSERT_GT(cbm_ac_num_states(ac), 0); /* Scan Go source with http.Get and CreateTask */ const char *source = "package main\nimport \"net/http\"\n" "func callAPI() {\n" " resp, _ := http.Get(\"https://api.example.com/data\")\n" " defer resp.Body.Close()\n}\n" "func createTask() {\n client.CreateTask(ctx, req)\n}\n"; uint64_t mask = cbm_ac_scan_bitmask(ac, source, (int)strlen(source)); /* Find indices for http.Get and CreateTask */ int http_get_idx = -1, create_task_idx = -1; for (int i = 0; i < count; i++) { if (strcmp(patterns[i], "http.Get") == 0) http_get_idx = i; if (strcmp(patterns[i], "CreateTask") == 0) create_task_idx = i; } ASSERT(mask & (1ULL << http_get_idx)); ASSERT(mask & (1ULL << create_task_idx)); /* Non-matching source */ mask = cbm_ac_scan_bitmask(ac, "func main() { fmt.Println(42) }", 31); ASSERT_EQ(mask, 0ULL); free(lengths); cbm_ac_free(ac); PASS(); } /* --- Ported from ac_test.go: TestACBuild_MultiplePatterns --- */ TEST(ac_http_patterns) { const char *patterns[] = {"http.Get", "fetch(", "requests.post", "axios."}; int lengths[] = {8, 6, 13, 6}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 4, NULL, 256); ASSERT_NOT_NULL(ac); ASSERT_EQ(cbm_ac_num_patterns(ac), 4); struct { const char *input; uint64_t expected; } tests[] = { {"resp := http.Get(url)", 1ULL << 0}, {"const r = fetch(url)", 1ULL << 1}, {"requests.post(url, data)", 1ULL << 2}, {"axios.get('/api')", 1ULL << 3}, {"http.Get then fetch(", (1ULL << 0) | (1ULL << 1)}, {"no http calls here", 0}, {"", 0}, }; for (int i = 0; i < 7; i++) { uint64_t mask = cbm_ac_scan_bitmask(ac, tests[i].input, (int)strlen(tests[i].input)); ASSERT_EQ(mask, tests[i].expected); } cbm_ac_free(ac); PASS(); } /* --- Ported from ac_test.go: TestACCompactAlphabet (extended) --- */ TEST(ac_compact_alphabet_extended) { /* Build a-z=1..26, 0-9=27..36, _=37 */ uint8_t alpha_map[256] = {0}; uint8_t idx = 1; for (int c = 'a'; c <= 'z'; c++) alpha_map[c] = idx++; for (int c = '0'; c <= '9'; c++) alpha_map[c] = idx++; alpha_map['_'] = idx; int alpha_size = (int)idx + 1; /* 38 */ const char *patterns[] = {"database_url", "api_key", "port"}; int lengths[] = {12, 7, 4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 3, alpha_map, alpha_size); ASSERT_NOT_NULL(ac); /* Exact match */ uint64_t mask = cbm_ac_scan_bitmask(ac, "database_url", 12); ASSERT(mask & 1ULL); /* Substring match */ mask = cbm_ac_scan_bitmask(ac, "my_database_url_setting", 23); ASSERT(mask & 1ULL); /* Verify table size = states * alpha_size * sizeof(int) */ int table_bytes = cbm_ac_table_bytes(ac); ASSERT_EQ(table_bytes, cbm_ac_num_states(ac) * alpha_size * 4); cbm_ac_free(ac); PASS(); } /* --- Ported from ac_test.go: TestACScanBatch (detailed) --- */ TEST(ac_batch_scan_detailed) { const char *patterns[] = {"database", "port", "host"}; int lengths[] = {8, 4, 4}; CBMAutomaton *ac = cbm_ac_build(patterns, lengths, 3, NULL, 256); ASSERT_NOT_NULL(ac); /* 5 names: database_url, server_port, api_key, database_host, max_retries */ const char names_buf[] = "database_url\0server_port\0api_key\0database_host\0max_retries"; int offsets[] = {0, 13, 25, 33, 47}; int lens[] = {12, 11, 7, 13, 11}; CBMMatchResult matches[16]; int n = cbm_ac_scan_batch(ac, names_buf, offsets, lens, 5, matches, 16); /* Expect: * name 0 (database_url) → pattern 0 (database) * name 1 (server_port) → pattern 1 (port) * name 3 (database_host) → pattern 0 (database) AND pattern 2 (host) * name 2 (api_key) → no match * name 4 (max_retries) → no match * Total: 4 matches */ ASSERT_EQ(n, 4); /* Build lookup: name_index → list of pattern_ids */ int found_name0 = 0, found_name1 = 0, found_name3_db = 0, found_name3_host = 0; for (int i = 0; i < n; i++) { if (matches[i].name_index == 0 && matches[i].pattern_id == 0) found_name0 = 1; if (matches[i].name_index == 1 && matches[i].pattern_id == 1) found_name1 = 1; if (matches[i].name_index == 3 && matches[i].pattern_id == 0) found_name3_db = 1; if (matches[i].name_index == 3 && matches[i].pattern_id == 2) found_name3_host = 1; } ASSERT_TRUE(found_name0); ASSERT_TRUE(found_name1); ASSERT_TRUE(found_name3_db); ASSERT_TRUE(found_name3_host); cbm_ac_free(ac); PASS(); } SUITE(ac) { RUN_TEST(ac_build_single); RUN_TEST(ac_build_multiple); RUN_TEST(ac_scan_single_match); RUN_TEST(ac_scan_no_match); RUN_TEST(ac_scan_multiple_matches); RUN_TEST(ac_scan_overlapping); RUN_TEST(ac_scan_empty_text); RUN_TEST(ac_custom_alphabet); RUN_TEST(ac_batch_scan); RUN_TEST(ac_table_bytes); RUN_TEST(ac_null_input); RUN_TEST(ac_scan_string); RUN_TEST(ac_free_double_call); RUN_TEST(ac_scan_lz4_bitmask); RUN_TEST(ac_scan_lz4_batch); RUN_TEST(ac_scan_lz4_batch_empty); RUN_TEST(ac_large_pattern_set); RUN_TEST(ac_http_patterns); RUN_TEST(ac_compact_alphabet_extended); RUN_TEST(ac_batch_scan_detailed); }