Files
mu4e/lib/utils/mu-sexp.cc
Dirk-Jan C. Binnema da2995a768 mu-sexp: improve corner-cases, performance
Some C++20 updates; use the new ascii/ctype functions. Avoid unlimited
recursion. Speed-up conversion to_string

Update unit-tests.
2026-08-20 15:05:30 -07:00

605 lines
14 KiB
C++

/*
** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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-sexp.hh"
#include "mu-utils.hh"
#include <charconv>
using namespace Mu;
/* avoid unbounded recursion (i.e., stack overflow) for pathological input */
constexpr size_t MaxDepth{1024};
template<typename...T> static Mu::Error
parsing_error(size_t pos, fmt::format_string<T...> frm, T&&... args)
{
const auto&& msg{fmt::format(frm, std::forward<T>(args)...)};
return Mu::Error(Error::Code::Parsing, "{}: {}", pos, msg);
}
static size_t
skip_whitespace(const std::string& s, size_t pos)
{
while (pos != s.size() && is_ascii_space(s[pos]))
++pos;
return pos;
}
static Result<Sexp> parse(const std::string& expr, size_t& pos, size_t depth);
static Result<Sexp>
parse_list(const std::string& expr, size_t& pos, size_t depth)
{
if (expr[pos] != '(') // sanity check.
return Err(parsing_error(pos, "expected: '(' but got '{}'", expr[pos]));
if (depth >= MaxDepth)
return Err(parsing_error(pos, "parentheses nested too deeply"));
Sexp lst{};
++pos;
while (pos < expr.size() && expr[pos] != ')') {
if (auto&& item = parse(expr, pos, depth + 1); item)
lst.add(std::move(*item));
else
return Err(item.error());
}
if (pos >= expr.size())
return Err(parsing_error(pos, "expected: ')'"));
else if (expr[pos] != ')')
return Err(parsing_error(pos, "expected: ')' but got '{}'", expr[pos]));
++pos;
return Ok(std::move(lst));
}
static Result<Sexp>
parse_string(const std::string& expr, size_t& pos)
{
if (expr[pos] != '"') // sanity check.
return Err(parsing_error(pos, "expected: '\"' but got '{}'", expr[pos]));
bool escape{};
std::string str;
for (++pos; pos != expr.size(); ++pos) {
auto kar = expr[pos];
if (escape) {
// follow the elisp reader: '\n', '\t' are special,
// any other escaped character stands for itself.
switch (kar) {
case 'n': str += '\n'; break;
case 't': str += '\t'; break;
default: str += kar; break;
}
escape = false;
} else if (kar == '\\')
escape = true;
else if (kar == '"')
break;
else
str += kar;
}
if (escape || pos == expr.size())
return Err(parsing_error(pos, "unterminated string '{}'", str));
++pos;
return Ok(Sexp{std::move(str)});
}
static Result<Sexp>
parse_integer(const std::string& expr, size_t& pos)
{
if (!is_ascii_digit(expr[pos]) && expr[pos] != '-') // sanity check.
return Err(parsing_error(pos, "expected: <digit> but got '{}'", expr[pos]));
auto end{pos + (expr[pos] == '-' ? 1U : 0U)};
while (end != expr.size() && is_ascii_digit(expr[end]))
++end;
Sexp::Number num{};
const auto res{std::from_chars(expr.data() + pos, expr.data() + end, num)};
if (res.ec != std::errc{} || res.ptr != expr.data() + end)
return Err(parsing_error(pos, "invalid number '{}'",
expr.substr(pos, end - pos)));
pos = end;
return Ok(Sexp{num});
}
static Result<Sexp>
parse_symbol(const std::string& expr, size_t& pos)
{
if (!is_ascii_alpha(expr[pos]) && expr[pos] != ':') // sanity check.
return Err(parsing_error(pos, "expected: <alpha>|: but got '{}'", expr[pos]));
const auto start{pos};
for (++pos; pos != expr.size() &&
(is_ascii_alnum(expr[pos]) || expr[pos] == '-'); ++pos)
;
return Ok(Sexp{Sexp::Symbol{expr.substr(start, pos - start)}});
}
static Result<Sexp>
parse(const std::string& expr, size_t& pos, size_t depth)
{
pos = skip_whitespace(expr, pos);
if (pos == expr.size())
return Err(parsing_error(pos, "unexpected end of input"));
const auto kar = expr[pos];
const auto sexp = std::invoke([&]() -> Result<Sexp> {
if (kar == '(')
return parse_list(expr, pos, depth);
else if (kar == '"')
return parse_string(expr, pos);
else if (is_ascii_digit(kar) || kar == '-')
return parse_integer(expr, pos);
else if (is_ascii_alpha(kar) || kar == ':')
return parse_symbol(expr, pos);
else
return Err(parsing_error(pos, "unexpected character '{}'", kar));
});
if (sexp)
pos = skip_whitespace(expr, pos);
return sexp;
}
Result<Sexp>
Sexp::parse(const std::string& expr)
{
size_t pos{};
auto res = ::parse(expr, pos, 0/*depth*/);
if (!res)
return res;
else if (pos != expr.size())
return Err(parsing_error(pos, "trailing data starting with '{}'", expr[pos]));
else
return res;
}
std::string
Sexp::to_string(Format fopts) const
{
std::string str;
to_string_into(str, fopts);
return str;
}
void
Sexp::to_string_into(std::string& out, Format fopts) const
{
if (listp()) {
out += '(';
bool first{true};
for(auto&& elm: list()) {
if (!first)
out += ' ';
elm.to_string_into(out, fopts);
first = false;
}
out += ')';
if (any_of(fopts & Format::SplitList))
out += '\n';
} else if (stringp())
out += quote(string());
else if (numberp())
out += std::to_string(number());
else if (symbolp())
out += symbol().name;
if (any_of(fopts & Format::TypeInfo)) {
out += '<';
out += Sexp::type_name(type());
out += '>';
}
}
// LCOV_EXCL_START
// convert emacs-timestamp (a list) to a unix-tstamp
static uint64_t
unix_tstamp(const Sexp& emacs_tstamp)
{
if (!emacs_tstamp.listp() || emacs_tstamp.list().size() < 2)
throw std::runtime_error("unexpected type");
const auto& lst{emacs_tstamp.list()};
return (lst.at(0).number() << 16) | lst.at(1).number();
}
std::string
Sexp::to_json_string(Format fopts) const
{
std::string str;
to_json_string_into(str, fopts);
return str;
}
void
Sexp::to_json_string_into(std::string& out, Format fopts) const
{
const auto sym_name=[&](const std::string& sym) {
if (any_of(fopts & Format::NoColon) && sym[0] == ':')
return sym.substr(1); // remove colon
else
return sym;
};
switch (type()) {
case Type::List: {
// property-lists become JSON objects
if (plistp()) {
out += '{';
auto it{list().begin()};
bool first{true};
while (it != list().end()) {
const auto key{it->symbol().name};
if (!first)
out += ',';
out += quote(sym_name(key));
out += ':';
++it;
const auto& propval{*it};
propval.to_json_string_into(out, fopts);
++it;
first = false;
// special-case: tstamp-fields also get a "unix" value,
// which are easier to work with than the "emacs" timestamps
if (key == ":date" || key == ":changed") {
out += ',';
out += quote(sym_name(key) + "-unix");
out += ':';
out += std::to_string(unix_tstamp(propval));
}
}
out += '}';
if (any_of(fopts & Format::SplitList))
out += '\n';
} else { // other lists become arrays.
out += '[';
bool first{true};
for (auto&& child : list()) {
if (!first)
out += ", ";
child.to_json_string_into(out, fopts);
first = false;
}
out += ']';
if (any_of(fopts & Format::SplitList))
out += '\n';
}
break;
}
case Type::String:
out += quote(string());
break;
case Type::Symbol:
if (nilp())
out += "false";
else if (symbol() == "t")
out += "true";
else
out += quote(symbol().name);
break;
case Type::Number:
out += std::to_string(number());
break;
default:
break;
}
}
Sexp&
Sexp::del_prop(const std::string& pname)
{
if (auto kill_it = find_prop(pname, begin(), end()); kill_it != cend())
list().erase(kill_it, kill_it + 2);
return *this;
}
Sexp::const_iterator
Sexp::find_prop(const std::string& s,
Sexp::const_iterator b, Sexp::const_iterator e) const
{
for (auto&& it = b; it != e && it+1 != e; it += 2)
if (it->symbolp() && it->symbol() == s)
return it;
return e;
}
Sexp::iterator
Sexp::find_prop(const std::string& s,
Sexp::iterator b, Sexp::iterator e)
{
for (auto&& it = b; it != e && it+1 != e; it += 2)
if (it->symbolp() && it->symbol() == s)
return it;
return e;
}
bool
Sexp::plistp(Sexp::const_iterator b, Sexp::const_iterator e) const
{
if (b == e)
return true;
else if (b + 1 == e)
return false;
else
return b->symbolp() &&
b->symbol().name[0] == ':' &&
plistp(b + 2, e);
}
// LCOV_EXCL_STOP
#if BUILD_TESTS
#include "mu-test-utils.hh"
static void
test_list()
{
{
Sexp s;
g_assert_true(s.listp());
g_assert_true(s.to_string() == "()");
g_assert_true(Sexp::type_name(s.type()) == "list");
g_assert_true(s.empty());
}
{
Sexp::List items = {
Sexp("hello"),
Sexp(123),
Sexp::Symbol("world")
};
const Sexp s{std::move(items)};
g_assert_false(s.empty());
g_assert_cmpuint(s.size(),==,3);
g_assert_true(s.to_string() == "(\"hello\" 123 world)");
/* copy */
Sexp s2 = s;
g_assert_true(s2.to_string() == "(\"hello\" 123 world)");
/* move */
Sexp s3 = std::move(s2);
g_assert_true(s3.to_string() == "(\"hello\" 123 world)");
s3.clear();
g_assert_true(s3.empty());
}
}
static void
test_string()
{
{
Sexp s("hello");
g_assert_true(s.stringp());
g_assert_true(s.string()=="hello");
g_assert_true(s.to_string()=="\"hello\"");
g_assert_true(Sexp::type_name(s.type()) == "string");
}
{
// Sexp s(std::string_view("hel\"lo"));
// g_assert_true(s.is_string());
// g_assert_cmpstr(s.string().c_str(),==,"hel\"lo");
// g_assert_cmpstr(s.to_string().c_str(),==,"\"hel\\\"lo\"");
}
}
static void
test_number()
{
{
Sexp s(123);
g_assert_true(s.numberp());
g_assert_cmpint(s.number(),==,123);
g_assert_true(s.to_string() == "123");
g_assert_true(Sexp::type_name(s.type()) == "number");
}
{
Sexp s(true);
g_assert_true(s.numberp());
g_assert_cmpint(s.number(),==,1);
g_assert_true(s.to_string()=="1");
}
}
static void
test_symbol()
{
{
Sexp s{Sexp::Symbol("hello")};
g_assert_true(s.symbolp());
g_assert_true(s.symbol()=="hello");
g_assert_true (s.to_string()=="hello");
g_assert_true(Sexp::type_name(s.type()) == "symbol");
}
{
Sexp s{"hello"_sym};
g_assert_true(s.symbolp());
g_assert_true(s.symbol()=="hello");
g_assert_true (s.to_string()=="hello");
}
}
static void
test_multi()
{
Sexp s{"abc", 123, Sexp::Symbol{"def"}};
g_assert_true(s.to_string() == "(\"abc\" 123 def)");
}
static void
test_add()
{
{
Sexp s{"abc", 123};
s.add("def"_sym);
g_assert_true(s.to_string() == "(\"abc\" 123 def)");
}
}
static void
test_add_multi()
{
{
Sexp s{"abc", 123};
s.add("def"_sym, 456, Sexp{"boo", 2});
g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))");
}
{
Sexp s{"abc", 123};
Sexp t{"boo", 2};
s.add("def"_sym, 456, t);
g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))");
}
}
static void
test_plist()
{
Sexp s;
s.put_props("hello", "world"_sym, "foo", 123, "bar"_sym, "cuux");
g_assert_true(s.to_string() == R"((hello world foo 123 bar "cuux"))");
s.put_props("hello", 12345);
g_assert_true(s.to_string() == R"((foo 123 bar "cuux" hello 12345))");
}
static void
check_parse(const std::string& expr, const std::string& expected)
{
auto sexp = Sexp::parse(expr);
assert_valid_result(sexp);
assert_equal(to_string(*sexp), expected);
}
static void
test_parser()
{
check_parse(":foo-123", ":foo-123");
check_parse("foo", "foo");
check_parse(R"(12345)", "12345");
check_parse(R"(-12345)", "-12345");
check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")");
check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\"");
check_parse(R"("foo
bar")",
"\"foo\nbar\"");
// escapes: '\n'/'\t' are special; \\ and unknown escapes
// stand for the escaped character itself
check_parse(R"("hello\nworld")", "\"hello\nworld\"");
check_parse(R"("tab\there")", "\"tab\there\"");
check_parse(R"("back\\slash")", "\"back\\\\slash\"");
check_parse(R"("what\qever")", "\"whatqever\"");
// full int64 range
check_parse("4294967296", "4294967296");
check_parse("9223372036854775807", "9223372036854775807");
check_parse("-9223372036854775808", "-9223372036854775808");
// \r\n is whitespace, too
check_parse("(foo\r\nbar)", "(foo bar)");
}
static void
test_parser_fail()
{
g_assert_false(!!Sexp::parse("\""));
g_assert_false(!!Sexp::parse("123abc"));
g_assert_false(!!Sexp::parse("("));
g_assert_false(!!Sexp::parse(")"));
g_assert_false(!!Sexp::parse("(hello (boo))))"));
// numbers that don't fit an int64, or aren't numbers at all
g_assert_false(!!Sexp::parse("99999999999999999999"));
g_assert_false(!!Sexp::parse("-"));
// trailing backslash
g_assert_false(!!Sexp::parse(R"("foo\)"));
// deeply-nested input gives an error, not a stack overflow
std::string deep(100000, '(');
deep += "a";
deep.append(100000, ')');
g_assert_false(!!Sexp::parse(deep));
g_assert_true(Sexp::type_name(static_cast<Sexp::Type>(-1)) == "<error>");
}
int
main(int argc, char* argv[])
try {
mu_test_init(&argc, &argv);
g_test_add_func("/sexp/list", test_list);
g_test_add_func("/sexp/string", test_string);
g_test_add_func("/sexp/number", test_number);
g_test_add_func("/sexp/symbol", test_symbol);
g_test_add_func("/sexp/multi", test_multi);
g_test_add_func("/sexp/add", test_add);
g_test_add_func("/sexp/add-multi", test_add_multi);
g_test_add_func("/sexp/plist", test_plist);
g_test_add_func("/sexp/parser", test_parser);
g_test_add_func("/sexp/parser-fail", test_parser_fail);
return g_test_run();
} catch (const std::runtime_error& re) {
mu_printerrln("{}", re.what());
return 1;
}
#endif /*BUILD_TESTS*/