From 7e83f5e4b07ad253bc2cbada9e1a1341b246f772 Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Thu, 23 Jul 2026 13:38:38 +0300 Subject: [PATCH] mu-regex: add match_groups, use everywhere Add match_groups to Mu::Regex, and update old "raw" GRegex users. --- lib/message/mu-message.cc | 18 +++------- lib/tests/bench-indexer.cc | 4 +-- lib/tests/test-mu-maildir.cc | 3 +- lib/utils/mu-regex.cc | 64 ++++++++++++++++++++++++++++++++++++ lib/utils/mu-regex.hh | 42 ++++++++++++++++++++--- lib/utils/mu-utils.cc | 37 ++++++++------------- 6 files changed, 122 insertions(+), 46 deletions(-) diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc index 41b737d3..57230098 100644 --- a/lib/message/mu-message.cc +++ b/lib/message/mu-message.cc @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -673,26 +674,17 @@ doc_add_list_post(Document& doc, const MimeMessage& mime_msg) /* some mailing lists do not set the reply-to; see pull #1278. So for * those cases, check the List-Post address and use that instead */ - GMatchInfo* minfo{}; const auto list_post{mime_msg.header("List-Post")}; if (!list_post) return; /* compile the regex only once */ - static GRegex *rx = g_regex_new( - "?", - G_REGEX_CASELESS, (GRegexMatchFlags)0, {}); - g_return_if_fail(rx); + static const auto rx = unwrap(Regex::make( + "?", G_REGEX_CASELESS)); Contacts contacts; - if (g_regex_match(rx, list_post->c_str(), (GRegexMatchFlags)0, &minfo)) { - auto address = (char*)g_match_info_fetch(minfo, 1); - contacts.push_back(Contact(address)); - g_free(address); - } - - if (minfo) - g_match_info_free(minfo); + if (const auto groups{rx.match_groups(*list_post)}; groups) + contacts.push_back(Contact(groups->at(1))); doc.add_extra_contacts(":list-post", contacts); } diff --git a/lib/tests/bench-indexer.cc b/lib/tests/bench-indexer.cc index 607413ca..ee3299bb 100644 --- a/lib/tests/bench-indexer.cc +++ b/lib/tests/bench-indexer.cc @@ -402,9 +402,7 @@ message(const Regex& rx, size_t id) char buf[16]; ::snprintf(buf, sizeof(buf), "%zu", id); - return to_string_gchar( - g_regex_replace(rx, test_msg, -1, 0, buf, - G_REGEX_MATCH_DEFAULT, {})); + return unwrap(rx.replace(test_msg, buf)); } struct TestData { diff --git a/lib/tests/test-mu-maildir.cc b/lib/tests/test-mu-maildir.cc index aee8189c..532bf6a2 100644 --- a/lib/tests/test-mu-maildir.cc +++ b/lib/tests/test-mu-maildir.cc @@ -31,6 +31,7 @@ #include "utils/mu-utils.hh" #include "utils/mu-utils-file.hh" #include "utils/mu-result.hh" +#include "utils/mu-regex.hh" using namespace Mu; @@ -135,7 +136,7 @@ test_maildir_mkdir_05(void) [[maybe_unused]] static void assert_matches_regexp(const char* str, const char* rx) { - if (!g_regex_match_simple(rx, str, (GRegexCompileFlags)0, (GRegexMatchFlags)0)) { + if (const auto rex{Regex::make(rx)}; !rex || !rex->matches(str)) { if (g_test_verbose()) g_print("%s does not match %s", str, rx); g_assert(0); diff --git a/lib/utils/mu-regex.cc b/lib/utils/mu-regex.cc index 81276952..dee9d8c5 100644 --- a/lib/utils/mu-regex.cc +++ b/lib/utils/mu-regex.cc @@ -61,6 +61,69 @@ test_regex_match2() } +static void +test_regex_match_groups() +{ + { + auto rx = Regex::make("^(\\d+)-(\\w+)$"); + assert_valid_result(rx); + + const auto groups{rx->match_groups("123-abc")}; + g_assert_true(!!groups); + g_assert_cmpuint(groups->size(), ==, 3); + assert_equal(groups->at(0), "123-abc"); + assert_equal(groups->at(1), "123"); + assert_equal(groups->at(2), "abc"); + + // no match -> Nothing + g_assert_false(!!rx->match_groups("nope")); + } + + { // always capture-count + 1 elements; a trailing group that + // did not participate is an empty string + auto rx = Regex::make("^(\\d+)(k|m|g)?$", G_REGEX_CASELESS); + assert_valid_result(rx); + + const auto groups{rx->match_groups("512")}; + g_assert_true(!!groups); + g_assert_cmpuint(groups->size(), ==, 3); + assert_equal(groups->at(1), "512"); + assert_equal(groups->at(2), ""); + + const auto groups2{rx->match_groups("512K")}; + g_assert_true(!!groups2); + g_assert_cmpuint(groups2->size(), ==, 3); + assert_equal(groups2->at(2), "K"); + } + + { // a non-participating group in the middle + auto rx = Regex::make("^(?:(a)|(b))(c)$"); + assert_valid_result(rx); + + const auto groups{rx->match_groups("bc")}; + g_assert_true(!!groups); + g_assert_cmpuint(groups->size(), ==, 4); + assert_equal(groups->at(1), ""); + assert_equal(groups->at(2), "b"); + assert_equal(groups->at(3), "c"); + } + + { // no capture groups at all -> just the full match + auto rx = Regex::make("b.c"); + assert_valid_result(rx); + + const auto groups{rx->match_groups("abxcd")}; + g_assert_true(!!groups); + g_assert_cmpuint(groups->size(), ==, 1); + assert_equal(groups->at(0), "bxc"); + } + + { // unset rx matches nothing + Regex rx; + g_assert_false(!!rx.match_groups("foo")); + } +} + static void test_regex_replace() { @@ -105,6 +168,7 @@ main(int argc, char* argv[]) g_test_add_func("/regex/match", test_regex_match); g_test_add_func("/regex/match2", test_regex_match2); + g_test_add_func("/regex/match-groups", test_regex_match_groups); g_test_add_func("/regex/replace", test_regex_replace); g_test_add_func("/regex/fail", test_regex_fail); diff --git a/lib/utils/mu-regex.hh b/lib/utils/mu-regex.hh index e0da7be5..1987a319 100644 --- a/lib/utils/mu-regex.hh +++ b/lib/utils/mu-regex.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022-2023 Dirk-Jan C. Binnema +** Copyright (C) 2022-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -30,10 +30,6 @@ namespace Mu { * PCRE rather than std::regex because it is much faster. */ struct Regex { -#if !GLIB_CHECK_VERSION(2,74,0) /* backward compat */ -#define G_REGEX_DEFAULT (static_cast(0)) -#define G_REGEX_MATCH_DEFAULT (static_cast(0)) -#endif /** * Trivial constructor * @@ -144,6 +140,42 @@ struct Regex { // the str.c_str(). It *seems* like a false alarm. } + /** + * Match this regexp against the given string and return the captured + * groups. + * + * Element 0 is the full match, element n corresponds with the n-th + * capture group in the pattern. The result always has capture-count + + * 1 elements; groups that did not participate in the match are empty + * strings. + * + * @param str string to match + * @param mflags match flags + * + * @return the captured groups, or Nothing if there was no match (or no + * valid regexp) + */ + Option> + match_groups(const std::string& str, + GRegexMatchFlags mflags=G_REGEX_MATCH_DEFAULT) const { + GMatchInfo *minfo{}; + if (!rx_ || !g_regex_match(rx_, str.c_str(), mflags, &minfo)) { + g_clear_pointer(&minfo, g_match_info_unref); + return Nothing; + } + + std::vector groups; + groups.reserve(g_regex_get_capture_count(rx_) + 1); + for (int i = 0; i != g_regex_get_capture_count(rx_) + 1; ++i) { + auto&& grp{g_match_info_fetch(minfo, i)}; + groups.emplace_back(grp ? grp : ""); + g_free(grp); + } + g_match_info_unref(minfo); + + return Some(std::move(groups)); + } + /** * Replace all occurrences of @this regexp in some string with a * replacement string diff --git a/lib/utils/mu-utils.cc b/lib/utils/mu-utils.cc index bb1c2b2a..f0faae40 100644 --- a/lib/utils/mu-utils.cc +++ b/lib/utils/mu-utils.cc @@ -44,6 +44,7 @@ #include #include "mu-utils.hh" +#include "mu-regex.hh" #include "mu-unbroken.hh" #include "mu-error.hh" @@ -538,43 +539,31 @@ Mu::parse_date_time(const std::string& dstr, bool is_first, bool utc) Option Mu::parse_size(const std::string& val, bool is_first) { - int64_t size{-1}; - std::string str; - GRegex* rx; - GMatchInfo* minfo; - /* one-sided ranges */ if (val.empty()) return is_first ? 0 : std::numeric_limits::max(); - rx = g_regex_new("^(\\d+)(b|k|kb|m|mb|g|gb)?$", - G_REGEX_CASELESS, (GRegexMatchFlags)0, NULL); - minfo = NULL; - if (g_regex_match(rx, val.c_str(), (GRegexMatchFlags)0, &minfo)) { + static const auto rx = + unwrap(Regex::make("^(\\d+)(b|k|kb|m|mb|g|gb)?$", G_REGEX_CASELESS)); - char* s = g_match_info_fetch(minfo, 1); - size = atoll(s); // check overflow? - g_free(s); + const auto groups{rx.match_groups(val)}; + if (!groups) + return Nothing; - s = g_match_info_fetch(minfo, 2); - switch (s ? g_ascii_tolower(s[0]) : 0) { - case 'k': size *= 1024; break; - case 'm': size *= (1024 * 1024); break; - case 'g': size *= (1024 * 1024 * 1024); break; - default: break; - } + int64_t size{::atoll(groups->at(1).c_str())}; // check overflow? - g_free(s); + const auto& unit{groups->at(2)}; + switch (unit.empty() ? 0 : g_ascii_tolower(unit.at(0))) { + case 'k': size *= 1024; break; + case 'm': size *= (1024 * 1024); break; + case 'g': size *= (1024 * 1024 * 1024); break; + default: break; } - g_regex_unref(rx); - g_match_info_unref(minfo); - if (size < 0) return Nothing; else return size; - } std::string