#include "vdm/rules/match.hpp" #include #include "vtest.hpp" using namespace vdm::rules; namespace { Rule make_rule(std::string id, std::int64_t priority, RuleMatch m, RuleAction a, bool enabled = true) { Rule r; r.rule_id = std::move(id); r.priority = priority; r.enabled = enabled; r.match = std::move(m); r.action = std::move(a); return r; } // -Wmissing-field-initializers (part of -Wextra) flags a designated-initializer list that // skips a member, even one this repo's designated-init style would normally leave // implicit -- these small builders keep the tests below readable without tripping it. RuleAction action_with_category(std::string id) { RuleAction a; a.category_id = std::move(id); return a; } MatchInput input_with_extension(std::string ext) { MatchInput in; in.extension = std::move(ext); return in; } MatchInput input_with_host(std::string host) { MatchInput in; in.host = std::move(host); return in; } MatchInput input_with_size(std::optional size) { MatchInput in; in.size_bytes = size; return in; } MatchInput input_with_extension_and_host(std::string ext, std::string host) { MatchInput in; in.extension = std::move(ext); in.host = std::move(host); return in; } } // namespace // --- glob_match --------------------------------------------------------------------- VT_TEST(glob_exact_match) { VT_CHECK(glob_match("example.com", "example.com")); VT_CHECK(!glob_match("example.com", "example.org")); } VT_TEST(glob_star_suffix) { VT_CHECK(glob_match("*.example.com", "cdn.example.com")); VT_CHECK(glob_match("*.example.com", "a.b.example.com")); VT_CHECK(!glob_match("*.example.com", "example.com")); // no room for the literal '.' } VT_TEST(glob_star_matches_empty) { VT_CHECK(glob_match("file*.zip", "file.zip")); VT_CHECK(glob_match("file*.zip", "file123.zip")); } VT_TEST(glob_question_mark) { VT_CHECK(glob_match("file?.txt", "file1.txt")); VT_CHECK(!glob_match("file?.txt", "file.txt")); VT_CHECK(!glob_match("file?.txt", "file12.txt")); } VT_TEST(glob_case_insensitive) { VT_CHECK(glob_match("*.EXAMPLE.com", "cdn.example.COM")); } VT_TEST(glob_multiple_stars) { VT_CHECK(glob_match("*foo*bar*", "xxfooyybarzz")); VT_CHECK(!glob_match("*foo*bar*", "xxbarzzfooyy")); // order matters } VT_TEST(glob_pathological_stars_terminate) { // A pattern of nothing but '*' against a long text must not blow up (bounded work). std::string pattern(50, '*'); std::string text(10000, 'x'); VT_CHECK(glob_match(pattern, text)); } // --- match_rules --------------------------------------------------------------------- VT_TEST(match_empty_table_yields_nullopt) { VT_CHECK(!match_rules({}, input_with_extension("zip")).has_value()); } VT_TEST(match_no_clause_matches_everything) { auto rules = {make_rule("r1", 0, RuleMatch{}, action_with_category("default"))}; auto r = match_rules(rules, input_with_extension("anything")); VT_REQUIRE(r.has_value()); VT_CHECK_EQ(*r->category_id, std::string("default")); } VT_TEST(match_by_extension_case_insensitive) { RuleMatch m; m.extensions = std::vector{"zip", "rar"}; auto rules = {make_rule("r1", 0, m, action_with_category("archives"))}; VT_CHECK(match_rules(rules, input_with_extension("ZIP")).has_value()); VT_CHECK(!match_rules(rules, input_with_extension("txt")).has_value()); } VT_TEST(match_priority_order_lower_wins) { std::vector rules = { make_rule("hi", 10, RuleMatch{}, action_with_category("first")), make_rule("lo", 0, RuleMatch{}, action_with_category("second")), }; auto r = match_rules(rules, MatchInput{}); VT_REQUIRE(r.has_value()); VT_CHECK_EQ(*r->category_id, std::string("second")); // priority 0 runs first } VT_TEST(match_ties_keep_table_order) { std::vector rules = { make_rule("a", 5, RuleMatch{}, action_with_category("first")), make_rule("b", 5, RuleMatch{}, action_with_category("second")), }; auto r = match_rules(rules, MatchInput{}); VT_REQUIRE(r.has_value()); VT_CHECK_EQ(*r->category_id, std::string("first")); } VT_TEST(match_skips_disabled_rules) { std::vector rules = { make_rule("a", 0, RuleMatch{}, action_with_category("disabled"), /*enabled=*/false), make_rule("b", 1, RuleMatch{}, action_with_category("enabled")), }; auto r = match_rules(rules, MatchInput{}); VT_REQUIRE(r.has_value()); VT_CHECK_EQ(*r->category_id, std::string("enabled")); } VT_TEST(match_host_pattern) { RuleMatch m; m.host_pattern = "*.cdn.example.com"; auto rules = {make_rule("r1", 0, m, action_with_category("cdn"))}; VT_CHECK(match_rules(rules, input_with_host("a.cdn.example.com")).has_value()); VT_CHECK(!match_rules(rules, input_with_host("example.com")).has_value()); } VT_TEST(match_size_bounds) { RuleMatch m; m.min_size_bytes = 1000; m.max_size_bytes = 2000; auto rules = {make_rule("r1", 0, m, action_with_category("midsize"))}; VT_CHECK(match_rules(rules, input_with_size(1500)).has_value()); VT_CHECK(!match_rules(rules, input_with_size(500)).has_value()); VT_CHECK(!match_rules(rules, input_with_size(5000)).has_value()); } VT_TEST(match_size_clause_with_unknown_size_does_not_match) { RuleMatch m; m.min_size_bytes = 1000; auto rules = {make_rule("r1", 0, m, action_with_category("big"))}; // size_bytes left absent (pre-probe) -- a size clause must not match speculatively. VT_CHECK(!match_rules(rules, MatchInput{}).has_value()); } VT_TEST(match_all_clauses_must_hold) { RuleMatch m; m.extensions = std::vector{"iso"}; m.host_pattern = "*.trusted.example"; auto rules = {make_rule("r1", 0, m, action_with_category("isos"))}; VT_CHECK(match_rules(rules, input_with_extension_and_host("iso", "mirror.trusted.example")) .has_value()); // Extension matches but host doesn't -- must not match. VT_CHECK(!match_rules(rules, input_with_extension_and_host("iso", "evil.example")) .has_value()); } VT_TEST(match_falls_through_to_default_when_nothing_matches) { RuleMatch m; m.extensions = std::vector{"exe"}; auto rules = {make_rule("r1", 0, m, action_with_category("installers"))}; VT_CHECK(!match_rules(rules, input_with_extension("pdf")).has_value()); }