From e44e1f1cbb956059a973bbdf7b77dfb4a8e39772 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:21:51 +0200 Subject: [PATCH] Introduce simple regex utils --- client/src/cmdanalyse.c | 87 +++++++++++++++++++++++++++++++++++++++++ client/src/util.c | 84 +++++++++++++++++++++++++++++++++++++++ client/src/util.h | 6 +++ tools/pm3_tests.sh | 1 + 4 files changed, 178 insertions(+) diff --git a/client/src/cmdanalyse.c b/client/src/cmdanalyse.c index 9c2d7ec8b..74873b94c 100644 --- a/client/src/cmdanalyse.c +++ b/client/src/cmdanalyse.c @@ -36,6 +36,7 @@ #include "cliparser.h" #include "generator.h" // generate nuid #include "iso14b.h" // defines for ETU conversions +#include "util.h" // regex utility static int CmdHelp(const char *Cmd); @@ -1168,6 +1169,91 @@ static int CmdAnalyseUnits(const char *Cmd) { return PM3_SUCCESS; } +static int CmdAnalyseRegex(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "analyse regex", + "Regex utility (subset: ^ $ . * with \\\\ escape)", + "analyse regex --pattern '^A000' --text A000000476D0000111\n" + "analyse regex --pattern '.*500A416E64726F6964506179.*9000$' --text 6F8150500A416E64726F69645061799000 --insensitive\n" + "analyse regex --test" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0("p", "pattern", "", "regex pattern"), + arg_str0("d", "text", "", "text to match"), + arg_lit0("i", "insensitive", "case-insensitive match"), + arg_lit0("t", "test", "run self tests"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + struct arg_str *arg_pattern = arg_get_str(ctx, 1); + struct arg_str *arg_text = arg_get_str(ctx, 2); + bool insensitive = arg_get_lit(ctx, 3); + bool selftest = arg_get_lit(ctx, 4); + + if (selftest) { + CLIParserFree(ctx); + + typedef struct { + const char *pattern; + const char *text; + bool case_insensitive; + bool expect_match; + } regex_test_case_t; + + const regex_test_case_t tests[] = { + {.pattern = "^A000", .text = "A000000476D0000111", .case_insensitive = false, .expect_match = true}, + {.pattern = "9000$", .text = "6F009000", .case_insensitive = false, .expect_match = true}, + {.pattern = ".*500A416E64726F6964506179.*9000$", .text = "6F8150500A416E64726F69645061799000", .case_insensitive = true, .expect_match = true}, + {.pattern = "^a0.*$", .text = "A0000000", .case_insensitive = true, .expect_match = true}, + {.pattern = "^a0.*$", .text = "B0000000", .case_insensitive = true, .expect_match = false}, + {.pattern = "A\\*B", .text = "ZZA*BZZ", .case_insensitive = false, .expect_match = true}, + {.pattern = "A+B", .text = "AAAB", .case_insensitive = false, .expect_match = false}, + {.pattern = "*ABC", .text = "ABC", .case_insensitive = false, .expect_match = false}, + {.pattern = "ABC\\", .text = "ABC", .case_insensitive = false, .expect_match = false}, + }; + + bool all_ok = true; + for (size_t i = 0; i < ARRAYLEN(tests); i++) { + bool matched = tests[i].case_insensitive + ? str_regex_match_case_insensitive(tests[i].pattern, tests[i].text) + : str_regex_match(tests[i].pattern, tests[i].text); + + bool ok = (matched == tests[i].expect_match); + PrintAndLogEx(ok ? SUCCESS : FAILED, "%zu. pattern=`%s` valid=%s match=%s ( %s )", + i + 1, + tests[i].pattern, + "true", + matched ? "true" : "false", + ok ? _GREEN_("ok") : _RED_("fail")); + if (!ok) { + all_ok = false; + } + } + + PrintAndLogEx(all_ok ? SUCCESS : FAILED, "Tests ( %s )", all_ok ? _GREEN_("ok") : _RED_("fail")); + return all_ok ? PM3_SUCCESS : PM3_ESOFT; + } + + if (arg_pattern->count == 0 || arg_text->count == 0) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "pattern and text are required unless --test is used"); + return PM3_EINVARG; + } + + const char *pattern = arg_pattern->sval[0]; + const char *text = arg_text->sval[0]; + + bool matched = insensitive + ? str_regex_match_case_insensitive(pattern, text) + : str_regex_match(pattern, text); + CLIParserFree(ctx); + PrintAndLogEx(matched ? SUCCESS : INFO, "Regex match: %s", matched ? _GREEN_("true") : _YELLOW_("false")); + return PM3_SUCCESS; +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"lrc", CmdAnalyseLRC, AlwaysAvailable, "Generate final byte for XOR LRC"}, @@ -1180,6 +1266,7 @@ static command_t CommandTable[] = { {"demodbuff", CmdAnalyseDemodBuffer, AlwaysAvailable, "Load binary string to DemodBuffer"}, {"freq", CmdAnalyseFreq, AlwaysAvailable, "Calc wave lengths"}, {"foo", CmdAnalyseFoo, AlwaysAvailable, "muxer"}, + {"regex", CmdAnalyseRegex, AlwaysAvailable, "Regex utility (subset: ^ $ . * with \\\\ escape)"}, {"units", CmdAnalyseUnits, AlwaysAvailable, "convert ETU <> US <> SSP_CLK (3.39MHz)"}, {NULL, NULL, NULL, NULL} }; diff --git a/client/src/util.c b/client/src/util.c index 0cb222326..e043a4120 100644 --- a/client/src/util.c +++ b/client/src/util.c @@ -1499,6 +1499,90 @@ size_t str_nlen(const char *src, size_t maxlen) { return len; } +static bool str_regex_atom_matches(char atom, bool escaped, char c) { + if (!escaped && atom == '.') { + return true; + } + return (atom == c); +} + +static bool str_regex_match_here(const char *regexp, const char *text); + +static bool str_regex_match_star(char atom, bool escaped, const char *regexp, const char *text) { + do { + if (str_regex_match_here(regexp, text)) { + return true; + } + } while (*text != '\0' && str_regex_atom_matches(atom, escaped, *text++)); + + return false; +} + +static bool str_regex_match_here(const char *regexp, const char *text) { + if (regexp[0] == '\0') { + return true; + } + + if (regexp[0] == '$' && regexp[1] == '\0') { + return (text[0] == '\0'); + } + + bool escaped = false; + char atom = regexp[0]; + size_t atom_len = 1; + if (regexp[0] == '\\' && regexp[1] != '\0') { + escaped = true; + atom = regexp[1]; + atom_len = 2; + } + + if (regexp[atom_len] == '*') { + return str_regex_match_star(atom, escaped, regexp + atom_len + 1, text); + } + + if (text[0] != '\0' && str_regex_atom_matches(atom, escaped, text[0])) { + return str_regex_match_here(regexp + atom_len, text + 1); + } + + return false; +} + +bool str_regex_match(const char *regexp, const char *text) { + if (regexp[0] == '^') { + return str_regex_match_here(regexp + 1, text); + } + + do { + if (str_regex_match_here(regexp, text)) { + return true; + } + } while (*text++ != '\0'); + + return false; +} + +bool str_regex_match_case_insensitive(const char *regexp, const char *text) { + if (regexp == NULL || text == NULL) { + return false; + } + + char *pattern_lc = str_dup(regexp); + char *text_lc = str_dup(text); + if (pattern_lc == NULL || text_lc == NULL) { + free(pattern_lc); + free(text_lc); + return false; + } + + str_lower(pattern_lc); + str_lower(text_lc); + bool matched = str_regex_match(pattern_lc, text_lc); + + free(pattern_lc); + free(text_lc); + return matched; +} + void str_reverse(char *buf, size_t len) { for (size_t i = 0; i < (len >> 1); i++) { char tmp = buf[i]; diff --git a/client/src/util.h b/client/src/util.h index cb3a1555c..036a67d5c 100644 --- a/client/src/util.h +++ b/client/src/util.h @@ -181,6 +181,12 @@ void str_trim(char *s); char *str_dup(const char *src); char *str_ndup(const char *src, size_t len); size_t str_nlen(const char *src, size_t maxlen); +// Lightweight regex subset: +// - supported metacharacters: '^' (start), '$' (end), '.' (any char), '*' (zero or more) +// - escaping: '\\' to match the following char literally +// - all other regex constructs are currently treated as literal characters +bool str_regex_match(const char *regexp, const char *text); +bool str_regex_match_case_insensitive(const char *regexp, const char *text); int hexstring_to_u96(uint32_t *hi2, uint32_t *hi, uint32_t *lo, const char *str); int binstring_to_u96(uint32_t *hi2, uint32_t *hi, uint32_t *lo, const char *str); diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index 2fcaaf82d..c7aa29d2e 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -465,6 +465,7 @@ while true; do if ! CheckExecute "mfu pwdgen test" "$CLIENTBIN -c 'hf mfu pwdgen --test'" "Selftest ok"; then break; fi if ! CheckExecute "mfu keygen test" "$CLIENTBIN -c 'hf mfu keygen --uid 11223344556677'" "80 B1 C2 71 D8 A0"; then break; fi if ! CheckExecute "jooki encode test" "$CLIENTBIN -c 'hf jooki encode --test'" "04 28 F4 DA F0 4A 81 \( ok \)"; then break; fi + if ! CheckExecute "analyse regex selftest" "$CLIENTBIN -c 'analyse regex --test'" "Tests \( ok \)"; then break; fi if ! CheckExecute "trace load/list 14a" "$CLIENTBIN -c 'trace load -f traces/hf_14a_mfu.trace; trace list -1 -t 14a;'" "READBLOCK\(8\)"; then break; fi if ! CheckExecute "trace load/list x" "$CLIENTBIN -c 'trace load -f traces/hf_14a_mfu.trace; trace list -x1 -t 14a;'" "0.0101840425"; then break; fi if ! CheckExecute "nfc decode test - oob" "$CLIENTBIN -c 'nfc decode -d DA2010016170706C69636174696F6E2F766E642E626C7565746F6F74682E65702E6F6F62301000649201B96DFB0709466C65782032'" "Flex 2"; then break; fi