owntone-server/src/misc_xml.c

401 lines
9.1 KiB
C

/*
* Copyright (C) 2023 Espen Jurgensen
*
* 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 2 of the License, 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., 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
# include <config.h>
#endif
#include <stdio.h> // fopen
#include <stdarg.h> // va_*
#include <ctype.h>
#include <mxml.h>
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
/* --------------------------------- 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)
{
char *term;
if (!str)
return NULL;
while (isspace(*str))
str++;
term = (char *)str + strlen(str);
while (term != str && isspace(*(term - 1)))
term--;
// Dirty write to the const string from mxml
*term = '\0';
return str;
}
/* -------------------------- Wrapper implementation ------------------------ */
char *
xml_to_string(xml_node *top)
{
return mxmlSaveAllocString(top, MXML_NO_CALLBACK);
}
// This works both for well-formed xml strings (beginning with <?xml..) and for
// those that get straight down to business (<foo...)
xml_node *
xml_from_string(const char *string)
{
mxml_node_t *top;
mxml_node_t *node;
top = mxmlNewXML("1.0");
if (!top)
goto error;
node = mxmlLoadString(top, string, MXML_OPAQUE_CALLBACK);
if (!node)
goto error;
return top;
error:
mxmlDelete(top);
return NULL;
}
xml_node *
xml_from_file(const char *path)
{
FILE *fp;
mxml_node_t *top;
mxml_node_t *node;
top = mxmlNewXML("1.0");
if (!top)
goto error;
fp = fopen(path, "r");
node = mxmlLoadFile(top, fp, MXML_OPAQUE_CALLBACK);
fclose(fp);
if (!node)
goto error;
return top;
error:
mxmlDelete(top);
return NULL;
}
void
xml_free(xml_node *top)
{
mxmlDelete(top);
}
xml_node *
xml_get_node(xml_node *top, const char *path)
{
mxml_node_t *node;
mxml_type_t type;
// This example shows why we can't just return the result of mxmlFindPath:
// <?xml version="1.0""?><rss>
// <channel>
// <title><![CDATA[Tissages]]></title>
// 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;
return mxmlGetParent(node);
}
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;
}
// 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:
//
// <foo>FOO FOO</foo><bar>\nBAR BAR \n</bar>
// <foo>FOO FOO</foo><bar><![CDATA[BAR BAR]]></bar>
// <foo>\nFOO FOO\n</foo><bar>\n<![CDATA[BAR BAR]]></bar>
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 = "";
parent = xml_get_node(top, path);
if (!parent)
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;
}
const char *
xml_get_attr(xml_node *top, const char *path, const char *name)
{
mxml_node_t *node = mxmlFindPath(top, path);
return mxmlElementGetAttr(node, name);
}
xml_node *
xml_new_node(xml_node *parent, const char *name, const char *val)
{
if (!parent)
parent = MXML_NO_PARENT;
mxml_node_t *node = mxmlNewElement(parent, name);
if (!val)
return node; // We're done, caller gets an ELEMENT to use as parent
mxmlNewText(node, 0, val);
return node;
}
xml_node *
xml_new_node_textf(xml_node *parent, const char *name, const char *format, ...)
{
char *s = NULL;
va_list va;
mxml_node_t *node;
int ret;
va_start(va, format);
ret = vasprintf(&s, format, va);
va_end(va);
if (ret < 0)
return NULL;
node = xml_new_node(parent, name, s);
free(s);
return node;
}
void
xml_new_text(xml_node *parent, const char *val)
{
mxmlNewText(parent, 0, val);
}