diff --git a/.gitignore b/.gitignore index 62e23bc8..3e76d235 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,4 @@ perf.data.old mu-*-coverage mu*tar.xz compile_commands.json +/lib/utils/test-sexp diff --git a/NEWS.org b/NEWS.org index bf8cd180..372176f1 100644 --- a/NEWS.org +++ b/NEWS.org @@ -1,5 +1,5 @@ #+STARTUP:showall -* NEWS (user visible changes) +* NEWS (user visible changes & bigger non-visible ones) * 1.5.x (unreleased, development version) @@ -14,6 +14,9 @@ - Optionally provide readline support for the mu server (when in tty-mode) + - Reworked the way mu generates s-expressions for mu4e; they are created + programmatically now instead of through string building. + *** mu4e - Include maildir-shortcuts in the main-view with overall/unread counts, diff --git a/configure.ac b/configure.ac index 4298cd91..d166955b 100644 --- a/configure.ac +++ b/configure.ac @@ -15,7 +15,7 @@ ## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. AC_PREREQ([2.68]) -AC_INIT([mu],[1.5.1],[https://github.com/djcb/mu/issues],[mu]) +AC_INIT([mu],[1.5.2],[https://github.com/djcb/mu/issues],[mu]) AC_COPYRIGHT([Copyright (C) 2008-2020 Dirk-Jan C. Binnema]) AC_CONFIG_HEADERS([config.h]) AC_CONFIG_SRCDIR([mu/mu.cc]) diff --git a/lib/Makefile.am b/lib/Makefile.am index f4aa4289..8f2fa27e 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -100,7 +100,7 @@ libmu_la_SOURCES= \ mu-msg-prio.c \ mu-msg-prio.h \ mu-msg-priv.h \ - mu-msg-sexp.c \ + mu-msg-sexp.cc \ mu-msg.c \ mu-msg.h \ mu-msg.h \ diff --git a/lib/mu-msg-sexp.c b/lib/mu-msg-sexp.c deleted file mode 100644 index 0d1ab466..00000000 --- a/lib/mu-msg-sexp.c +++ /dev/null @@ -1,634 +0,0 @@ -/* -** Copyright (C) 2011-2017 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 -#include - -#include "utils/mu-str.h" -#include "mu-msg.h" -#include "mu-msg-iter.h" -#include "mu-msg-part.h" -#include "mu-maildir.h" - -static void -append_sexp_attr_list (GString *gstr, const char* elm, const GSList *lst) -{ - const GSList *cur; - - if (!lst) - return; /* empty list, don't include */ - - g_string_append_printf (gstr, "\t:%s ( ", elm); - - for (cur = lst; cur; cur = g_slist_next(cur)) { - char *str; - str = mu_str_escape_c_literal - ((const gchar*)cur->data, TRUE); - g_string_append_printf (gstr, "%s ", str); - g_free (str); - } - - g_string_append (gstr, ")\n"); -} - -static void -append_sexp_attr (GString *gstr, const char* elm, const char *str) -{ - gchar *esc, *utf8, *cur; - - if (!str || strlen(str) == 0) - return; /* empty: don't include */ - - - utf8 = mu_str_utf8ify (str); - - for (cur = utf8; *cur; ++cur) - if (iscntrl(*cur)) - *cur = ' '; - - esc = mu_str_escape_c_literal (utf8, TRUE); - g_free (utf8); - - g_string_append_printf (gstr, "\t:%s %s\n", elm, esc); - g_free (esc); -} - -static void -append_sexp_body_attr (GString *gstr, const char* elm, const char *str) -{ - gchar *esc; - - if (!str || strlen(str) == 0) - return; /* empty: don't include */ - - esc = mu_str_escape_c_literal (str, TRUE); - - g_string_append_printf (gstr, "\t:%s %s\n", elm, esc); - g_free (esc); -} - -struct _ContactData { - gboolean from, to, cc, bcc, reply_to; - GString *gstr; - MuMsgContactType prev_ctype; -}; -typedef struct _ContactData ContactData; - -static gchar* -get_name_email_pair (MuMsgContact *c) -{ - gchar *name, *email, *pair; - - name = (char*)mu_msg_contact_name(c); - email = (char*)mu_msg_contact_email(c); - - name = name ? mu_str_escape_c_literal (name, TRUE) : NULL; - email = email ? mu_str_escape_c_literal (email, TRUE) : NULL; - - pair = g_strdup_printf ("(%s . %s)", - name ? name : "nil", - email ? email : "nil"); - g_free (name); - g_free (email); - - return pair; -} - - -static void -add_prefix_maybe (GString *gstr, gboolean *field, const char *prefix) -{ - /* if there's nothing in the field yet, add the prefix */ - if (!*field) - g_string_append (gstr, prefix); - - *field = TRUE; -} - -static gboolean -each_contact (MuMsgContact *c, ContactData *cdata) -{ - char *pair; - MuMsgContactType ctype; - - ctype = mu_msg_contact_type (c); - - /* if the current type is not the previous type, close the - * previous first */ - if (cdata->prev_ctype != ctype && cdata->prev_ctype != (unsigned)-1) - g_string_append (cdata->gstr, ")\n"); - - switch (ctype) { - - case MU_MSG_CONTACT_TYPE_FROM: - add_prefix_maybe (cdata->gstr, &cdata->from, "\t:from ("); - break; - case MU_MSG_CONTACT_TYPE_TO: - add_prefix_maybe (cdata->gstr, &cdata->to, "\t:to ("); - break; - case MU_MSG_CONTACT_TYPE_CC: - add_prefix_maybe (cdata->gstr, &cdata->cc, "\t:cc ("); - break; - case MU_MSG_CONTACT_TYPE_BCC: - add_prefix_maybe (cdata->gstr, &cdata->bcc, "\t:bcc ("); - break; - case MU_MSG_CONTACT_TYPE_REPLY_TO: - add_prefix_maybe (cdata->gstr, &cdata->reply_to, - "\t:reply-to ("); - break; - default: g_return_val_if_reached (FALSE); - } - - cdata->prev_ctype = ctype; - - pair = get_name_email_pair (c); - g_string_append (cdata->gstr, pair); - g_free (pair); - - return TRUE; -} - -static void -maybe_append_list_post (GString *gstr, MuMsg *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; - GRegex *rx; - const char* list_post; - - list_post = mu_msg_get_header (msg, "List-Post"); - if (!list_post) - return; - - rx = g_regex_new ("?", G_REGEX_CASELESS, 0, NULL); - g_return_if_fail(rx); - - if (g_regex_match (rx, list_post, 0, &minfo)) { - char *addr; - addr = g_match_info_fetch (minfo, 1); - g_string_append_printf (gstr,"\t:list-post ((nil . \"%s\"))\n", addr); - g_free(addr); - } - - g_match_info_free (minfo); - g_regex_unref (rx); -} - - - -static void -append_sexp_contacts (GString *gstr, MuMsg *msg) -{ - ContactData cdata; - - cdata.from = cdata.to = cdata.cc = cdata.bcc - = cdata.reply_to = FALSE; - cdata.gstr = gstr; - cdata.prev_ctype = (unsigned)-1; - - mu_msg_contact_foreach (msg, (MuMsgContactForeachFunc)each_contact, - &cdata); - if (cdata.from || cdata.to || cdata.cc || cdata.bcc || cdata.reply_to) - gstr = g_string_append (gstr, ")\n"); - - maybe_append_list_post (gstr, msg); -} - -struct _FlagData { - char *flagstr; - MuFlags msgflags; -}; -typedef struct _FlagData FlagData; - -static void -each_flag (MuFlags flag, FlagData *fdata) -{ - if (!(flag & fdata->msgflags)) - return; - - if (!fdata->flagstr) - fdata->flagstr = g_strdup (mu_flag_name(flag)); - else { - gchar *tmp; - tmp = g_strconcat (fdata->flagstr, " ", - mu_flag_name(flag), NULL); - g_free (fdata->flagstr); - fdata->flagstr = tmp; - } -} - -static void -append_sexp_flags (GString *gstr, MuMsg *msg) -{ - FlagData fdata; - - fdata.msgflags = mu_msg_get_flags (msg); - fdata.flagstr = NULL; - - mu_flags_foreach ((MuFlagsForeachFunc)each_flag, &fdata); - if (fdata.flagstr) - g_string_append_printf (gstr, "\t:flags (%s)\n", - fdata.flagstr); - g_free (fdata.flagstr); -} - -static char* -get_temp_file (MuMsg *msg, MuMsgOptions opts, unsigned index) -{ - char *path; - GError *err; - - err = NULL; - path = mu_msg_part_get_cache_path (msg, opts, index, &err); - if (!path) - goto errexit; - - if (!mu_msg_part_save (msg, opts, path, index, &err)) - goto errexit; - - return path; - -errexit: - g_warning ("failed to save mime part: %s", - err->message ? err->message : "something went wrong"); - g_clear_error (&err); - g_free (path); - return NULL; -} - - -static gchar* -get_temp_file_maybe (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts) -{ - char *tmp, *tmpfile; - - opts |= MU_MSG_OPTION_USE_EXISTING; - - if (!(opts & MU_MSG_OPTION_EXTRACT_IMAGES) || - g_ascii_strcasecmp (part->type, "image") != 0) - return NULL; - - tmp = get_temp_file (msg, opts, part->index); - if (!tmp) - return NULL; - - tmpfile = mu_str_escape_c_literal (tmp, TRUE); - g_free (tmp); - return tmpfile; -} - - -struct _PartInfo { - char *parts; - MuMsgOptions opts; -}; -typedef struct _PartInfo PartInfo; - -static char* -sig_verdict (MuMsgPart *mpart) -{ - char *signers, *s; - const char *verdict; - MuMsgPartSigStatusReport *report; - - report = mpart->sig_status_report; - if (!report) - return g_strdup (""); - - switch (report->verdict) { - case MU_MSG_PART_SIG_STATUS_GOOD: - verdict = ":signature verified"; - break; - case MU_MSG_PART_SIG_STATUS_BAD: - verdict = ":signature bad"; - break; - case MU_MSG_PART_SIG_STATUS_ERROR: - verdict = ":signature unverified"; - break; - default: - verdict = ""; - break; - } - - if (!report->signers) - return g_strdup (verdict); - - signers = mu_str_escape_c_literal (report->signers, TRUE); - s = g_strdup_printf ("%s :signers %s", verdict, signers); - g_free (signers); - - return s; -} - -static const char* -dec_verdict (MuMsgPart *mpart) -{ - MuMsgPartType ptype; - - ptype = mpart->part_type; - - if (ptype & MU_MSG_PART_TYPE_DECRYPTED) - return ":decryption succeeded"; - else if (ptype & MU_MSG_PART_TYPE_ENCRYPTED) - return ":decryption failed"; - else - return ""; -} - - -static gchar * -get_part_type_string (MuMsgPartType ptype) -{ - GString *gstr; - unsigned u; - struct PartTypes { - MuMsgPartType ptype; - const char* name; - } ptypes[] = { - { MU_MSG_PART_TYPE_LEAF, "leaf" }, - { MU_MSG_PART_TYPE_MESSAGE, "message" }, - { MU_MSG_PART_TYPE_INLINE, "inline" }, - { MU_MSG_PART_TYPE_ATTACHMENT, "attachment" }, - { MU_MSG_PART_TYPE_SIGNED, "signed" }, - { MU_MSG_PART_TYPE_ENCRYPTED, "encrypted" } - }; - - gstr = g_string_sized_new (100); /* more than enough */ - gstr = g_string_append_c (gstr, '('); - - for (u = 0; u!= G_N_ELEMENTS(ptypes); ++u) { - if (ptype & ptypes[u].ptype) { - if (gstr->len > 1) - gstr = g_string_append_c (gstr, ' '); - gstr = g_string_append (gstr, ptypes[u].name); - } - } - - gstr = g_string_append_c (gstr, ')'); - - return g_string_free (gstr, FALSE); -} - - -static void -each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) -{ - char *name, *encname, *tmp, *parttype; - char *tmpfile, *cidesc, *verdict; - const char *cid; - - name = mu_msg_part_get_filename (part, TRUE); - encname = name ? - mu_str_escape_c_literal(name, TRUE) : - g_strdup("\"noname\""); - g_free (name); - - tmpfile = get_temp_file_maybe (msg, part, pinfo->opts); - parttype = get_part_type_string (part->part_type); - verdict = sig_verdict (part); - - cid = mu_msg_part_get_content_id(part); - cidesc = cid ? mu_str_escape_c_literal(cid, TRUE) : NULL; - - tmp = g_strdup_printf - ("%s(:index %d :name %s :mime-type \"%s/%s\"%s%s " - ":type %s " - ":attachment %s %s%s :size %i %s %s)", - pinfo->parts ? pinfo->parts: "", - part->index, - encname, - part->type ? part->type : "application", - part->subtype ? part->subtype : "octet-stream", - tmpfile ? " :temp" : "", tmpfile ? tmpfile : "", - parttype, - mu_msg_part_maybe_attachment (part) ? "t" : "nil", - cidesc ? " :cid" : "", cidesc ? cidesc : "", - (int)part->size, - verdict, - dec_verdict (part)); - - g_free (encname); - g_free (tmpfile); - g_free (parttype); - g_free (verdict); - g_free (cidesc); - - g_free (pinfo->parts); - pinfo->parts = tmp; -} - - -static void -append_sexp_parts (GString *gstr, MuMsg *msg, MuMsgOptions opts) -{ - PartInfo pinfo; - - pinfo.parts = NULL; - pinfo.opts = opts; - - if (!mu_msg_part_foreach (msg, opts, (MuMsgPartForeachFunc)each_part, - &pinfo)) { - /* do nothing */ - } else if (pinfo.parts) { - g_string_append_printf (gstr, "\t:parts (%s)\n", pinfo.parts); - g_free (pinfo.parts); - } -} - -static void -append_sexp_thread_info (GString *gstr, const MuMsgIterThreadInfo *ti) -{ - g_string_append_printf - (gstr, "\t:thread (:path \"%s\" :level %u%s%s%s%s%s)\n", - ti->threadpath, - ti->level, - ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD ? - " :first-child t" : "", - ti->prop & MU_MSG_ITER_THREAD_PROP_LAST_CHILD ? - " :last-child t" : "", - ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT ? - " :empty-parent t" : "", - ti->prop & MU_MSG_ITER_THREAD_PROP_DUP ? - " :duplicate t" : "", - ti->prop & MU_MSG_ITER_THREAD_PROP_HAS_CHILD ? - " :has-child t" : ""); -} - -static void -append_sexp_param (GString *gstr, const GSList *param) -{ - for (;param; param = g_slist_next (param)) { - const char *str; - char *key, *value; - - str = param->data; - key = str ? mu_str_escape_c_literal (str, FALSE) : g_strdup (""); - - param = g_slist_next (param); - str = param->data; - value = str ? mu_str_escape_c_literal (str, FALSE) : g_strdup (""); - - g_string_append_printf (gstr, "(\"%s\" . \"%s\")", key, value); - g_free (key); - g_free (value); - - if (param->next) - g_string_append_c (gstr, ' '); - } -} - -static void -append_message_file_parts (GString *gstr, MuMsg *msg, MuMsgOptions opts) -{ - const char *str; - GError *err; - const GSList *params; - - err = NULL; - - if (!mu_msg_load_msg_file (msg, &err)) { - g_warning ("failed to load message file: %s", - err ? err->message : "some error occurred"); - g_clear_error (&err); - return; - } - - append_sexp_parts (gstr, msg, opts); - append_sexp_contacts (gstr, msg); - - /* add the user-agent / x-mailer */ - str = mu_msg_get_header (msg, "User-Agent"); - if (str || (str = mu_msg_get_header (msg, "X-Mailer"))) - append_sexp_attr (gstr, "user-agent", str); - - params = mu_msg_get_body_text_content_type_parameters (msg, opts); - if (params) { - g_string_append_printf (gstr, "\t:body-txt-params ("); - append_sexp_param (gstr, params); - g_string_append_printf (gstr, ")\n"); - } - - append_sexp_body_attr (gstr, "body-txt", - mu_msg_get_body_text(msg, opts)); - append_sexp_body_attr (gstr, "body-html", - mu_msg_get_body_html(msg, opts)); -} - -static void -append_sexp_date_and_size (GString *gstr, MuMsg *msg) -{ - time_t t; - size_t s; - - t = mu_msg_get_date (msg); - if (t == (time_t)-1) /* invalid date? */ - t = 0; - - s = mu_msg_get_size (msg); - if (s == (size_t)-1) /* invalid size? */ - s = 0; - - g_string_append_printf - (gstr, - "\t:date (%d %u 0)\n\t:size %u\n", - (unsigned)(t >> 16), - (unsigned)(t & 0xffff), - (unsigned)s); -} - - -static void -append_sexp_tags (GString *gstr, MuMsg *msg) -{ - const GSList *tags, *t; - gchar *tagesc; - GString *tagstr = g_string_new(""); - - tags = mu_msg_get_tags (msg); - - for(t = tags; t; t = t->next) { - if (t != tags) - g_string_append(tagstr, " "); - - tagesc = mu_str_escape_c_literal((const gchar *)t->data, TRUE); - g_string_append(tagstr, tagesc); - - g_free(tagesc); - } - - if (tagstr->len > 0) - g_string_append_printf (gstr, "\t:tags (%s)\n", - tagstr->str); - g_string_free (tagstr, TRUE); -} - -char* -mu_msg_to_sexp (MuMsg *msg, unsigned docid, const MuMsgIterThreadInfo *ti, - MuMsgOptions opts) -{ - GString *gstr; - - g_return_val_if_fail (msg, NULL); - g_return_val_if_fail (!((opts & MU_MSG_OPTION_HEADERS_ONLY) && - (opts & MU_MSG_OPTION_EXTRACT_IMAGES)),NULL); - gstr = g_string_sized_new - ((opts & MU_MSG_OPTION_HEADERS_ONLY) ? 1024 : 8192); - - if (docid == 0) - g_string_append (gstr, "(\n"); - else - g_string_append_printf (gstr, "(\n\t:docid %u\n", docid); - - if (ti) - append_sexp_thread_info (gstr, ti); - - append_sexp_attr (gstr, "subject", mu_msg_get_subject (msg)); - - /* in the no-headers-only case (see below) we get a more complete list - * of contacts, so no need to get them here if that's the case */ - if (opts & MU_MSG_OPTION_HEADERS_ONLY) - append_sexp_contacts (gstr, msg); - - append_sexp_date_and_size (gstr, msg); - - append_sexp_attr (gstr, "message-id", mu_msg_get_msgid (msg)); - append_sexp_attr (gstr, "mailing-list", - mu_msg_get_mailing_list (msg)); - append_sexp_attr (gstr, "path", mu_msg_get_path (msg)); - append_sexp_attr (gstr, "maildir", mu_msg_get_maildir (msg)); - g_string_append_printf (gstr, "\t:priority %s\n", - mu_msg_prio_name(mu_msg_get_prio(msg))); - append_sexp_flags (gstr, msg); - append_sexp_tags (gstr, msg); - - append_sexp_attr_list (gstr, "references", - mu_msg_get_references (msg)); - append_sexp_attr (gstr, "in-reply-to", - mu_msg_get_header (msg, "In-Reply-To")); - - /* headers are retrieved from the database, views from the - * message file file attr things can only be gotten from the - * file (ie., mu view), not from the database (mu find). */ - if (!(opts & MU_MSG_OPTION_HEADERS_ONLY)) - append_message_file_parts (gstr, msg, opts); - - g_string_append (gstr, ")\n"); - return g_string_free (gstr, FALSE); -} diff --git a/lib/mu-msg-sexp.cc b/lib/mu-msg-sexp.cc new file mode 100644 index 00000000..8b659a46 --- /dev/null +++ b/lib/mu-msg-sexp.cc @@ -0,0 +1,470 @@ +/* +** Copyright (C) 2011-2020 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 +#include + +#include "utils/mu-str.h" +#include "mu-msg.h" +#include "mu-msg-iter.h" +#include "mu-msg-part.h" +#include "mu-maildir.h" + +using namespace Mu; +using namespace Sexp; + +static void +add_prop_nonempty (Node::Seq& items, const char* elm, const GSList *str_lst) +{ + Node::Seq elms; + while (str_lst) { + elms.add((const char*)str_lst->data); + str_lst = g_slist_next(str_lst); + } + + if (!elms.empty()) + items.add_prop(elm, elms); +} + +static void +add_prop_nonempty (Node::Seq& items, const char* elm, Node::Seq&& seq) +{ + if (!seq.empty()) + items.add_prop(elm, std::move(seq)); +} + +static void +add_prop_nonempty (Node::Seq& items, const char* name, const char *str) +{ + if (str && str[0]) + items.add_prop(name, str); +} + +static void +add_prop_symbol (Node::Seq& items, const char* name, const char *str) +{ + items.add_prop(name, Node::make_symbol(str)); +} + +static Node +make_contact_node (MuMsgContact *c) +{ + // a cons-pair...perhaps make this a plist too? + + Node::Seq contact; + if (mu_msg_contact_name(c)) + contact.add (mu_msg_contact_name(c)); + else + contact.add (Node::make_symbol("nil")); + + contact.add(Node::make_symbol(".")); + contact.add(mu_msg_contact_email(c)); + + return Node::make_list(std::move(contact)); +} + +static void +add_list_post (Node::Seq& items, MuMsg *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; + GRegex *rx; + const char* list_post; + + list_post = mu_msg_get_header (msg, "List-Post"); + if (!list_post) + return; + + rx = g_regex_new ("?", G_REGEX_CASELESS, + (GRegexMatchFlags)0, NULL); + g_return_if_fail(rx); + + Node::Seq addrs; + if (g_regex_match (rx, list_post, (GRegexMatchFlags)0, &minfo)) { + auto address = (char*)g_match_info_fetch (minfo, 1); + MuMsgContact contact{NULL, address}; + addrs.add(make_contact_node(&contact)); + items.add_prop(":list-post", std::move(addrs)); + g_free(address); + } + + g_match_info_free (minfo); + g_regex_unref (rx); +} + + +struct _ContactData { + Node::Seq from, to, cc, bcc, reply_to; +}; +typedef struct _ContactData ContactData; + + +static gboolean +each_contact (MuMsgContact *c, ContactData *cdata) +{ + switch (mu_msg_contact_type (c)) { + + case MU_MSG_CONTACT_TYPE_FROM: + cdata->from.add(make_contact_node(c)); + break; + case MU_MSG_CONTACT_TYPE_TO: + cdata->to.add(make_contact_node(c)); + break; + case MU_MSG_CONTACT_TYPE_CC: + cdata->cc.add(make_contact_node(c)); + break; + case MU_MSG_CONTACT_TYPE_BCC: + cdata->bcc.add(make_contact_node(c)); + break; + case MU_MSG_CONTACT_TYPE_REPLY_TO: + cdata->reply_to.add(make_contact_node(c)); + break; + default: + g_return_val_if_reached (FALSE); + return FALSE; + } + return TRUE; +} + + +static void +add_contacts (Node::Seq& items, MuMsg *msg) +{ + ContactData cdata{}; + mu_msg_contact_foreach (msg, (MuMsgContactForeachFunc)each_contact, + &cdata); + + add_prop_nonempty(items, ":from", std::move(cdata.from)); + add_prop_nonempty(items, ":to", std::move(cdata.to)); + add_prop_nonempty(items, ":cc", std::move(cdata.cc)); + add_prop_nonempty(items, ":bcc", std::move(cdata.bcc)); + add_prop_nonempty(items, ":reply-to", std::move(cdata.reply_to)); + + add_list_post (items, msg); +} + +typedef struct { + Node::Seq flagseq; + MuFlags msgflags; +} FlagData; + +static void +each_flag (MuFlags flag, FlagData *fdata) +{ + if (flag & fdata->msgflags) + fdata->flagseq.add(Node::make_symbol(mu_flag_name(flag))); +} + +static void +add_flags (Node::Seq& items, MuMsg *msg) +{ + FlagData fdata{}; + fdata.msgflags = mu_msg_get_flags (msg); + + mu_flags_foreach ((MuFlagsForeachFunc)each_flag, &fdata); + add_prop_nonempty(items, ":flags", std::move(fdata.flagseq)); +} + +static char* +get_temp_file (MuMsg *msg, MuMsgOptions opts, unsigned index) +{ + char *path; + GError *err; + + err = NULL; + path = mu_msg_part_get_cache_path (msg, opts, index, &err); + if (!path) + goto errexit; + + if (!mu_msg_part_save (msg, opts, path, index, &err)) + goto errexit; + + return path; + +errexit: + g_warning ("failed to save mime part: %s", + err->message ? err->message : "something went wrong"); + g_clear_error (&err); + g_free (path); + + return NULL; +} + +static gchar* +get_temp_file_maybe (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts) +{ + opts = (MuMsgOptions)((int)opts | (int)MU_MSG_OPTION_USE_EXISTING); + if (!(opts & MU_MSG_OPTION_EXTRACT_IMAGES) || + g_ascii_strcasecmp (part->type, "image") != 0) + return NULL; + else + return get_temp_file (msg, opts, part->index); +} + +struct PartInfo { + Node::Seq parts; + MuMsgOptions opts; +}; + +static void +sig_verdict (Node::Seq& partseq, MuMsgPart *mpart) +{ + MuMsgPartSigStatusReport *report = mpart->sig_status_report; + if (!report) + return; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch (report->verdict) { + case MU_MSG_PART_SIG_STATUS_GOOD: + add_prop_symbol (partseq, ":signature", "verified"); + break; + case MU_MSG_PART_SIG_STATUS_BAD: + add_prop_symbol (partseq, ":signature", "bad"); + break; + case MU_MSG_PART_SIG_STATUS_ERROR: + add_prop_symbol (partseq, ":signature", "unverified"); + break; + default: + break; + } +#pragma GCC diagnostic pop + + add_prop_nonempty (partseq, ":signers", report->signers); +} + +static void +dec_verdict (Node::Seq& partseq, MuMsgPart *mpart) +{ + if (mpart->part_type & MU_MSG_PART_TYPE_DECRYPTED) + partseq.add_prop(":decryption", Node::make_symbol("succeeded")); + else if (mpart->part_type & MU_MSG_PART_TYPE_ENCRYPTED) + partseq.add_prop(":decryption", Node::make_symbol("failed")); +} + + +static Node::Seq +make_part_types (MuMsgPartType ptype) +{ + struct PartTypes { + MuMsgPartType ptype; + const char* name; + } ptypes[] = { + { MU_MSG_PART_TYPE_LEAF, "leaf" }, + { MU_MSG_PART_TYPE_MESSAGE, "message" }, + { MU_MSG_PART_TYPE_INLINE, "inline" }, + { MU_MSG_PART_TYPE_ATTACHMENT, "attachment" }, + { MU_MSG_PART_TYPE_SIGNED, "signed" }, + { MU_MSG_PART_TYPE_ENCRYPTED, "encrypted" } + }; + + Node::Seq seq; + for (auto u = 0U; u!= G_N_ELEMENTS(ptypes); ++u) + if (ptype & ptypes[u].ptype) + seq.add(Node::make_symbol(ptypes[u].name)); + + return seq; +} + + +static void +each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) +{ + auto mimetype = format("%s/%s", + part->type ? part->type : "application", + part->subtype ? part->subtype : "octet-stream"); + auto maybe_attach = Node::make_symbol(mu_msg_part_maybe_attachment (part) ? + "t" : "nil"); + + Node::Seq partseq; + + partseq.add_prop(":index", part->index); + partseq.add_prop(":mime-type", mimetype); + partseq.add_prop(":size", part->size); + + dec_verdict (partseq, part); + sig_verdict (partseq, part); + + add_prop_nonempty(partseq, ":type", make_part_types(part->part_type)); + + char *fname = mu_msg_part_get_filename (part, TRUE); + add_prop_nonempty(partseq, ":name", fname); + g_free (fname); + + if (mu_msg_part_maybe_attachment (part)) + add_prop_symbol (partseq, ":attachment", "t"); + + add_prop_nonempty (partseq, ":cid", mu_msg_part_get_content_id(part)); + + char *tempfile = get_temp_file_maybe (msg, part, pinfo->opts); + add_prop_nonempty (partseq, ":temp", tempfile); + g_free (tempfile); + + pinfo->parts.add(Node::make_list(std::move(partseq))); +} + + +static void +add_parts (Node::Seq& items, MuMsg *msg, MuMsgOptions opts) +{ + PartInfo pinfo; + pinfo.opts = opts; + + if (mu_msg_part_foreach (msg, opts, (MuMsgPartForeachFunc)each_part, &pinfo)) + add_prop_nonempty (items, ":parts", std::move(pinfo.parts)); +} + +static void +add_thread_info (Node::Seq& items, const MuMsgIterThreadInfo *ti) +{ + Node::Seq info; + + info.add_prop(":path", ti->threadpath); + info.add_prop(":level", ti->level); + + if (ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD) + add_prop_symbol(info, ":first-child", "t"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_LAST_CHILD) + add_prop_symbol(info, ":last-child", "t"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT) + add_prop_symbol(info, ":empty-parent", "t"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_DUP) + add_prop_symbol(info, ":duplicate", "t"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_HAS_CHILD) + add_prop_symbol(info, ":has-child", "t"); + + items.add_prop(":thread", std::move(info)); +} + + +static void +add_message_file_parts (Node::Seq& items, MuMsg *msg, MuMsgOptions opts) +{ + GError *err{NULL}; + if (!mu_msg_load_msg_file (msg, &err)) { + g_warning ("failed to load message file: %s", + err ? err->message : "some error occurred"); + g_clear_error (&err); + return; + } + + add_parts (items, msg, opts); + add_contacts (items, msg); + + /* add the user-agent / x-mailer */ + auto str = mu_msg_get_header (msg, "User-Agent"); + if (!str) + str = mu_msg_get_header (msg, "X-Mailer"); + + add_prop_nonempty (items, ":user-agent", str); + add_prop_nonempty (items, ":body-txt-params", + mu_msg_get_body_text_content_type_parameters (msg, opts)); + + add_prop_nonempty (items, ":body-txt", mu_msg_get_body_text(msg, opts)); + add_prop_nonempty (items, ":body-html", mu_msg_get_body_html(msg, opts)); +} + +static void +add_date_and_size (Node::Seq& items, MuMsg *msg) +{ + auto t = mu_msg_get_date (msg); + if (t == (time_t)-1) /* invalid date? */ + t = 0; + + Node::Seq dseq; + dseq.add((unsigned)(t >> 16)); + dseq.add((unsigned)(t && 0xffff)); + dseq.add(0); + + items.add_prop(":date", std::move(dseq)); + + auto s = mu_msg_get_size (msg); + if (s == (size_t)-1) /* invalid size? */ + s = 0; + + items.add_prop(":size", s); +} + + +static void +add_tags (Node::Seq& items, MuMsg *msg) +{ + Node::Seq tagseq; + for (auto tags = mu_msg_get_tags(msg); tags; tags = g_slist_next(tags)) + tagseq.add((const char*)tags->data); + + add_prop_nonempty (items, ":tags", std::move(tagseq)); +} + + +Mu::Sexp::Node +Mu::msg_to_sexp (MuMsg *msg, unsigned docid, + const struct _MuMsgIterThreadInfo *ti, + MuMsgOptions opts) +{ + g_return_val_if_fail (msg, Sexp::Node::make("error")); + g_return_val_if_fail (!((opts & MU_MSG_OPTION_HEADERS_ONLY) && + (opts & MU_MSG_OPTION_EXTRACT_IMAGES)), + Sexp::Node::make("error")); + Node::Seq items; + + if (docid != 0) + items.add_prop(":docid", docid); + + if (ti) + add_thread_info (items, ti); + + add_prop_nonempty (items, ":subject", mu_msg_get_subject (msg)); + add_prop_nonempty (items, ":message-id", mu_msg_get_msgid (msg)); + add_prop_nonempty (items, ":mailing-list", mu_msg_get_mailing_list (msg)); + add_prop_nonempty (items, ":path", mu_msg_get_path (msg)); + add_prop_nonempty (items, ":maildir", mu_msg_get_maildir (msg)); + + items.add_prop(":priority", Node::make_symbol(mu_msg_prio_name(mu_msg_get_prio(msg)))); + + /* in the no-headers-only case (see below) we get a more complete list of contacts, so no + * need to get them here if that's the case */ + if (opts & MU_MSG_OPTION_HEADERS_ONLY) + add_contacts (items, msg); + + add_prop_nonempty (items, ":references", mu_msg_get_references (msg)); + add_prop_nonempty (items, ":in-reply-to", mu_msg_get_header (msg, "In-Reply-To")); + + add_date_and_size (items, msg); + add_flags (items, msg); + add_tags (items, msg); + + /* headers are retrieved from the database, views from the + * message file file attr things can only be gotten from the + * file (ie., mu view), not from the database (mu find). */ + if (!(opts & MU_MSG_OPTION_HEADERS_ONLY)) + add_message_file_parts (items, msg, opts); + + return Node::make_list(std::move(items)); +} + + +char* +mu_msg_to_sexp (MuMsg *msg, unsigned docid, const MuMsgIterThreadInfo *ti, + MuMsgOptions opts) +{ + return g_strdup (Mu::msg_to_sexp (msg, docid, ti, opts) + .to_string().c_str()); +} diff --git a/lib/mu-msg.h b/lib/mu-msg.h index e4842aa2..2fede752 100644 --- a/lib/mu-msg.h +++ b/lib/mu-msg.h @@ -1,3 +1,4 @@ + /* -*- mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ** ** Copyright (C) 2010-2013 Dirk-Jan C. Binnema @@ -32,7 +33,7 @@ struct _MuMsg; typedef struct _MuMsg MuMsg; /* options for various functions */ -enum _MuMsgOptions { +typedef enum { MU_MSG_OPTION_NONE = 0, /* 1 << 0 is still free! */ @@ -59,10 +60,7 @@ enum _MuMsgOptions { /* recurse into submessages */ MU_MSG_OPTION_RECURSE_RFC822 = 1 << 11 -}; -typedef enum _MuMsgOptions MuMsgOptions; - - +} MuMsgOptions; /** * create a new MuMsg* instance which parses a message and provides @@ -435,59 +433,6 @@ int mu_msg_cmp (MuMsg *m1, MuMsg *m2, MuMsgFieldId mfid); gboolean mu_msg_is_readable (MuMsg *self); -struct _MuMsgIterThreadInfo; - - -/** - * convert the msg to a Lisp symbolic expression (for further processing in - * e.g. emacs) - * - * @param msg a valid message - * @param docid the docid for this message, or 0 - * @param ti thread info for the current message, or NULL - * @param opts, bitwise OR'ed; - * - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be - * obtained from the database (this is much faster if the MuMsg is - * database-backed, so no file needs to be opened) - * - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary - * files and include links to those in the sexp - * and for message parts: - * MU_MSG_OPTION_CHECK_SIGNATURES: check signatures - * MU_MSG_OPTION_AUTO_RETRIEVE_KEY: attempt to retrieve keys online - * MU_MSG_OPTION_USE_AGENT: attempt to use GPG-agent - * MU_MSG_OPTION_USE_PKCS7: attempt to use PKCS (instead of gpg) - * - * @return a string with the sexp (free with g_free) or NULL in case of error - */ -char* mu_msg_to_sexp (MuMsg *msg, unsigned docid, - const struct _MuMsgIterThreadInfo *ti, - MuMsgOptions ops) - G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; - -#ifdef HAVE_JSON_GLIB - -struct _JsonNode; /* forward declaration */ - -/** - * convert the msg to json - * - * @param msg a valid message - * @param docid the docid for this message, or 0 - * @param ti thread info for the current message, or NULL - * @param opts, bitwise OR'ed; - * - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be - * obtained from the database (this is much faster if the MuMsg is - * database-backed, so no file needs to be opened) - * - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary - * files and include links to those in the sexp - * - * @return a string with the sexp (free with g_free) or NULL in case of error - */ -struct _JsonNode* mu_msg_to_json (MuMsg *msg, unsigned docid, - const struct _MuMsgIterThreadInfo *ti, - MuMsgOptions ops) G_GNUC_WARN_UNUSED_RESULT; -#endif /*HAVE_JSON_GLIB*/ - /** * move a message to another maildir; note that this does _not_ update * the database @@ -544,14 +489,13 @@ typedef guint MuMsgContactType; #define mu_msg_contact_type_is_valid(MCT)\ ((MCT) < MU_MSG_CONTACT_TYPE_NUM) -struct _MuMsgContact { +typedef struct { const char *name; /**< Foo Bar */ const char *email; /**< foo@bar.cuux */ const char *full_address; /**< Foo Bar */ MuMsgContactType type; /**< MU_MSG_CONTACT_TYPE_{ TO, CC, BCC, FROM, REPLY_TO} */ -}; -typedef struct _MuMsgContact MuMsgContact; +} MuMsgContact; /** @@ -649,6 +593,97 @@ char* mu_str_flags (MuFlags flags) G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; +struct _MuMsgIterThreadInfo; + +#ifdef HAVE_JSON_GLIB + +struct _JsonNode; /* forward declaration */ + +/** + * convert the msg to json + * + * @param msg a valid message + * @param docid the docid for this message, or 0 + * @param ti thread info for the current message, or NULL + * @param opts, bitwise OR'ed; + * - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be + * obtained from the database (this is much faster if the MuMsg is + * database-backed, so no file needs to be opened) + * - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary + * files and include links to those in the sexp + * + * @return a string with the sexp (free with g_free) or NULL in case of error + */ +struct _JsonNode* mu_msg_to_json (MuMsg *msg, unsigned docid, + const struct _MuMsgIterThreadInfo *ti, + MuMsgOptions ops) G_GNUC_WARN_UNUSED_RESULT; +#endif /*HAVE_JSON_GLIB*/ + + +/** + * convert the msg to a Lisp symbolic expression (for further processing in + * e.g. emacs) + * + * @param msg a valid message + * @param docid the docid for this message, or 0 + * @param ti thread info for the current message, or NULL + * @param opts, bitwise OR'ed; + * - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be + * obtained from the database (this is much faster if the MuMsg is + * database-backed, so no file needs to be opened) + * - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary + * files and include links to those in the sexp + * and for message parts: + * MU_MSG_OPTION_CHECK_SIGNATURES: check signatures + * MU_MSG_OPTION_AUTO_RETRIEVE_KEY: attempt to retrieve keys online + * MU_MSG_OPTION_USE_AGENT: attempt to use GPG-agent + * MU_MSG_OPTION_USE_PKCS7: attempt to use PKCS (instead of gpg) + * + * @return a string with the sexp (free with g_free) or NULL in case of error + */ +char* mu_msg_to_sexp (MuMsg *msg, unsigned docid, + const struct _MuMsgIterThreadInfo *ti, + MuMsgOptions ops) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + + G_END_DECLS + +#ifdef __cplusplus + +#include + +namespace Mu { + +/** + * convert the msg to a Lisp symbolic expression (for further processing in + * e.g. emacs) + * + * @param msg a valid message + * @param docid the docid for this message, or 0 + * @param ti thread info for the current message, or NULL + * @param opts, bitwise OR'ed; + * - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be + * obtained from the database (this is much faster if the MuMsg is + * database-backed, so no file needs to be opened) + * - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary + * files and include links to those in the sexp + * and for message parts: + * MU_MSG_OPTION_CHECK_SIGNATURES: check signatures + * MU_MSG_OPTION_AUTO_RETRIEVE_KEY: attempt to retrieve keys online + * MU_MSG_OPTION_USE_AGENT: attempt to use GPG-agent + * MU_MSG_OPTION_USE_PKCS7: attempt to use PKCS (instead of gpg) + * + * @return a Sexp::Node representing the message + */ +Mu::Sexp::Node msg_to_sexp (MuMsg *msg, unsigned docid, + const struct _MuMsgIterThreadInfo *ti, + MuMsgOptions ops); +} + +#endif /*__cplusplus*/ + + #endif /*__MU_MSG_H__*/ diff --git a/lib/utils/Makefile.am b/lib/utils/Makefile.am index 9c5681d3..04f616d7 100644 --- a/lib/utils/Makefile.am +++ b/lib/utils/Makefile.am @@ -55,8 +55,8 @@ libmu_utils_la_SOURCES= \ mu-command-parser.hh \ mu-readline.cc \ mu-readline.hh \ - mu-sexp-parser.cc \ - mu-sexp-parser.hh \ + mu-sexp.cc \ + mu-sexp.hh \ mu-str.c \ mu-str.h \ mu-util.c \ @@ -94,10 +94,10 @@ test_mu_str_LDADD= \ libmu-utils.la TEST_PROGS+= \ - test-sexp-parser -test_sexp_parser_SOURCES= \ - test-sexp-parser.cc -test_sexp_parser_LDADD= \ + test-sexp +test_sexp_SOURCES= \ + test-sexp.cc +test_sexp_LDADD= \ libmu-utils.la TEST_PROGS+= \ diff --git a/lib/utils/mu-command-parser.cc b/lib/utils/mu-command-parser.cc index 7c4aa4a9..596be055 100644 --- a/lib/utils/mu-command-parser.cc +++ b/lib/utils/mu-command-parser.cc @@ -27,24 +27,25 @@ using namespace Mu; using namespace Command; using namespace Sexp; +using Type = Node::Type; + static Mu::Error command_error(const std::string& msg) { return Mu::Error(Error::Code::Command, msg); } - void Command::invoke(const Command::CommandMap& cmap, const Node& call) { - if (call.type != Type::List || call.children.empty() || - call.children[0].type != Type::Symbol) + if (call.type() != Type::List || call.elements().empty() || + call.elements().at(0).type() != Type::Symbol) throw command_error("call must be a list starting with a symbol"); - const auto& params{call.children}; - const auto cmd_it = cmap.find(params[0].value); + const auto& params{call.elements()}; + const auto cmd_it = cmap.find(params.at(0).value()); if (cmd_it == cmap.end()) - throw command_error("unknown command '" + params[0].value + "'"); + throw command_error("unknown command '" + params.at(0).value() + "'"); const auto& cinfo{cmd_it->second}; @@ -58,8 +59,8 @@ Command::invoke(const Command::CommandMap& cmap, const Node& call) // so, we're looking for the odd-numbered parameters. const auto param_it = [&]() { for (size_t i = 1; i < params.size(); i += 2) - if (params[i].type == Type::Symbol && - params[i].value == ':' + argname) + if (params.at(i).type() == Type::Symbol && + params.at(i).value() == ':' + argname) return params.begin() + i + 1; return params.end(); @@ -75,31 +76,31 @@ Command::invoke(const Command::CommandMap& cmap, const Node& call) // the types must match, but the 'nil' symbol is acceptable as // "no value" - if (param_it->type != arginfo.type && - !(param_it->type == Type::Symbol && param_it->value == "nil")) + if (param_it->type() != arginfo.type && + !(param_it->type() == Type::Symbol && param_it->value() == "nil")) throw command_error("parameter '" + argname + "' expects type " + to_string(arginfo.type) + - " but got " + to_string(param_it->type)); + " but got " + to_string(param_it->type())); } // all passed parameters must be known for (size_t i = 1; i < params.size(); i += 2) { if (std::none_of(cinfo.args.begin(), cinfo.args.end(), - [&](auto&& arg) {return params[i].value == ":" + arg.first;})) - throw command_error("unknown parameter '" + params[i].value + "'"); + [&](auto&& arg) {return params.at(i).value() == ":" + arg.first;})) + throw command_error("unknown parameter '" + params.at(i).value() + "'"); } if (cinfo.handler) cinfo.handler(params); } -static Parameters::const_iterator +static auto find_param_node (const Parameters& params, const std::string& argname) { for (size_t i = 1; i < params.size(); i += 2) { if (i + 1 != params.size() && - params[i].type == Type::Symbol && - params[i].value == ':' + argname) + params.at(i).type() == Type::Symbol && + params.at(i).value() == ':' + argname) return params.begin() + i + 1; } @@ -111,7 +112,7 @@ constexpr auto Nil = "nil"; static bool is_nil(const Node& node) { - return node.type == Type::Symbol && node.value == Nil; + return node.type() == Type::Symbol && node.value() == Nil; } const std::string& @@ -121,12 +122,12 @@ Command::get_string_or (const Parameters& params, const std::string& argname, const auto it = find_param_node (params, argname); if (it == params.end() || is_nil(*it)) return alt; - else if (it->type != Type::String) + else if (it->type() != Type::String) throw Error(Error::Code::InvalidArgument, "expected but got %s (value: '%s')", - to_string(it->type).c_str(), - it->value.c_str()); + to_string(it->type()).c_str(), + it->value().c_str()); - return it->value; + return it->value(); } const std::string& @@ -136,12 +137,12 @@ Command::get_symbol_or (const Parameters& params, const std::string& argname, const auto it = find_param_node (params, argname); if (it == params.end() || is_nil(*it)) return alt; - else if (it->type != Type::Symbol) + else if (it->type() != Type::Symbol) throw Error(Error::Code::InvalidArgument, "expected but got %s (value: '%s')", - to_string(it->type).c_str(), - it->value.c_str()); + to_string(it->type()).c_str(), + it->value().c_str()); - return it->value; + return it->value(); } @@ -152,11 +153,11 @@ Command::get_int_or (const Parameters& params, const std::string& argname, const auto it = find_param_node (params, argname); if (it == params.end() || is_nil(*it)) return alt; - else if (it->type != Type::Integer) + else if (it->type() != Type::Number) throw Error(Error::Code::InvalidArgument, "expected but got %s", - to_string(it->type).c_str()); + to_string(it->type()).c_str()); else - return ::atoi(it->value.c_str()); + return ::atoi(it->value().c_str()); } bool @@ -166,11 +167,11 @@ Command::get_bool_or (const Parameters& params, const std::string& argname, const auto it = find_param_node (params, argname); if (it == params.end()) return alt; - else if (it->type != Type::Symbol) + else if (it->type() != Type::Symbol) throw Error(Error::Code::InvalidArgument, "expected but got %s", - to_string(it->type).c_str()); + to_string(it->type()).c_str()); else - return it->value != Nil; + return it->value() != Nil; } std::vector @@ -179,17 +180,17 @@ Command::get_string_vec (const Parameters& params, const std::string& argname) const auto it = find_param_node (params, argname); if (it == params.end() || is_nil(*it)) return {}; - else if (it->type != Type::List) + else if (it->type() != Type::List) throw Error(Error::Code::InvalidArgument, "expected but got %s", - to_string(it->type).c_str()); + to_string(it->type()).c_str()); std::vector vec; - for (const auto& n: it->children) { - if (n.type != Type::String) + for (const auto& n: it->elements()) { + if (n.type() != Type::String) throw Error(Error::Code::InvalidArgument, "expected string element but got %s", - to_string(n.type).c_str()); - vec.emplace_back (n.value); + to_string(n.type()).c_str()); + vec.emplace_back (n.value()); } return vec; diff --git a/lib/utils/mu-command-parser.hh b/lib/utils/mu-command-parser.hh index 58c2f115..9ad8110d 100644 --- a/lib/utils/mu-command-parser.hh +++ b/lib/utils/mu-command-parser.hh @@ -28,7 +28,7 @@ #include #include "utils/mu-error.hh" -#include "utils/mu-sexp-parser.hh" +#include "utils/mu-sexp.hh" namespace Mu { @@ -49,18 +49,18 @@ namespace Command { /// Information about a function argument struct ArgInfo { - ArgInfo (Sexp::Type typearg, bool requiredarg, std::string&& docarg): + ArgInfo (Sexp::Node::Type typearg, bool requiredarg, std::string&& docarg): type{typearg}, required{requiredarg},docstring{std::move(docarg)} {} - const Sexp::Type type; /**< Sexp::Type of the argument */ - const bool required; /**< Is this argument required? */ - const std::string docstring; /**< Documentation */ + const Sexp::Node::Type type; /**< Sexp::Node::Type of the argument */ + const bool required; /**< Is this argument required? */ + const std::string docstring; /**< Documentation */ }; /// The arguments for a function, which maps their names to the information. using ArgMap = std::unordered_map; // The parameters to a Handler. -using Parameters = std::vector; +using Parameters = Sexp::Node::Seq; int get_int_or (const Parameters& parms, const std::string& argname, int alt=0); bool get_bool_or (const Parameters& parms, const std::string& argname, bool alt=false); @@ -149,7 +149,6 @@ operator<<(std::ostream& os, const Command::CommandMap& map) return os; } - } // namespace Command } // namespace Mu diff --git a/lib/utils/mu-sexp-parser.hh b/lib/utils/mu-sexp-parser.hh deleted file mode 100644 index ecb97f2d..00000000 --- a/lib/utils/mu-sexp-parser.hh +++ /dev/null @@ -1,115 +0,0 @@ -/* -** Copyright (C) 2020 djcb -** -** 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. -** -*/ - -#ifndef MU_SEXP_PARSER_HH__ -#define MU_SEXP_PARSER_HH__ - -#include -#include - -#include "utils/mu-error.hh" - -namespace Mu { -namespace Sexp { - -/// Simple s-expression parser that parses lists () and atoms (strings -/// ("-quoted), (positive) integers ([0..9]+) and symbol starting with alpha or -/// ':', then alphanum and '-') -/// -/// (:foo (1234 "bar" nil) :quux (a b c)) - -/// Node type -enum struct Type { List, String, Integer, Symbol }; - -/// Parse node -struct Node { - /** - * Construct a new non-list node - * - * @param typearg the type of node - * @param valuearg the value - */ - Node(Type typearg, std::string&& valuearg): - type{typearg}, value{std::move(valuearg)} { - if (typearg == Type::List) - throw Error(Error::Code::Parsing, - "atomic type cannot be a "); - } - - /** - * Construct a list node - - * @param childrenarg the list children - * - * @return - */ - explicit Node(std::vector&& childrenarg): - type{Type::List}, children{std::move(childrenarg)} - {} - - const Type type; /**< Type of node */ - const std::string value; /**< String value of node (only for non-Type::List)*/ - const std::vector children; /**< Chiidren of node (only for Type::List) */ -}; - -/** - * Parse the string as an s-expressi9on. - * - * @param expr an s-expression string - * - * @return the parsed s-expression, or throw Error. - */ -Node parse(const std::string& expr); - -static inline std::ostream& -operator<<(std::ostream& os, Sexp::Type id) -{ - switch (id) { - case Sexp::Type::List: os << ""; break; - case Sexp::Type::String: os << ""; break; - case Sexp::Type::Integer: os << ""; break; - case Sexp::Type::Symbol: os << ""; break; - default: throw std::runtime_error ("unknown node type"); - } - - return os; -} - -static inline std::ostream& -operator<<(std::ostream& os, const Sexp::Node& node) -{ - os << node.type; - if (node.type == Sexp::Type::List) { - os << '('; - for (auto&& elm: node.children) - os << elm; - os << ')'; - } else - os << '{' << node.value << '}'; - - return os; -} - - -} // Sexp - - -} // Mu - -#endif /* MU_SEXP_PARSER_HH__ */ diff --git a/lib/utils/mu-sexp-parser.cc b/lib/utils/mu-sexp.cc similarity index 76% rename from lib/utils/mu-sexp-parser.cc rename to lib/utils/mu-sexp.cc index 52f418ad..c516a889 100644 --- a/lib/utils/mu-sexp-parser.cc +++ b/lib/utils/mu-sexp.cc @@ -1,5 +1,6 @@ /* -** Copyright (C) 2020 djcb +** Copyright (C) 2020 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 @@ -18,9 +19,11 @@ */ -#include "mu-sexp-parser.hh" +#include "mu-sexp.hh" #include "mu-utils.hh" +#include + using namespace Mu; using namespace Sexp; @@ -58,16 +61,16 @@ parse_list (const std::string& expr, size_t& pos) if (expr[pos] != '(') // sanity check. throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]); - std::vector children; + Node::Seq children; ++pos; while (expr[pos] != ')' && pos != expr.size()) - children.emplace_back(parse(expr, pos)); + children.add(parse(expr, pos)); if (expr[pos] != ')') throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]); ++pos; - return Node{std::move(children)}; + return Node::make_list(std::move(children)); } // parse string @@ -100,7 +103,7 @@ parse_string (const std::string& expr, size_t& pos) throw parsing_error(pos, "unterminated string '%s'", str.c_str()); ++pos; - return Node{Type::String, std::move(str)}; + return Node::make_string(std::move(str)); } static Node @@ -118,7 +121,7 @@ parse_integer (const std::string& expr, size_t& pos) for (; isdigit(expr[pos]); ++pos) num += expr[pos]; - return Node {Type::Integer, std::move(num)}; + return Node::make_number(::atoi(num.c_str())); } static Node @@ -131,7 +134,7 @@ parse_symbol (const std::string& expr, size_t& pos) for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos) symbol += expr[pos]; - return Node { Type::Symbol, std::move(symbol)}; + return Node::make_symbol(std::move(symbol)); } @@ -163,7 +166,7 @@ parse (const std::string& expr, size_t& pos) } Node -Sexp::parse (const std::string& expr) +Sexp::Node::make (const std::string& expr) { size_t pos{}; auto node{::parse (expr, pos)}; @@ -173,3 +176,42 @@ Sexp::parse (const std::string& expr) return node; } + + +std::string +Sexp::Node::to_string () const +{ + std::stringstream sstrm; + + switch (type()) { + case Type::List: { + sstrm << '('; + bool first{true}; + for (auto&& child : elements()) { + sstrm << (first ? "" : " ") << child.to_string(); + first = false; + } + sstrm << ')'; + break; + } + case Type::String: + //sstrm << quote(value()); + sstrm << "\""; + for (auto&& k: value()) { + switch (k) { + case '"' : sstrm << "\\\""; break; + case '\\': sstrm << "\\\\"; break; + default: sstrm << k; + } + } + sstrm << "\""; + break; + + case Type::Number: + case Type::Symbol: + default: + sstrm << value(); + } + + return sstrm.str(); +} diff --git a/lib/utils/mu-sexp.hh b/lib/utils/mu-sexp.hh new file mode 100644 index 00000000..b2aad10f --- /dev/null +++ b/lib/utils/mu-sexp.hh @@ -0,0 +1,252 @@ +/* +** Copyright (C) 2020 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. +** +*/ + + +#ifndef MU_SEXP_HH__ +#define MU_SEXP_HH__ + +#include +#include +#include + +#include "utils/mu-utils.hh" +#include "utils/mu-error.hh" + +namespace Mu { +namespace Sexp { + +/// Simple s-expression parser & builder that parses lists () and atoms (strings +/// ("-quoted), (positive) integers ([0..9]+) and symbol starting with alpha or +/// ':', then alphanum and '-') +/// +/// (:foo (1234 "bar" nil) :quux (a b c)) + + +/// Parse node +struct Node { + /// Node type + enum struct Type { List, String, Number, Symbol }; + + /** + * Make a node of out of an s-expression string. + * + * @param expr a string containing an s-expression + * + * @return the parsed s-expression, or throw Error. + */ + static Node make (const std::string& expr); + + /** + * Make a node for a string/integer/symbol/list value + * + * @param val some value + * + * @return a node + */ + static Node make_string (std::string&& val) { return Node{Type::String, std::move(val)}; } + static Node make_string (const std::string& val) { return Node{Type::String, std::string{val}}; } + static Node make_number (int val) { return Node{Type::Number, format("%d", val)}; } + static Node make_symbol (std::string&& val) { return Node{Type::Symbol, std::move(val)}; } + + + /// sequence of node objects + struct Seq { + /** + * Add an item to a node-sequence. + * + * @param node item to add; moved/consumed. + */ + void add(Node&& node) {nodes_.emplace_back(std::move(node));} + void add(Seq&& seq) {add(make_list(std::move(seq)));} + void add(std::string&& s) {add(make_string(std::move(s)));} + void add(int i) {add(make_number(i));} + // disambiguate. + void add_symbol(std::string&& s) {add(make_symbol(std::move(s)));} + + /** + * Add a property to tne node sequence; i.e. a property-symbol + * (starting with ':') and some node + * + * @param name name (must start with ":"), and some value or list + * @param val + */ + template void add_prop(std::string&& name, T&& val) { + + if (name.empty() || name[0] != ':') + throw Error{Error::Code::InvalidArgument, + "property names must start with ':'"}; + + add(make_symbol(std::move(name))); + add(std::move(val)); + } + + + // deliberately limited stl-like + + /** + * Is this an empty sequence? + * + * @return true or false + */ + bool empty() const {return nodes_.empty();} + + /** + * Get the number of elsements in the sequence + * + * @return number of elmement. + */ + size_t size() const {return nodes_.size();} + + /** + * Get begin iterator of the sequence + * + * @return iterator + */ + const auto begin() const { return nodes_.begin(); } + /** + * Get the end iterator of the sequnce + * + * @return an iterator + */ + const auto end() const { return nodes_.end(); } + + /** + * Get a const ref to the item at idx. + * + * @param idx index, must be < size() + * + * @return const ref to the item. + */ + const auto at(size_t idx) const { return nodes_.at(idx);} + + private: + std::vector nodes_; + }; + + /** + * Make a list node from a sequence + * + * @param seq a sequence of nodes + * + * @return a node + */ + static Node make_list (Seq&& seq) { return Node{std::move(seq)}; } + + /** + * Convert a Sexp::Node to its string representation + * + * @return the string representation + */ + std::string to_string() const; + + /** + * Return the type of this Node. + * + * @return the type + */ + Type type() const { return type_; } + + /// Some type helpers + bool is_list() const { return type() == Type::List; }; + bool is_string() const { return type() == Type::String; } + bool is_number() const { return type() == Type::Number; } + bool is_symbol() const { return type() == Type::Symbol; } + bool is_nil() const { return is_symbol() && value() == "nil"; } + bool is_t() const { return is_symbol() && value() == "t"; } + + /** + * The elements of this node; invalid unless this is a list node. + * + * @return + */ + const Seq& elements() const { + if (type() != Type::List) + throw Error(Error::Code::InvalidArgument, "no elements for non-list"); + return seq_; + } + + /** + * The value of this node; invalid for list nodes. + * + * @return + */ + const std::string& value() const { + if (type_ == Type::List) + throw Error(Error::Code::InvalidArgument, "no value for list"); + return value_; + } + +private: + /** + * Construct a new non-list node + * + * @param typearg the type of node + * @param valuearg the value + */ + Node(Type typearg, std::string&& valuearg): + type_{typearg}, value_{std::move(valuearg)} { + if (typearg == Type::List) + throw Error(Error::Code::Parsing, + "atomic type cannot be a "); + } + + /** + * Construct a list node + * + * @param kids the list's children + */ + explicit Node(Seq&& seq): + type_{Type::List}, seq_{std::move(seq)} + {} + + + const Type type_; /**< Type of node */ + const std::string value_; /**< String value of node (only for + * non-Type::Lst)*/ + const Seq seq_; /**< Children of node (only for + * Type::Lst) */ +}; + +static inline std::ostream& +operator<<(std::ostream& os, Sexp::Node::Type id) +{ + switch (id) { + case Sexp::Node::Type::List: os << ""; break; + case Sexp::Node::Type::String: os << ""; break; + case Sexp::Node::Type::Number: os << ""; break; + case Sexp::Node::Type::Symbol: os << ""; break; + default: throw std::runtime_error ("unknown node type"); + } + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Sexp::Node& node) +{ + os << node.to_string(); + return os; +} + +} // Sexp + + +} // Mu + +#endif /* MU_SEXP_HH__ */ diff --git a/lib/utils/test-command-parser.cc b/lib/utils/test-command-parser.cc index f18e6425..00da3163 100644 --- a/lib/utils/test-command-parser.cc +++ b/lib/utils/test-command-parser.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2017 Dirk-Jan C. Binnema +** Copyright (C) 2020 Dirk-Jan C. Binnema ** ** This library is free software; you can redistribute it and/or ** modify it under the terms of the GNU Lesser General Public License @@ -31,23 +31,23 @@ using namespace Mu; static void test_param_getters() { - const auto node { Sexp::parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")}; + const auto node { Sexp::Node::make(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")}; std::cout << node << "\n"; - g_assert_cmpint(Command::get_int_or(node.children,"bar"), ==, 123); - assert_equal(Command::get_string_or(node.children, "bra", "bla"), "bla"); - assert_equal(Command::get_string_or(node.children, "cuux"), "456"); + g_assert_cmpint(Command::get_int_or(node.elements(), "bar"), ==, 123); + assert_equal(Command::get_string_or(node.elements(), "bra", "bla"), "bla"); + assert_equal(Command::get_string_or(node.elements(), "cuux"), "456"); - g_assert_true(Command::get_bool_or(node.children,"boo") == false); - g_assert_true(Command::get_bool_or(node.children,"bah") == true); + g_assert_true(Command::get_bool_or(node.elements(),"boo") == false); + g_assert_true(Command::get_bool_or(node.elements(),"bah") == true); } static bool call (const Command::CommandMap& cmap, const std::string& sexp) try { - const auto node{Sexp::parse(sexp)}; + const auto node{Sexp::Node::make(sexp)}; g_message ("invoking %s", to_string(node).c_str()); invoke (cmap, node); @@ -70,8 +70,8 @@ test_command() cmap.emplace("my-command", CommandInfo{ - ArgMap{ {"param1", ArgInfo{Sexp::Type::String, true, "some string" }}, - {"param2", ArgInfo{Sexp::Type::Integer, false, "some integer"}}}, + ArgMap{ {"param1", ArgInfo{Sexp::Node::Type::String, true, "some string" }}, + {"param2", ArgInfo{Sexp::Node::Type::Number, false, "some integer"}}}, "My command,", {}}); @@ -93,8 +93,8 @@ test_command2() cmap.emplace("bla", CommandInfo{ ArgMap{ - {"foo", ArgInfo{Sexp::Type::Integer, false, "foo"}}, - {"bar", ArgInfo{Sexp::Type::String, false, "bar"}}, + {"foo", ArgInfo{Sexp::Node::Type::Number, false, "foo"}}, + {"bar", ArgInfo{Sexp::Node::Type::String, false, "bar"}}, },"yeah", [&](const auto& params){}}); @@ -115,8 +115,8 @@ test_command_fail() cmap.emplace("my-command", CommandInfo{ - ArgMap{ {"param1", ArgInfo{Sexp::Type::String, true, "some string" }}, - {"param2", ArgInfo{Sexp::Type::Integer, false, "some integer"}}}, + ArgMap{ {"param1", ArgInfo{Sexp::Node::Type::String, true, "some string" }}, + {"param2", ArgInfo{Sexp::Node::Type::Number, false, "some integer"}}}, "My command,", {}}); @@ -124,7 +124,6 @@ test_command_fail() g_assert_false (call(cmap, "(my-command2)")); g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)")); g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")")); - } diff --git a/lib/utils/test-sexp-parser.cc b/lib/utils/test-sexp-parser.cc deleted file mode 100644 index 0dccf963..00000000 --- a/lib/utils/test-sexp-parser.cc +++ /dev/null @@ -1,76 +0,0 @@ -/* -** Copyright (C) 2020 Dirk-Jan C. Binnema -** -** This library is free software; you can redistribute it and/or -** modify it under the terms of the GNU Lesser General Public License -** as published by the Free Software Foundation; either version 2.1 -** of the License, or (at your option) any later version. -** -** This library 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 -** Lesser General Public License for more details. -** -** You should have received a copy of the GNU Lesser General Public -** License along with this library; if not, write to the Free -** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA -** 02110-1301, USA. -*/ - -#include -#include - -#include -#include - -#include "mu-command-parser.hh" -#include "mu-utils.hh" - -using namespace Mu; - -static bool -check_parse (const std::string& expr, const std::string& expected) -{ - try { - const auto parsed{to_string(Sexp::parse(expr))}; - g_assert_cmpstr(parsed.c_str(), ==, expected.c_str()); - return true; - - } catch (const Error& err) { - g_warning ("caught exception parsing '%s': %s", expr.c_str(), err.what()); - return false; - } -} - -static void -test_parser() -{ - check_parse(R"(:foo-123)", "{:foo-123}"); - check_parse(R"("foo")", "{foo}"); - check_parse(R"(12345)", "{12345}"); - check_parse(R"(-12345)", "{-12345}"); - check_parse(R"((123 bar "cuux"))", "({123}{bar}{cuux})"); - - check_parse(R"("\"")", "{\"}"); - check_parse(R"("\\")", "{\\}"); -} - -int -main (int argc, char *argv[]) try -{ - g_test_init (&argc, &argv, NULL); - - if (argc == 2) { - std::cout << Sexp::parse(argv[1]) << '\n'; - return 0; - } - - g_test_add_func ("/utils/command-parser/parse", test_parser); - - return g_test_run (); - - -} catch (const std::runtime_error& re) { - std::cerr << re.what() << "\n"; - return 1; -} diff --git a/lib/utils/test-sexp.cc b/lib/utils/test-sexp.cc new file mode 100644 index 00000000..1bbbfe85 --- /dev/null +++ b/lib/utils/test-sexp.cc @@ -0,0 +1,133 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library 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 +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include +#include + +#include +#include + +#include "mu-command-parser.hh" +#include "mu-utils.hh" + +using namespace Mu; +using namespace Sexp; + +static bool +check_parse (const std::string& expr, const std::string& expected) +{ + try { + const auto parsed{to_string(Node::make(expr))}; + g_assert_cmpstr(parsed.c_str(), ==, expected.c_str()); + return true; + + } catch (const Error& err) { + g_warning ("caught exception parsing '%s': %s", expr.c_str(), err.what()); + return false; + } +} + +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\""); +} + +static void +test_builder() +{ + const auto nstr{Node::make_string("foo")}; + g_assert_true(nstr.value() == "foo"); + g_assert_true(nstr.type() == Node::Type::String); + assert_equal(nstr.to_string(), "\"foo\""); + + const auto nnum{Node::make_number(123)}; + g_assert_true(nnum.value() == "123"); + g_assert_true(nnum.type() == Node::Type::Number); + assert_equal(nnum.to_string(), "123"); + + const auto nsym{Node::make_symbol("blub")}; + g_assert_true(nsym.value() == "blub"); + g_assert_true(nsym.type() == Node::Type::Symbol); + assert_equal(nsym.to_string(), "blub"); + + Node::Seq seq; + seq.add(Node::make_string("foo")); + seq.add(Node::make_number(123)); + seq.add(Node::make_symbol("blub")); + + const auto nlst = Node::make_list(std::move(seq)); + g_assert_true(nlst.elements().size() == 3); + g_assert_true(nlst.type() == Node::Type::List); + g_assert_true(nlst.elements().at(1).value() == "123"); + + assert_equal(nlst.to_string(),"(\"foo\" 123 blub)"); +} + +static void +test_props() +{ + Node::Seq seq; + + seq.add_prop(":foo", "bär"); + seq.add_prop(":cuux", 123); + seq.add_prop(":flub", Node::make_symbol("fnord")); + + Node::Seq seq2; + seq2.add(Node::make_string("foo")); + seq2.add(Node::make_number(123)); + seq2.add(Node::make_symbol("blub")); + + seq.add_prop(":boo", std::move(seq2)); + + Node expr = Node::make_list(std::move(seq)); + assert_equal(expr.to_string(), + "(:foo \"b\\303\\244r\" :cuux 123 :flub fnord :boo (\"foo\" 123 blub))"); +} + +int +main (int argc, char *argv[]) try +{ + g_test_init (&argc, &argv, NULL); + + if (argc == 2) { + std::cout << Sexp::Node::make(argv[1]) << '\n'; + return 0; + } + + g_test_add_func ("/utils/sexp/parser", test_parser); + g_test_add_func ("/utils/sexp/builder", test_builder); + g_test_add_func ("/utils/sexp/props", test_props); + + return g_test_run (); + + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} diff --git a/mu/mu-cmd-server.cc b/mu/mu-cmd-server.cc index 83ca3bc2..b3e33b5f 100644 --- a/mu/mu-cmd-server.cc +++ b/mu/mu-cmd-server.cc @@ -146,6 +146,20 @@ print_expr (const char* frm, ...) } +static void +print_expr (const Node& sexp) +{ + print_expr ("%s", sexp.to_string().c_str()); +} + +static void +print_expr (Node::Seq&& seq) +{ + print_expr (Node::make_list(std::move(seq))); +} + + + G_GNUC_PRINTF(2,3) static MuError print_error (MuError errcode, const char* frm, ...) { @@ -156,7 +170,12 @@ print_error (MuError errcode, const char* frm, ...) g_vasprintf (&msg, frm, ap); va_end (ap); - print_expr ("(:error %u :message %s)", errcode, quote(msg).c_str()); + Node::Seq err_sexp; + err_sexp.add_prop(":error", (int)errcode); + err_sexp.add_prop(":message", msg); + + print_expr(Node::make_list(std::move(err_sexp))); + g_free (msg); return errcode; @@ -289,6 +308,7 @@ add_handler (Context& context, const Parameters& params) auto sexp{mu_msg_to_sexp (msg, docid, NULL, MU_MSG_OPTION_VERIFY)}; print_expr ("(:update %s :move nil)", sexp); + mu_msg_unref(msg); g_free (sexp); } @@ -322,7 +342,6 @@ each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) pinfo->attlist = g_slist_append (pinfo->attlist, att); g_free (cachefile); - } @@ -410,6 +429,7 @@ compose_handler (Context& context, const Parameters& params) atts = (ctype == FORWARD) ? include_attachments (msg, opts) : NULL; mu_msg_unref (msg); } + print_expr ("(:compose %s :original %s :include %s)", typestr.c_str(), sexp ? sexp : "nil", atts ? atts : "nil"); @@ -419,11 +439,11 @@ compose_handler (Context& context, const Parameters& params) struct SexpData { - GString *gstr; - gboolean personal; - time_t last_seen; - gint64 tstamp; - size_t rank; + Sexp::Node::Seq contacts; + gboolean personal; + time_t last_seen; + gint64 tstamp; + size_t rank; }; @@ -452,8 +472,11 @@ each_contact_sexp (const char* full_address, if (!email || !strstr (email, "@")) return; - g_string_append_printf (sdata->gstr, "(%s . %zu)\n", - quote(full_address).c_str(), sdata->rank); + Node::Seq contact; + contact.add_prop(":address", full_address); + contact.add_prop(":rank", sdata->rank); + + sdata->contacts.add(Node::make_list(std::move(contact))); } /** @@ -465,33 +488,25 @@ each_contact_sexp (const char* full_address, * * @return the sexp */ -static char* +static Sexp::Node contacts_to_sexp (const MuContacts *contacts, bool personal, int64_t last_seen, gint64 tstamp) { - - g_return_val_if_fail (contacts, NULL); - SexpData sdata{}; sdata.personal = personal; sdata.last_seen = last_seen; sdata.tstamp = tstamp; sdata.rank = 0; - /* make a guess for the initial size */ - sdata.gstr = g_string_sized_new (mu_contacts_count(contacts) * 128); - g_string_append (sdata.gstr, "(:contacts ("); - const auto cutoff{g_get_monotonic_time()}; mu_contacts_foreach (contacts, (MuContactsForeachFunc)each_contact_sexp, &sdata); - /* pass a string, elisp doesn't like 64-bit nums */ - g_string_append_printf (sdata.gstr, - ") :tstamp \"%" G_GINT64_FORMAT "\")", cutoff); + Node::Seq seq; + seq.add_prop(":contacts", std::move(sdata.contacts)); + seq.add_prop(":tstamp", Node::make_string(format("%" G_GINT64_FORMAT, cutoff))); - return g_string_free (sdata.gstr, FALSE); + return Node::make_list(std::move(seq)); } - static void contacts_handler (Context& context, const Parameters& params) { @@ -508,9 +523,7 @@ contacts_handler (Context& context, const Parameters& params) throw Error{Error::Code::Internal, "failed to get contacts"}; /* dump the contacts cache as a giant sexp */ - auto sexp = contacts_to_sexp (contacts, personal, after, tstamp); - print_expr ("%s\n", sexp); - g_free (sexp); + print_expr(contacts_to_sexp (contacts, personal, after, tstamp)); } static void @@ -526,7 +539,11 @@ save_part (MuMsg *msg, unsigned docid, unsigned index, path.c_str(), index, &gerr)) throw Error{Error::Code::File, &gerr, "failed to save part"}; - print_expr ("(:info save :message %s)", quote(path + " has been saved").c_str()); + Node::Seq seq; + seq.add_prop(":info", Node::make_symbol("save")); + seq.add_prop(":message", Node::make_string(format("%s has been saved", path.c_str()))); + + print_expr(std::move(seq)); } @@ -550,9 +567,12 @@ open_part (MuMsg *msg, unsigned docid, unsigned index, MuMsgOptions opts) throw Error{Error::Code::File, &gerr, "failed to play"}; } - print_expr ("(:info open :message %s)", - quote(std::string{targetpath} + " has been opened").c_str()); + Node::Seq seq; + seq.add_prop(":info", Node::make_symbol("open")); + seq.add_prop(":message", Node::make_string(format("%s has been opened", targetpath))); g_free (targetpath); + + print_expr(std::move(seq)); } static void @@ -576,19 +596,16 @@ temp_part (MuMsg *msg, unsigned docid, unsigned index, throw Error{Error::Code::File, &gerr, "saving failed"}; } - const auto qpath{quote(path)}; - g_free(path); + Node::Seq seq; + seq.add_prop(":temp", path); + seq.add_prop(":what", std::string(what)); + seq.add_prop(":docid", docid); if (!param.empty()) - print_expr ("(:temp %s" - " :what \"%s\"" - " :docid %u" - " :param %s" - ")", - qpath.c_str(), what.c_str(), docid, quote(param).c_str()); - else - print_expr ("(:temp %s :what \"%s\" :docid %u)", - qpath.c_str(), what.c_str(), docid); + seq.add_prop(":param", std::string(param)); + + g_free(path); + print_expr(std::move(seq)); } @@ -740,9 +757,19 @@ find_handler (Context& context, const Parameters& params) /* before sending new results, send an 'erase' message, so the frontend * knows it should erase the headers buffer. this will ensure that the * output of two finds will not be mixed. */ - print_expr ("(:erase t)"); - const auto foundnum{print_sexps (miter, maxnum)}; - print_expr ("(:found %u)", foundnum); + { + Node::Seq seq; + seq.add_prop(":erase", Node::make_symbol("t")); + print_expr(std::move(seq)); + } + //print_expr ("(:erase t)"); + { + const auto foundnum{print_sexps (miter, maxnum)}; + Node::Seq seq; + seq.add_prop(":found", foundnum); + print_expr(std::move(seq)); + } + //print_expr ("(:found %u)", foundnum); mu_msg_iter_destroy (miter); } @@ -802,9 +829,13 @@ index_msg_cb (MuIndexStats *stats, void *user_data) if (stats->_processed % 1000) return MU_OK; - print_expr ("(:info index :status running " - ":processed %u :updated %u)", - stats->_processed, stats->_updated); + Node::Seq seq; + seq.add_prop(":info", Node::make_symbol("index")); + seq.add_prop(":status", Node::make_symbol("running")); + seq.add_prop(":processed", stats->_processed); + seq.add_prop(":updated", stats->_updated); + + print_expr(std::move(seq)); return MU_OK; } @@ -828,11 +859,16 @@ index_and_maybe_cleanup (MuIndex *index, bool cleanup, bool lazy_check) throw Error{Error::Code::Store, &gerr, "cleanup failed"}; } - print_expr ("(:info index :status complete " - ":processed %u :updated %u :cleaned-up %u)", - stats._processed, stats._updated, stats2._cleaned_up); + Node::Seq seq; + seq.add_prop(":info", Node::make_symbol("index")); + seq.add_prop(":status", Node::make_symbol("complete")); + seq.add_prop(":processed", stats._processed); + seq.add_prop(":updated", stats._updated); + seq.add_prop(":cleaned-up", stats2._cleaned_up); - return rv; + print_expr(std::move(seq)); + + return MU_OK; } @@ -865,7 +901,11 @@ mkdir_handler (Context& context, const Parameters& params) if (!mu_maildir_mkdir(path.c_str(), 0755, FALSE, &gerr)) throw Error{Error::Code::File, &gerr, "failed to create maildir"}; - print_expr ("(:info mkdir :message \"%s has been created\")", path.c_str()); + Node::Seq seq; + seq.add_prop(":info", "mkdir"); + seq.add_prop(":message", format("%s has been created", path.c_str())); + + print_expr(std::move(seq)); } @@ -996,7 +1036,7 @@ move_handler (Context& context, const Parameters& params) if (flags == MU_FLAG_INVALID) { mu_msg_unref(msg); - throw Error{Error::Code::InvalidArgument, "invalid flagse"}; + throw Error{Error::Code::InvalidArgument, "invalid flags"}; } try { @@ -1018,46 +1058,43 @@ ping_handler (Context& context, const Parameters& params) throw Error{Error::Code::Store, &gerr, "failed to read store"}; const auto queries = get_string_vec (params, "queries"); - const auto qresults = [&]() -> std::string { - if (queries.empty()) - return {}; + Node::Seq qresults; + for (auto&& q: queries) { + const auto count{mu_query_count_run (context.query, q.c_str())}; + const auto unreadq{format("flag:unread AND (%s)", q.c_str())}; + const auto unread{mu_query_count_run (context.query, unreadq.c_str())}; - std::string res{":queries ("}; - for (auto&& q: queries) { - const auto count{mu_query_count_run (context.query, q.c_str())}; - const auto unreadq{format("flag:unread AND (%s)", q.c_str())}; - const auto unread{mu_query_count_run (context.query, unreadq.c_str())}; - res += format("(:query %s :count %zu :unread %zu)", quote(q).c_str(), - count, unread); - } - return res + ")"; - }(); + Node::Seq seq; + seq.add_prop(":query", std::string(q)); + seq.add_prop(":count", count); + seq.add_prop(":unread", unread); - const auto personal = [&]() ->std::string { - auto addrs{mu_store_personal_addresses (context.store)}; - std::string res; - if (addrs && g_strv_length(addrs) != 0) { - res = ":personal-addresses ("; - for (int i = 0; addrs[i]; ++i) - res += quote(addrs[i]) + ' '; - res += ")"; - } - g_strfreev(addrs); - return res; - }(); + qresults.add(Node::make_list(std::move(seq))); + } - print_expr ("(:pong \"mu\" :props (" - ":version \"" VERSION "\" " - "%s " - ":database-path %s " - ":root-maildir %s " - ":doccount %u " - "%s))", - personal.c_str(), - quote(mu_store_database_path(context.store)).c_str(), - quote(mu_store_root_maildir(context.store)).c_str(), - storecount, - qresults.c_str()); + Node::Seq addrs; + char **addrs_strv{mu_store_personal_addresses (context.store)}; + for (auto cur = addrs_strv; cur && *cur; ++cur) + addrs.add(*cur); + g_strfreev (addrs_strv); + + const auto dbpath{mu_store_database_path(context.store)}; + const auto root{mu_store_root_maildir(context.store)}; + + Node::Seq seq; + seq.add_prop(":pong", "mu"); + + Node::Seq propseq; + propseq.add_prop(":version", VERSION); + propseq.add_prop(":personal-addresses", std::move(addrs)); + propseq.add_prop(":database-path", dbpath); + propseq.add_prop(":root-maildir", root); + propseq.add_prop(":doccount", storecount); + propseq.add_prop(":queries", std::move(qresults)); + + seq.add_prop(":props", std::move(propseq)); + + print_expr(std::move(seq)); } static void @@ -1081,8 +1118,10 @@ remove_handler (Context& context, const Parameters& params) throw Error(Error::Code::Store, "failed to remove message @ %s (%d) from store", path.c_str(), docid); + Node::Seq seq; + seq.add_prop(":remove", docid); - print_expr ("(:remove %u)", docid); + print_expr(std::move(seq)); } @@ -1095,7 +1134,12 @@ sent_handler (Context& context, const Parameters& params) if (docid == MU_STORE_INVALID_DOCID) throw Error{Error::Code::Store, &gerr, "failed to add path"}; - print_expr ("(:sent t :path %s :docid %u)", quote(path).c_str(), docid); + Node::Seq seq; + seq.add_prop (":sent", Node::make_symbol("t")); + seq.add_prop (":path", std::string(path)); + seq.add_prop (":docid", docid); + + print_expr (std::move(seq)); } @@ -1118,11 +1162,11 @@ view_handler (Context& context, const Parameters& params) if (!msg) throw Error{Error::Code::Store, &gerr, "failed to find message for view"}; - auto sexp{mu_msg_to_sexp(msg, docid, {}, message_options(params))}; + Node::Seq seq; + seq.add_prop(":view", msg_to_sexp(msg, docid, {}, message_options(params))); mu_msg_unref(msg); - print_expr ("(:view %s)\n", sexp); - g_free (sexp); + print_expr (std::move(seq)); } @@ -1131,9 +1175,11 @@ make_command_map (Context& context) { CommandMap cmap; + using Type = Node::Type; + cmap.emplace("add", CommandInfo{ - ArgMap{ {"path", ArgInfo{Type::String, true, "file system path to the message" }}}, + ArgMap{ {"path", ArgInfo{Type::String, true, "file system path to the message" }}}, "add a message to the store", [&](const auto& params){add_handler(context, params);}}); @@ -1141,7 +1187,7 @@ make_command_map (Context& context) CommandInfo{ ArgMap{{"type", ArgInfo{Type::Symbol, true, "type of composition: reply/forward/edit/resend/new"}}, - {"docid", ArgInfo{Type::Integer, false,"document id of parent-message, if any"}}, + {"docid", ArgInfo{Type::Number, false,"document id of parent-message, if any"}}, {"decrypt", ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)" }}}, "get contact information", [&](const auto& params){compose_handler(context, params);}}); @@ -1159,8 +1205,8 @@ make_command_map (Context& context) cmap.emplace("extract", CommandInfo{ - ArgMap{{"docid", ArgInfo{Type::Integer, true, "document for the message" }}, - {"index", ArgInfo{Type::Integer, true, "index for the part to operate on" }}, + ArgMap{{"docid", ArgInfo{Type::Number, true, "document for the message" }}, + {"index", ArgInfo{Type::Number, true, "index for the part to operate on" }}, {"action", ArgInfo{Type::Symbol, true, "what to do with the part" }}, {"decrypt", ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)" }}, @@ -1178,7 +1224,7 @@ make_command_map (Context& context) {"sortfield", ArgInfo{Type::Symbol, false, "the field to sort results by" }}, {"descending", ArgInfo{Type::Symbol, false, "whether to sort in descending order" }}, - {"maxnum", ArgInfo{Type::Integer, false, + {"maxnum", ArgInfo{Type::Number, false, "maximum number of result (hint)" }}, {"skip-dups", ArgInfo{Type::Symbol, false, "whether to skip messages with duplicate message-ids" }}, @@ -1207,7 +1253,7 @@ make_command_map (Context& context) cmap.emplace("move", CommandInfo{ - ArgMap{{"docid", ArgInfo{Type::Integer, false, "document-id"}}, + ArgMap{{"docid", ArgInfo{Type::Number, false, "document-id"}}, {"msgid", ArgInfo{Type::String, false, "message-id"}}, {"flags", ArgInfo{Type::String, false, "new flags for the message"}}, {"maildir", ArgInfo{Type::String, false, "the target maildir" }}, @@ -1239,7 +1285,7 @@ make_command_map (Context& context) cmap.emplace("remove", CommandInfo{ - ArgMap{ {"docid", ArgInfo{Type::Integer, true, + ArgMap{ {"docid", ArgInfo{Type::Number, true, "document-id for the message to remove" }}}, "remove a message from filesystem and database", [&](const auto& params){remove_handler(context, params);}}); @@ -1254,7 +1300,7 @@ make_command_map (Context& context) cmap.emplace("view", CommandInfo{ - ArgMap{{"docid", ArgInfo{Type::Integer, false, "document-id"}}, + ArgMap{{"docid", ArgInfo{Type::Number, false, "document-id"}}, {"msgid", ArgInfo{Type::String, false, "message-id"}}, {"path", ArgInfo{Type::String, false, "message filesystem path"}}, @@ -1277,7 +1323,7 @@ mu_cmd_server (MuConfig *opts, GError **err) try if (opts->commands) { Context ctx{}; auto cmap = make_command_map(ctx); - invoke(cmap, Sexp::parse("(help :full t)")); + invoke(cmap, Sexp::Node::make("(help :full t)")); return MU_OK; } @@ -1285,7 +1331,7 @@ mu_cmd_server (MuConfig *opts, GError **err) try context.command_map = make_command_map (context); if (opts->eval) { // evaluate command-line command & exit - auto call{Sexp::parse(opts->eval)}; + auto call{Sexp::Node::make(opts->eval)}; invoke(context.command_map, call); return MU_OK; } @@ -1306,10 +1352,11 @@ mu_cmd_server (MuConfig *opts, GError **err) try if (line.find_first_not_of(" \t") == std::string::npos) continue; // skip whitespace-only lines - auto call{Sexp::parse(line)}; - + auto call{Sexp::Node::make(line)}; invoke(context.command_map, call); + save_line(line); + } catch (const Error& er) { std::cerr << ";; error: " << er.what() << "\n"; print_error ((MuError)er.code(), "%s (line was:'%s')", diff --git a/mu4e/mu4e-utils.el b/mu4e/mu4e-utils.el index b97a5295..b057483c 100644 --- a/mu4e/mu4e-utils.el +++ b/mu4e/mu4e-utils.el @@ -671,13 +671,15 @@ This is used by the completion function in mu4e-compose." :size (length contacts)))) (dolist (contact contacts) (cl-incf n) - (let ((address - (if (functionp mu4e-contact-process-function) - (funcall mu4e-contact-process-function (car contact)) - (car contact)))) + (let* ((address (plist-get contact :address)) + (address + (if (functionp mu4e-contact-process-function) + (funcall mu4e-contact-process-function address) + address))) (when address ;; note the explicit deccode; the strings we get are utf-8, ;; but emacs doesn't know yet. - (puthash (decode-coding-string address 'utf-8) (cdr contact) mu4e~contacts)))) + (puthash (decode-coding-string address 'utf-8) + (plist-get contact :rank) mu4e~contacts)))) (setq mu4e~contacts-tstamp (or tstamp "0"))