From 75b8f06e25ca5867e92d6dff54df48587bde83de Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 29 May 2024 16:37:30 +0200 Subject: [PATCH] [misc] Replace mxml with libxml2 mxml 4 is binary and source incompatible with 3, and there is no easy way to stay compatible with both. Not great for a library. So replace with libxml2, hopefully that is more stable. Also means we can get rid of all the mxml hacks --- configure.ac | 8 +- src/httpd_rsp.c | 75 ++++----- src/misc_xml.c | 403 +++++++++++++++++------------------------------- src/misc_xml.h | 22 +-- 4 files changed, 186 insertions(+), 322 deletions(-) diff --git a/configure.ac b/configure.ac index 3c6d9682..e387c9dd 100644 --- a/configure.ac +++ b/configure.ac @@ -120,13 +120,7 @@ OWNTONE_MODULES_CHECK([OWNTONE], [ZLIB], [zlib], [deflate], [zlib.h]) OWNTONE_MODULES_CHECK([OWNTONE], [CONFUSE], [libconfuse >= 3.0], [cfg_init], [confuse.h]) OWNTONE_MODULES_CHECK([OWNTONE], [LIBCURL], [libcurl], [curl_global_init], [curl/curl.h]) OWNTONE_MODULES_CHECK([OWNTONE], [LIBSODIUM], [libsodium], [sodium_init], [sodium.h]) - -OWNTONE_MODULES_CHECK([OWNTONE], [MINIXML], [mxml], - [mxmlNewElement], [mxml.h], - [ - dnl See mxml-compat.h - AC_CHECK_FUNCS([mxmlGetOpaque] [mxmlGetText] [mxmlGetType] [mxmlGetFirstChild]) - ]) +OWNTONE_MODULES_CHECK([OWNTONE], [LIBXML2], [libxml-2.0], [xmlInitParser], [libxml/parser.h]) OWNTONE_MODULES_CHECK([COMMON], [SQLITE3], [sqlite3 >= 3.5.0], [sqlite3_initialize], [sqlite3.h], diff --git a/src/httpd_rsp.c b/src/httpd_rsp.c index 42fc5733..dc8fc2f7 100644 --- a/src/httpd_rsp.c +++ b/src/httpd_rsp.c @@ -40,7 +40,7 @@ #include "parsers/rsp_parser.h" #define RSP_VERSION "1.0" -#define RSP_XML_ROOT "?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?" +#define RSP_XML_DECLARATION "" #define F_FULL (1 << 0) #define F_BROWSE (1 << 1) @@ -124,7 +124,7 @@ xml_to_evbuf(struct evbuffer *evbuf, xml_node *tree) char *xml; int ret; - xml = xml_to_string(tree); + xml = xml_to_string(tree, RSP_XML_DECLARATION); if (!xml) { DPRINTF(E_LOG, L_RSP, "Could not finalize RSP reply\n"); @@ -142,44 +142,53 @@ xml_to_evbuf(struct evbuffer *evbuf, xml_node *tree) return 0; } -static void -rsp_xml_response_new(xml_node **xml_ptr, xml_node **response_ptr, int errorcode, const char *errorstring, int records, int totalrecords) +static int +rsp_xml_response_new(xml_node **response_ptr, int errorcode, const char *errorstring, int records, int totalrecords) { - xml_node *xml = xml_new_node(NULL, RSP_XML_ROOT, NULL); - xml_node *response = xml_new_node(xml, "response", NULL); + xml_node *node; + xml_node *response = xml_new_node(NULL, "response", NULL); xml_node *status = xml_new_node(response, "status", NULL); + if (!response || !status) + return -1; + xml_new_node_textf(status, "errorcode", "%d", errorcode); - xml_new_node(status, "errorstring", errorstring); + node = xml_new_node(status, "errorstring", errorstring); + if (errorstring && *errorstring == '\0') + xml_new_text(node, ""); // Prevents sending which the Soundbridge may not understand + xml_new_node_textf(status, "records", "%d", records); xml_new_node_textf(status, "totalrecords", "%d", totalrecords); if (response_ptr) *response_ptr = response; - if (xml_ptr) - *xml_ptr = xml; + + return 0; } static void rsp_send_error(struct httpd_request *hreq, char *errmsg) { - xml_node *xml; + xml_node *response = NULL; int ret; - rsp_xml_response_new(&xml, NULL, 1, errmsg, 0, 0); - ret = xml_to_evbuf(hreq->out_body, xml); - xml_free(xml); + CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 1, errmsg, 0, 0)); + ret = xml_to_evbuf(hreq->out_body, response); if (ret < 0) - { - httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error"); - return; - } + goto error; httpd_header_add(hreq->out_headers, "Content-Type", "text/xml; charset=utf-8"); httpd_header_add(hreq->out_headers, "Connection", "close"); httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); + + xml_free(response); + return; + + error: + httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error"); + xml_free(response); } static int @@ -305,7 +314,6 @@ rsp_request_authorize(struct httpd_request *hreq) static int rsp_reply_info(struct httpd_request *hreq) { - xml_node *xml; xml_node *response; xml_node *info; cfg_t *lib; @@ -317,7 +325,7 @@ rsp_reply_info(struct httpd_request *hreq) lib = cfg_getsec(cfg, "library"); library = cfg_getstr(lib, "name"); - rsp_xml_response_new(&xml, &response, 0, "", 0, 0); + CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 0, "", 0, 0)); info = xml_new_node(response, "info", NULL); @@ -326,7 +334,7 @@ rsp_reply_info(struct httpd_request *hreq) xml_new_node(info, "server-version", VERSION); xml_new_node(info, "name", library); - rsp_send_reply(hreq, xml); + rsp_send_reply(hreq, response); return 0; } @@ -336,7 +344,6 @@ rsp_reply_db(struct httpd_request *hreq) struct query_params qp; struct db_playlist_info dbpli; char **strval; - xml_node *xml; xml_node *response; xml_node *pls; xml_node *pl; @@ -357,7 +364,7 @@ rsp_reply_db(struct httpd_request *hreq) return -1; } - rsp_xml_response_new(&xml, &response, 0, "", qp.results, qp.results); + CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 0, "", qp.results, qp.results)); pls = xml_new_node(response, "playlists", NULL); @@ -386,7 +393,7 @@ rsp_reply_db(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); - xml_free(xml); + xml_free(response); db_query_end(&qp); rsp_send_error(hreq, "Error fetching query results"); return -1; @@ -394,7 +401,7 @@ rsp_reply_db(struct httpd_request *hreq) /* HACK * Add a dummy empty string to the playlists element if there is no data - * to return - this prevents mxml from sending out an empty + * to return - this prevents us from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) @@ -402,7 +409,7 @@ rsp_reply_db(struct httpd_request *hreq) db_query_end(&qp); - rsp_send_reply(hreq, xml); + rsp_send_reply(hreq, response); return 0; } @@ -473,7 +480,6 @@ rsp_reply_playlist(struct httpd_request *hreq) struct query_params qp; const char *param; const char *client_codecs; - xml_node *xml; xml_node *response; xml_node *items; int mode; @@ -538,7 +544,7 @@ rsp_reply_playlist(struct httpd_request *hreq) if (qp.limit && (records > qp.limit)) records = qp.limit; - rsp_xml_response_new(&xml, &response, 0, "", records, qp.results); + CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 0, "", records, qp.results)); // Add a parent items block (all items), and then one item per file items = xml_new_node(response, "items", NULL); @@ -554,7 +560,7 @@ rsp_reply_playlist(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); - xml_free(xml); + xml_free(response); db_query_end(&qp); rsp_send_error(hreq, "Error fetching query results"); return -1; @@ -562,7 +568,7 @@ rsp_reply_playlist(struct httpd_request *hreq) /* HACK * Add a dummy empty string to the items element if there is no data - * to return - this prevents mxml from sending out an empty + * to return - this prevents us from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) @@ -570,7 +576,7 @@ rsp_reply_playlist(struct httpd_request *hreq) db_query_end(&qp); - rsp_send_reply(hreq, xml); + rsp_send_reply(hreq, response); return 0; } @@ -580,7 +586,6 @@ rsp_reply_browse(struct httpd_request *hreq) { struct query_params qp; char *browse_item; - xml_node *xml; xml_node *response; xml_node *items; int records; @@ -643,7 +648,7 @@ rsp_reply_browse(struct httpd_request *hreq) if (qp.limit && (records > qp.limit)) records = qp.limit; - rsp_xml_response_new(&xml, &response, 0, "", records, qp.results); + CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 0, "", records, qp.results)); items = xml_new_node(response, "items", NULL); @@ -660,7 +665,7 @@ rsp_reply_browse(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); - xml_free(xml); + xml_free(response); db_query_end(&qp); rsp_send_error(hreq, "Error fetching query results"); return -1; @@ -668,7 +673,7 @@ rsp_reply_browse(struct httpd_request *hreq) /* HACK * Add a dummy empty string to the items element if there is no data - * to return - this prevents mxml from sending out an empty + * to return - this prevents us from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) @@ -676,7 +681,7 @@ rsp_reply_browse(struct httpd_request *hreq) db_query_end(&qp); - rsp_send_reply(hreq, xml); + rsp_send_reply(hreq, response); return 0; } diff --git a/src/misc_xml.c b/src/misc_xml.c index f343a2a0..d4ddc91f 100644 --- a/src/misc_xml.c +++ b/src/misc_xml.c @@ -15,19 +15,6 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * - * - * About pipe.c - * -------------- - * This module will read a PCM16 stream from a named pipe and write it to the - * input buffer. The user may start/stop playback from a pipe by selecting it - * through a client. If the user has configured pipe_autostart, then pipes in - * the library will also be watched for data, and playback will start/stop - * automatically. - * - * The module will also look for pipes with a .metadata suffix, and if found, - * the metadata will be parsed and fed to the player. The metadata must be in - * the format Shairport uses for this purpose. - * */ #ifdef HAVE_CONFIG_H @@ -36,162 +23,18 @@ #include // fopen #include // va_* -#include +#include // strlen +#include // isspace -#include - -typedef mxml_node_t xml_node; - - -/* ---------------- Compability with older versions of mxml ----------------- */ - -// mxml 2.10 has a memory leak in mxmlDelete, see https://github.com/michaelrsweet/mxml/issues/183 -// - and since this is the version in Ubuntu 18.04 LTS and Raspian Stretch, we -// fix it by including a fixed mxmlDelete here. It should be removed once the -// major distros no longer have 2.10. The below code is msweet's fixed mxml. -#if (MXML_MAJOR_VERSION == 2) && (MXML_MINOR_VERSION <= 10) - -#define mxmlDelete compat_mxmlDelete - -static void -compat_mxml_free(mxml_node_t *node) -{ - int i; - - switch (node->type) - { - case MXML_ELEMENT : - if (node->value.element.name) - free(node->value.element.name); - - if (node->value.element.num_attrs) - { - for (i = 0; i < node->value.element.num_attrs; i ++) - { - if (node->value.element.attrs[i].name) - free(node->value.element.attrs[i].name); - if (node->value.element.attrs[i].value) - free(node->value.element.attrs[i].value); - } - - free(node->value.element.attrs); - } - break; - case MXML_INTEGER : - break; - case MXML_OPAQUE : - if (node->value.opaque) - free(node->value.opaque); - break; - case MXML_REAL : - break; - case MXML_TEXT : - if (node->value.text.string) - free(node->value.text.string); - break; - case MXML_CUSTOM : - if (node->value.custom.data && - node->value.custom.destroy) - (*(node->value.custom.destroy))(node->value.custom.data); - break; - default : - break; - } - - free(node); -} - -__attribute__((unused)) static void -compat_mxmlDelete(mxml_node_t *node) -{ - mxml_node_t *current, - *next; - - if (!node) - return; - - mxmlRemove(node); - for (current = node->child; current; current = next) - { - if ((next = current->child) != NULL) - { - current->child = NULL; - continue; - } - - if ((next = current->next) == NULL) - { - if ((next = current->parent) == node) - next = NULL; - } - compat_mxml_free(current); - } - - compat_mxml_free(node); -} -#endif - -/* For compability with mxml 2.6 */ -#ifndef HAVE_MXMLGETTEXT -__attribute__((unused)) static const char * /* O - Text string or NULL */ -mxmlGetText(mxml_node_t *node, /* I - Node to get */ - int *whitespace) /* O - 1 if string is preceded by whitespace, 0 otherwise */ -{ - if (node->type == MXML_TEXT) - return (node->value.text.string); - else if (node->type == MXML_ELEMENT && - node->child && - node->child->type == MXML_TEXT) - return (node->child->value.text.string); - else - return (NULL); -} -#endif - -#ifndef HAVE_MXMLGETOPAQUE -__attribute__((unused)) static const char * /* O - Opaque string or NULL */ -mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */ -{ - if (!node) - return (NULL); - - if (node->type == MXML_OPAQUE) - return (node->value.opaque); - else if (node->type == MXML_ELEMENT && - node->child && - node->child->type == MXML_OPAQUE) - return (node->child->value.opaque); - else - return (NULL); -} -#endif - -#ifndef HAVE_MXMLGETFIRSTCHILD -__attribute__((unused)) static mxml_node_t * /* O - First child or NULL */ -mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */ -{ - if (!node || node->type != MXML_ELEMENT) - return (NULL); - - return (node->child); -} -#endif - -#ifndef HAVE_MXMLGETTYPE -__attribute__((unused)) static mxml_type_t /* O - Type of node */ -mxmlGetType(mxml_node_t *node) /* I - Node to get */ -{ - return (node->type); -} -#endif +#include +#include +typedef xmlNode xml_node; /* --------------------------------- Helpers -------------------------------- */ -// We get values from mxml via GetOpaque, but that means they can whitespace, -// thus we trim them. A bit dirty, since the values are in principle const. -static const char * -trim(const char *str) +static char * +trim(char *str) { char *term; @@ -205,7 +48,6 @@ trim(const char *str) while (term != str && isspace(*(term - 1))) term--; - // Dirty write to the const string from mxml *term = '\0'; return str; @@ -215,9 +57,25 @@ trim(const char *str) /* -------------------------- Wrapper implementation ------------------------ */ char * -xml_to_string(xml_node *top) +xml_to_string(xml_node *top, const char *xml_declaration) { - return mxmlSaveAllocString(top, MXML_NO_CALLBACK); + xmlBuffer *buf; + char *s; + + buf = xmlBufferCreate(); + if (!buf) + return NULL; + + if (xml_declaration) + xmlBufferWriteChar(buf, xml_declaration); + + xmlNodeDump(buf, top->doc, top, 0, 0); + + s = strdup((char *)buf->content); + + xmlBufferFree(buf); + + return s; } // This works both for well-formed xml strings (beginning with doc); } xml_node * -xml_get_node(xml_node *top, const char *path) +xml_get_child(xml_node *top, const char *name) { - mxml_node_t *node; - mxml_type_t type; + xml_node *cur; - // This example shows why we can't just return the result of mxmlFindPath: - // - // - // <![CDATA[Tissages]]> - // mxmlFindPath(top, "rss/channel") will return an OPAQUE node where the - // opaque value is just the whitespace. What we want is the ELEMENT parent, - // because that's the one we can use to search for children nodes ("title"). - node = mxmlFindPath(top, path); - type = mxmlGetType(node); - if (type == MXML_ELEMENT) - return node; + for (cur = xmlFirstElementChild(top); cur; cur = xmlNextElementSibling(cur)) + { + if (xmlStrEqual(BAD_CAST name, cur->name)) + break; + } - return mxmlGetParent(node); + return cur; } xml_node * xml_get_next(xml_node *top, xml_node *node) { - const char *name; - const char *s; - - name = mxmlGetElement(node); - if (!name) - return NULL; - - while ( (node = mxmlGetNextSibling(node)) ) - { - s = mxmlGetElement(node); - if (s && strcmp(s, name) == 0) - return node; - } - - return NULL; + return xmlNextElementSibling(node); } -// Walks through the children of the "path" node until it finds one that is -// not just whitespace and returns a trimmed value (except for CDATA). Means -// that these variations will all give the same result: +// We don't use xpath because I couldn't figure how to make it search in a node +// subtree instead of in the entire xmlDoc + it is more complex than the below. +// If the XML is value then both path = "foo/bar" and path +// = "bar" (so a path without the top element) will return "value". +xml_node * +xml_get_node(xml_node *top, const char *path) +{ + xml_node *node = top; + char *path_cpy; + char *needle; + char *ptr; + + if (!top) + return NULL; + if (!path) + return top; + + path_cpy = strdup(path); + + needle = strtok_r(path_cpy, "/", &ptr); + if (!needle) + node = NULL; + else if (xmlStrEqual(BAD_CAST needle, node->name)) + needle = strtok_r(NULL, "/", &ptr); // Descend one level down the path + + while (node && needle) + { + node = xml_get_child(node, needle); + needle = strtok_r(NULL, "/", &ptr); + } + + free(path_cpy); + + return node; +} + +// These variations will all give the same result: // // FOO FOO\nBAR BAR \n // FOO FOO @@ -325,50 +174,69 @@ xml_get_next(xml_node *top, xml_node *node) const char * xml_get_val(xml_node *top, const char *path) { - mxml_node_t *parent; - mxml_node_t *node; - mxml_type_t type; - const char *s = ""; + xml_node *node; - parent = xml_get_node(top, path); - if (!parent) + node = xml_get_node(top, path); + if (!node || !node->children) return NULL; - for (node = mxmlGetFirstChild(parent); node; node = mxmlGetNextSibling(node)) - { - type = mxmlGetType(node); - if (type == MXML_OPAQUE) - s = trim(mxmlGetOpaque(node)); - else if (type == MXML_ELEMENT) - s = mxmlGetCDATA(node); - - if (s && *s != '\0') - break; - } - - return s; + return trim((char *)node->children->content); } const char * xml_get_attr(xml_node *top, const char *path, const char *name) { - mxml_node_t *node = mxmlFindPath(top, path); + xml_node *node; + xmlAttr *prop; - return mxmlElementGetAttr(node, name); + node = xml_get_node(top, path); + if (!node) + return NULL; + + prop = xmlHasProp(node, BAD_CAST name); + if (!prop || !prop->children) + return NULL; + + return trim((char *)prop->children->content); +} + +xml_node * +xml_new(void) +{ + xmlDoc *doc; + + doc = xmlNewDoc(BAD_CAST "1.0"); + if (!doc) + return NULL; + + return xmlDocGetRootElement(doc); } xml_node * xml_new_node(xml_node *parent, const char *name, const char *val) { - if (!parent) - parent = MXML_NO_PARENT; + xml_node *node; + xmlDoc *doc = NULL; - mxml_node_t *node = mxmlNewElement(parent, name); - if (!val) - return node; // We're done, caller gets an ELEMENT to use as parent + doc = parent ? parent->doc : xmlNewDoc(BAD_CAST "1.0"); + if (!doc) + goto error; + + node = xmlNewDocNode(doc, NULL, BAD_CAST name, BAD_CAST val); + if (!node) + return NULL; + + if (parent) + xmlAddChild(parent, node); + else + xmlDocSetRootElement(doc, node); - mxmlNewText(node, 0, val); return node; + + error: + if (!parent) + xmlFreeDoc(doc); + return NULL; } xml_node * @@ -376,7 +244,7 @@ xml_new_node_textf(xml_node *parent, const char *name, const char *format, ...) { char *s = NULL; va_list va; - mxml_node_t *node; + xml_node *node; int ret; va_start(va, format); @@ -393,8 +261,17 @@ xml_new_node_textf(xml_node *parent, const char *name, const char *format, ...) return node; } -void +xml_node * xml_new_text(xml_node *parent, const char *val) { - mxmlNewText(parent, 0, val); + xml_node *node; + + if (!parent) + return NULL; + + node = xmlNewDocText(parent->doc, BAD_CAST val); + if (!node) + return NULL; + + return xmlAddChild(parent, node); } diff --git a/src/misc_xml.h b/src/misc_xml.h index ac34edbd..38ab7e0e 100644 --- a/src/misc_xml.h +++ b/src/misc_xml.h @@ -1,55 +1,43 @@ #ifndef SRC_MISC_XML_H_ #define SRC_MISC_XML_H_ -// This wraps mxml and adds some convenience functions. This also means that -// callers don't need to concern themselves with changes and bugs in various -// versions of mxml. +// This wraps libxml2 and adds some convenience functions typedef void xml_node; -// Wraps mxmlSaveAllocString. Returns NULL on error. char * -xml_to_string(xml_node *top); +xml_to_string(xml_node *top, const char *xml_declaration); -// Wraps mxmlNewXML and mxmlLoadString, so creates an xml struct with the parsed -// content of string. Returns NULL on error. xml_node * xml_from_string(const char *string); -// Wraps mxmlNewXML and mxmlLoadFile, so creates an xml struct with the parsed -// content of string. Returns NULL on error. xml_node * xml_from_file(const char *path); -// Wraps mxmlDelete, which will free node + underlying nodes void xml_free(xml_node *top); -// Wraps mxmlFindPath. xml_node * xml_get_node(xml_node *top, const char *path); -// Wraps mxmlGetNextSibling, but only returns sibling nodes that have the same -// name as input node. +// Only returns sibling nodes that have the same name as input node xml_node * xml_get_next(xml_node *top, xml_node *node); -// Wraps mxmlFindPath and mxmlGetOpaque + mxmlGetCDATA. Returns NULL if nothing -// can be found. const char * xml_get_val(xml_node *top, const char *path); -// Wraps mxmlFindPath and mxmlElementGetAttr. Returns NULL if nothing can be -// found. const char * xml_get_attr(xml_node *top, const char *path, const char *name); +// Will create a new XML document with the node as root if parent is NULL xml_node * xml_new_node(xml_node *parent, const char *name, const char *val); xml_node * xml_new_node_textf(xml_node *parent, const char *name, const char *format, ...); +// Adds a text node to parent, which must be an element node void xml_new_text(xml_node *parent, const char *val);