/* ** Copyright (C) 2023-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 ** Free Software Foundation; either version 3, or (at your option) any ** later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software Foundation, ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ** */ #include "mu-query-parser.hh" #include #include #include #include #include "utils/mu-utils.hh" #include "utils/mu-sexp.hh" #include "utils/mu-option.hh" #include #include "utils/mu-utils-file.hh" using namespace Mu; // Sexp extensions... static Option second(Sexp& s) { if (s.listp() && !s.empty() && s.cbegin() + 1 != s.cend()) return *(s.begin()+1); else return Nothing; } static bool looks_like_matcher(const Sexp& sexp) { // all the "terminal values" (from the Mu parser's pov) const auto value_syms = std::to_array({ placeholder_sym, phrase_sym, regex_sym, range_sym, wildcard_sym }); if (!sexp.listp() || sexp.empty() || !sexp.front().symbolp()) return false; const auto symbol{sexp.front().symbol()}; if (seq_some(value_syms, [&](auto &&sym) { return symbol == sym; })) return true; else if (!!field_from_name(symbol.name) || field_is_combi(symbol.name)) return true; else return false; } struct ParseContext { bool expand{}; size_t depth{}; /* current parenthesis-nesting depth */ }; /* parsing is best-effort; deeper nesting than this is ignored * (this also caps the recursion depth) */ constexpr size_t MaxDepth{100}; /** * A cursor over the flat token-list, so popping is O(1) */ struct TokenStream { explicit TokenStream(Sexp& tokens): toks_{tokens.list()} {} bool empty() const { return pos_ >= toks_.size(); } Option head() { if (empty()) return Nothing; else return toks_[pos_]; } bool head_symbolp(const Sexp::Symbol& sym) const { return pos_ < toks_.size() && toks_[pos_].symbolp(sym); } void pop_front() { ++pos_; } private: Sexp::List& toks_; size_t pos_{}; }; /** * Indexable fields become _phrase_ fields if they contain * wordbreakable data; * * @param field * @param val * * @return an s-expr or nothing */ static Option phrasify(const Field& field, const Sexp& val) { if (!field.is_phrasable_term() || !val.stringp()) return Nothing; // nothing to phrasify auto words{utf8_wordbreak(val.string())}; if (words.find(' ') == std::string::npos) return Nothing; // nothing to phrasify auto phrase = Sexp { Sexp::Symbol{field.name}, Sexp{phrase_sym, Sexp{std::move(words)}}}; // if the field both a normal term & phrasable, match both // if they are different if (val.string() != words) return Sexp{or_sym, Sexp {Sexp::Symbol{field.name}, Sexp(val.string())}, std::move(phrase)}; else return phrase; } /* * Grammar * * query -> factor { ( | ) factor } * factor -> unit { [] unit } * unit -> matcher | query | <(> query <)> * matcher */ static Sexp query(TokenStream& tokens, ParseContext& ctx); static Sexp finalize_matcher(Sexp&& val, ParseContext& ctx) { const auto fieldsym{val.front().symbol()}; // Note the _expand_ case is what we use when processing the query 'for real'; // the non-expand case is only to have a bit more human-readable Sexp for use // mu find's '--analyze' // // Re: phrase-fields We map something like 'subject:hello-world' // to // (or (subject "hello-world" (subject (phrase "hello world")))) if (ctx.expand) { /* should we expand meta-fields? */ auto fields = fields_from_name(fieldsym == placeholder_sym ? "" : fieldsym.name); if (!fields.empty() && second(val)) { Sexp vals{}; vals.add(or_sym); for (auto&& field: fields) { if (auto&& phrase{phrasify(field, *second(val))}; phrase) vals.add(std::move(*phrase)); else vals.add(Sexp{Sexp::Symbol{field.name}, Sexp{*second(val)}}); } val = std::move(vals); } } if (auto&& field{field_from_name(fieldsym.name)}; field) { if (auto&& v{second(val)}; v) if (auto&& phrase{phrasify(*field, *v)}; phrase) val = std::move(*phrase); } return std::move(val); } static Sexp matcher(TokenStream& tokens, ParseContext& ctx) { if (tokens.empty()) return {}; auto val{*tokens.head()}; tokens.pop_front(); /* special case: if we find some non-matcher type here, we need to second-guess the token */ if (!looks_like_matcher(val)) val = Sexp{placeholder_sym, val.symbol().name}; return finalize_matcher(std::move(val), ctx); } static Sexp unit(TokenStream& tokens, ParseContext& ctx) { if (tokens.head_symbolp(not_sym)) { /* NOT */ /* handle (chains of) NOTs iteratively; parity decides */ bool neg{}; while (tokens.head_symbolp(not_sym)) { tokens.pop_front(); neg = !neg; } Sexp sub{unit(tokens, ctx)}; /* special case: interpret a trailing "not" as a matcher instead */ if (sub.empty()) { sub = finalize_matcher(Sexp{placeholder_sym, not_sym.name}, ctx); neg = !neg; } if (!neg) return sub; /* we try to optimize: double negations are removed */ if (sub.head_symbolp(not_sym)) return *second(sub); else return Sexp(not_sym, std::move(sub)); } else if (tokens.head_symbolp(open_sym)) { /* ( sub) */ tokens.pop_front(); if (ctx.depth >= MaxDepth) /* nested too deeply; bail out */ return {}; ++ctx.depth; Sexp sub{query(tokens, ctx)}; --ctx.depth; if (tokens.head_symbolp(close_sym)) tokens.pop_front(); return sub; } /* matcher */ return matcher(tokens, ctx); } static Sexp factor(TokenStream& tokens, ParseContext& ctx) { Sexp un = unit(tokens, ctx); /* query 'a b' is to be interpreted as 'a AND b'; * * we need an implicit AND if the head symbol is either * a matcher (value) or the start of a sub-expression */ auto implicit_and = [&]() { if (tokens.head_symbolp(open_sym)) return true; else if (tokens.head_symbolp(not_sym)) // turn a lone 'not' -> 'and not' return true; else if (auto&& head{tokens.head()}; head) return looks_like_matcher(*head); else return false; }; Sexp uns; while (true) { if (tokens.head_symbolp(and_sym)) tokens.pop_front(); else if (!implicit_and()) break; if (auto&& un2 = unit(tokens, ctx); !un2.empty()) uns.add(std::move(un2)); else break; } if (!uns.empty()) { un = Sexp{and_sym, std::move(un)}; un.add_list(std::move(uns)); } return un; } static Sexp query(TokenStream& tokens, ParseContext& ctx) { /* process a left-associative chain of factors, separated by * /. Chains of the same operator are flattened, i.e. * (or (or a b) c) => (or a b c), since Xapian likes flat trees */ Sexp fact = factor(tokens, ctx); while (true) { const Sexp::Symbol* opsym{}; if (tokens.head_symbolp(or_sym)) opsym = &or_sym; else if (tokens.head_symbolp(xor_sym)) opsym = &xor_sym; else break; tokens.pop_front(); Sexp rhs = factor(tokens, ctx); if (rhs.empty()) break; /* trailing op; ignore */ if (!fact.head_symbolp(*opsym)) fact = Sexp{*opsym, std::move(fact)}; fact.add(std::move(rhs)); } return fact; } Sexp Mu::parse_query(const std::string& expr, bool expand) { ParseContext context; context.expand = expand; auto items = process_query(expr); if (!items.listp()) throw std::runtime_error("tokens must be a list-sexp"); TokenStream tokens{items}; return query(tokens, context); } #if defined(BUILD_PARSE_QUERY)||defined(BUILD_PARSE_QUERY_EXPAND) int main (int argc, char *argv[]) { if (argc < 2) { mu_printerrln("expected: {} ", argv[0]); return 1; } std::string expr; for (auto i = 1; i < argc; ++i) { expr += argv[i]; expr += " "; } auto&& sexp = parse_query(expr, #ifdef BUILD_PARSE_QUERY_EXPAND true/*expand*/ #else false/*don't expand*/ #endif ); mu_println("{}", sexp.to_string()); return 0; } #endif // BUILD_PARSE_QUERY || BUILD_PARSE_QUERY_EXPAND #if BUILD_TESTS /* * Tests. * */ #include "utils/mu-test-utils.hh" using TestCase = std::pair; static void test_parser_basic() { std::vector cases = { // single term TestCase{R"(a)", R"((_ "a"))"}, // a and b TestCase{R"(a and b)", R"((and (_ "a") (_ "b")))"}, // a and b and c TestCase{R"(a and b and c)", R"((and (_ "a") (_ "b") (_ "c")))"}, // a or b TestCase{R"(a or b)", R"((or (_ "a") (_ "b")))"}, // or-chains are flattened TestCase{R"(a or b or c)", R"((or (_ "a") (_ "b") (_ "c")))"}, // a or b and c TestCase{R"(a or b and c)", R"((or (_ "a") (and (_ "b") (_ "c"))))"}, // a and b or c TestCase{R"(a and b or c)", R"((or (and (_ "a") (_ "b")) (_ "c")))"}, // mixed or/xor associate to the left TestCase{R"(a or b xor c)", R"((xor (or (_ "a") (_ "b")) (_ "c")))"}, TestCase{R"(a xor b or c)", R"((or (xor (_ "a") (_ "b")) (_ "c")))"}, // not a TestCase{R"(not a)", R"((not (_ "a")))"}, // lone not TestCase{R"(not)", R"((_ "not"))"}, // a and (b or c) TestCase{R"(a and (b or c))", R"((and (_ "a") (or (_ "b") (_ "c"))))"}, // not a and not b TestCase{R"(not a and b)", R"((and (not (_ "a")) (_ "b")))"}, // a not b TestCase{R"(a not b)", R"((and (_ "a") (not (_ "b"))))"}, }; for (auto&& test: cases) { auto&& sexp{parse_query(test.first)}; //mu_message ("'{}' <=> '{}'", sexp.to_string(), test.second); assert_equal(sexp.to_string(), test.second); } } static void test_parser_recover() { std::vector cases = { // implicit AND TestCase{R"(a b)", R"((and (_ "a") (_ "b")))"}, // a or or (second to be used as value) TestCase{R"(a or and)", R"((or (_ "a") (_ "and")))"}, // missing end ) TestCase{R"(a and ()", R"((_ "a"))"}, // missing end ) TestCase{R"(a and (b)", R"((and (_ "a") (_ "b")))"}, // trailing operator is dropped TestCase{R"(a or)", R"((_ "a"))"}, // quoted operators are matchers, not operators TestCase{R"(foo "and" bar)", R"((and (_ "foo") (_ "and") (_ "bar")))"}, }; for (auto&& test: cases) { auto&& sexp{parse_query(test.first)}; assert_equal(sexp.to_string(), test.second); } } static void test_parser_pathological() { // pathological queries parse (possibly partially) without // crashes or quadratic slow-down. std::string parens(10000, '('); parens += "a"; parens.append(10000, ')'); g_assert_true(parse_query(parens).listp()); std::string nots; for (auto i = 0; i != 10000; ++i) nots += "not "; nots += "a"; // even number of nots assert_equal(parse_query(nots).to_string(), R"((_ "a"))"); } static void test_parser_fields() { std::vector cases = { // simple field TestCase{R"(s:hello)", R"((subject "hello"))"}, // field, wildcard, regexp TestCase{R"(subject:a* recip:/b/)", R"((and (subject (wildcard "a")) (recip (regex "b"))))"}, TestCase{R"(from:hello or subject:world)", R"((or (from "hello") (subject "world")))"}, }; for (auto&& test: cases) { auto&& sexp{parse_query(test.first)}; assert_equal(sexp.to_string(), test.second); } } static void test_parser_expand() { std::vector cases = { // simple field TestCase{R"(recip:a)", R"((or (to "a") (cc "a") (bcc "a")))"}, // field, wildcard, regexp TestCase{R"(a*)", R"((or (to (wildcard "a")) (cc (wildcard "a")) (bcc (wildcard "a")) (from (wildcard "a")) (subject (wildcard "a")) (body (wildcard "a")) (embed (wildcard "a"))))"}, TestCase{R"(a xor contact:b)", R"((xor (or (to "a") (cc "a") (bcc "a") (from "a") (subject "a") (body "a") (embed "a")) (or (to "b") (cc "b") (bcc "b") (from "b"))))"} }; for (auto&& test: cases) { auto&& sexp{parse_query(test.first, true/*expand*/)}; assert_equal(sexp.to_string(), test.second); } } static void test_parser_range() { std::vector cases = { TestCase{R"(size:1)", R"((size (range "1" "1")))"}, TestCase{R"(size:2..)", R"((size (range "2" "")))"}, TestCase{R"(size:..1k)", R"((size (range "" "1024")))"}, TestCase{R"(size:..)", R"((size (range "" "")))"}, }; for (auto&& test: cases) { auto&& sexp{parse_query(test.first, true/*expand*/)}; assert_equal(sexp.to_string(), test.second); } } static void test_parser_optimize() { std::vector cases = { TestCase{R"(not a)", R"((not (_ "a")))"}, TestCase{R"(not not a)", R"((_ "a"))"}, TestCase{R"(not not not a)", R"((not (_ "a")))"}, TestCase{R"(not not not not a)", R"((_ "a"))"}, }; for (auto&& test: cases) { auto&& sexp{parse_query(test.first)}; assert_equal(sexp.to_string(), test.second); } } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_test_add_func("/query-parser/basic", test_parser_basic); g_test_add_func("/query-parser/recover", test_parser_recover); g_test_add_func("/query-parser/pathological", test_parser_pathological); g_test_add_func("/query-parser/fields", test_parser_fields); g_test_add_func("/query-parser/range", test_parser_range); g_test_add_func("/query-parser/expand", test_parser_expand); g_test_add_func("/query-parser/optimize", test_parser_optimize); return g_test_run(); } #endif /*BUILD_TESTS*/