/* ** Copyright (C) 2022-2024 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-sexp.hh" #include "mu-utils.hh" #include using namespace Mu; /* avoid unbounded recursion (i.e., stack overflow) for pathological input */ constexpr size_t MaxDepth{1024}; template static Mu::Error parsing_error(size_t pos, fmt::format_string frm, T&&... args) { const auto&& msg{fmt::format(frm, std::forward(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 parse(const std::string& expr, size_t& pos, size_t depth); static Result 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 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 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: 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 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: |: 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 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 { 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::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(-1)) == ""); } 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*/