Introduce simple regex utils

This commit is contained in:
kormax
2026-03-24 20:21:51 +02:00
parent 7bedde39a7
commit e44e1f1cbb
4 changed files with 178 additions and 0 deletions
+87
View File
@@ -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", "<str>", "regex pattern"),
arg_str0("d", "text", "<str>", "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}
};
+84
View File
@@ -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];
+6
View File
@@ -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);
+1
View File
@@ -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