diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c index 1f75ffcfd9d..dab00954c6f 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -157,7 +157,6 @@ static bool xml_doctype_in_content(const xmlChar *str); static xmlDocPtr xml_parse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace, int encoding, XmlOptionType *parsed_xmloptiontype, - xmlNodePtr *parsed_nodes, Node *escontext); static text *xml_xmlnodetoxmltype(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt); static int xml_xpathobjtoxmlarray(xmlXPathObjectPtr xpathobj, @@ -285,7 +284,7 @@ xml_in(PG_FUNCTION_ARGS) * Note: we don't need to worry about whether a soft error is detected. */ doc = xml_parse(vardata, xmloption, true, GetDatabaseEncoding(), - NULL, NULL, fcinfo->context); + NULL, fcinfo->context); if (doc != NULL) xmlFreeDoc(doc); @@ -413,7 +412,7 @@ xml_recv(PG_FUNCTION_ARGS) * Parse the data to check if it is well-formed XML data. Assume that * xml_parse will throw ERROR if not. */ - doc = xml_parse(result, xmloption, true, encoding, NULL, NULL, NULL); + doc = xml_parse(result, xmloption, true, encoding, NULL, NULL); xmlFreeDoc(doc); /* Now that we know what we're dealing with, convert to server encoding */ @@ -681,7 +680,6 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent) text *volatile result; xmlDocPtr doc; XmlOptionType parsed_xmloptiontype; - xmlNodePtr content_nodes; volatile xmlBufferPtr buf = NULL; volatile xmlSaveCtxtPtr ctxt = NULL; ErrorSaveContext escontext = {T_ErrorSaveContext}; @@ -707,7 +705,7 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent) * libxml2 will fail to indent elements that have whitespace between them. */ doc = xml_parse(data, xmloption_arg, !indent, GetDatabaseEncoding(), - &parsed_xmloptiontype, &content_nodes, + &parsed_xmloptiontype, (Node *) &escontext); if (doc == NULL || escontext.error_occurred) { @@ -772,35 +770,16 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent) xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, "could not save document to xmlBuffer"); } - else if (content_nodes != NULL) + else { /* - * Deal with the case where we have non-singly-rooted XML. - * libxml's dump functions don't work well for that without help. - * We build a fake root node that serves as a container for the - * content nodes, and then iterate over the nodes. + * When we have non-singly-rooted XML, we need to insert newlines + * between the content nodes, else the output is ugly. */ - xmlNodePtr root; - xmlNodePtr oldroot; + xmlNodePtr root = xmlDocGetRootElement(doc); xmlNodePtr newline; - root = xmlNewNode(NULL, (const xmlChar *) "content-root"); - if (root == NULL || xmlerrcxt->err_occurred) - xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, - "could not allocate xml node"); - - /* - * This attaches root to doc, so we need not free it separately... - * but instead, we have to free the old root if there was one. - */ - oldroot = xmlDocSetRootElement(doc, root); - if (oldroot != NULL) - xmlFreeNode(oldroot); - - if (xmlAddChildList(root, content_nodes) == NULL || - xmlerrcxt->err_occurred) - xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, - "could not append xml node list"); + Assert(root != NULL); /* * We use this node to insert newlines in the dump. Note: in at @@ -1035,7 +1014,7 @@ xmlparse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace, Node xmlDocPtr doc; doc = xml_parse(data, xmloption_arg, preserve_whitespace, - GetDatabaseEncoding(), NULL, NULL, escontext); + GetDatabaseEncoding(), NULL, escontext); if (doc) xmlFreeDoc(doc); @@ -1179,7 +1158,7 @@ xml_is_document(xmltype *arg) * We'll report "true" if no soft error is reported by xml_parse(). */ doc = xml_parse((text *) arg, XMLOPTION_DOCUMENT, true, - GetDatabaseEncoding(), NULL, NULL, (Node *) &escontext); + GetDatabaseEncoding(), NULL, (Node *) &escontext); if (doc) xmlFreeDoc(doc); @@ -1772,9 +1751,8 @@ xml_doctype_in_content(const xmlChar *str) * XmlOptionType actually used to parse the input (typically the same as * xmloption_arg, but a DOCTYPE node in the input can force DOCUMENT mode). * - * If parsed_nodes isn't NULL and we parse in CONTENT mode, the list - * of parsed nodes from the xmlParseBalancedChunkMemory call will be returned - * to *parsed_nodes. (It is caller's responsibility to free that.) + * When we parse in CONTENT mode, the resulting document has a dummy root + * node with zero or more child nodes, reflecting the contents found. * * Errors normally result in ereport(ERROR), but if escontext is an * ErrorSaveContext, then "safe" errors are reported there instead, and the @@ -1790,7 +1768,7 @@ xml_doctype_in_content(const xmlChar *str) static xmlDocPtr xml_parse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace, int encoding, - XmlOptionType *parsed_xmloptiontype, xmlNodePtr *parsed_nodes, + XmlOptionType *parsed_xmloptiontype, Node *escontext) { int32 len; @@ -1799,7 +1777,6 @@ xml_parse(text *data, XmlOptionType xmloption_arg, PgXmlErrorContext *xmlerrcxt; volatile xmlParserCtxtPtr ctxt = NULL; volatile xmlDocPtr doc = NULL; - volatile int save_keep_blanks = -1; /* * This step looks annoyingly redundant, but we must do it to have a @@ -1827,10 +1804,10 @@ xml_parse(text *data, XmlOptionType xmloption_arg, PG_TRY(); { bool parse_as_document = false; - int res_code; size_t count = 0; xmlChar *version = NULL; int standalone = 0; + int options; /* Any errors here are reported as hard ereport's */ xmlInitParser(); @@ -1841,6 +1818,8 @@ xml_parse(text *data, XmlOptionType xmloption_arg, else { /* Parse and skip over the XML declaration, if any */ + int res_code; + res_code = parse_xml_decl(utf8string, &count, &version, NULL, &standalone); if (res_code != 0) @@ -1857,35 +1836,31 @@ xml_parse(text *data, XmlOptionType xmloption_arg, parse_as_document = true; } - /* initialize output parameters */ + /* + * Select parse options. + * + * Note that here we try to apply DTD defaults (XML_PARSE_DTDATTR) + * according to SQL/XML:2008 GR 10.16.7.d: 'Default values defined by + * internal DTD are applied'. As for external DTDs, we try to support + * them too (see SQL/XML:2008 GR 10.16.7.e), but that doesn't really + * happen because xmlPgEntityLoader prevents it. + */ + options = XML_PARSE_NOENT | XML_PARSE_DTDATTR + | (preserve_whitespace ? 0 : XML_PARSE_NOBLANKS); + + /* initialize output parameter */ if (parsed_xmloptiontype != NULL) *parsed_xmloptiontype = parse_as_document ? XMLOPTION_DOCUMENT : XMLOPTION_CONTENT; - if (parsed_nodes != NULL) - *parsed_nodes = NULL; if (parse_as_document) { - int options; - /* set up parser context used by xmlCtxtReadDoc */ ctxt = xmlNewParserCtxt(); if (ctxt == NULL || xmlerrcxt->err_occurred) xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, "could not allocate parser context"); - /* - * Select parse options. - * - * Note that here we try to apply DTD defaults (XML_PARSE_DTDATTR) - * according to SQL/XML:2008 GR 10.16.7.d: 'Default values defined - * by internal DTD are applied'. As for external DTDs, we try to - * support them too (see SQL/XML:2008 GR 10.16.7.e), but that - * doesn't really happen because xmlPgEntityLoader prevents it. - */ - options = XML_PARSE_NOENT | XML_PARSE_DTDATTR - | (preserve_whitespace ? 0 : XML_PARSE_NOBLANKS); - doc = xmlCtxtReadDoc(ctxt, utf8string, NULL, /* no URL */ "UTF-8", @@ -1907,29 +1882,61 @@ xml_parse(text *data, XmlOptionType xmloption_arg, } else { - /* set up document that xmlParseBalancedChunkMemory will add to */ + xmlNodePtr root; + + /* + * Create our result document and set up its dummy root node, + * which we'll use as the context node for xmlParseInNodeContext. + */ doc = xmlNewDoc(version); if (doc == NULL || xmlerrcxt->err_occurred) xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, "could not allocate XML document"); - Assert(doc->encoding == NULL); - doc->encoding = xmlStrdup((const xmlChar *) "UTF-8"); - if (doc->encoding == NULL || xmlerrcxt->err_occurred) + root = xmlNewNode(NULL, (const xmlChar *) "content-root"); + if (root == NULL || xmlerrcxt->err_occurred) xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, - "could not allocate XML document"); - doc->standalone = standalone; + "could not allocate xml node"); + xmlDocSetRootElement(doc, root); - /* set parse options --- have to do this the ugly way */ - save_keep_blanks = xmlKeepBlanksDefault(preserve_whitespace ? 1 : 0); + /* + * Leave doc->encoding unset while parsing the UTF-8 content. + * Older libxml2 versions otherwise strip a leading U+FEFF as a + * BOM, even when it followed the XML declaration in the original + * input. libxml2 will treat the data as UTF-8 anyway, and there + * do not seem to be any negative side-effects during parsing. + */ + Assert(doc->encoding == NULL); + doc->standalone = standalone; /* allow empty content */ if (*(utf8string + count)) { - res_code = xmlParseBalancedChunkMemory(doc, NULL, NULL, 0, - utf8string + count, - parsed_nodes); - if (res_code != 0 || xmlerrcxt->err_occurred) + /* for some reason we need "char *" not "xmlChar *" here */ + const char *content = (const char *) (utf8string + count); + xmlNodePtr nodelist = NULL; + xmlParserErrors res_code; + + res_code = xmlParseInNodeContext(root, + content, + strlen(content), + options, + &nodelist); + + /* immediately attach the resulting nodelist to the root */ + if (nodelist) + { + if (xmlAddChildList(root, nodelist) == NULL) + { + /* don't leak the nodelist if we can't attach it */ + xmlFreeNodeList(nodelist); + xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not append xml node list"); + } + /* now, freeing the document will release the nodelist */ + } + + if (res_code != XML_ERR_OK || xmlerrcxt->err_occurred) { xml_errsave(escontext, xmlerrcxt, ERRCODE_INVALID_XML_CONTENT, @@ -1937,6 +1944,16 @@ xml_parse(text *data, XmlOptionType xmloption_arg, goto fail; } } + + /* + * Now install the correct encoding setting. If we don't, some + * aspects of document dumping don't do what we want. + */ + Assert(doc->encoding == NULL); + doc->encoding = xmlStrdup((const xmlChar *) "UTF-8"); + if (doc->encoding == NULL || xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate XML document"); } fail: @@ -1944,8 +1961,6 @@ fail: } PG_CATCH(); { - if (save_keep_blanks != -1) - xmlKeepBlanksDefault(save_keep_blanks); if (doc != NULL) xmlFreeDoc(doc); if (ctxt != NULL) @@ -1957,9 +1972,6 @@ fail: } PG_END_TRY(); - if (save_keep_blanks != -1) - xmlKeepBlanksDefault(save_keep_blanks); - if (ctxt != NULL) xmlFreeParserCtxt(ctxt); @@ -4648,7 +4660,7 @@ wellformed_xml(text *data, XmlOptionType xmloption_arg) * We'll report "true" if no soft error is reported by xml_parse(). */ doc = xml_parse(data, xmloption_arg, true, - GetDatabaseEncoding(), NULL, NULL, (Node *) &escontext); + GetDatabaseEncoding(), NULL, (Node *) &escontext); if (doc) xmlFreeDoc(doc); diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out index 350941f7172..44751d10579 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -682,6 +682,35 @@ SELECT xmlserialize(CONTENT 'text node ' AS text IN (1 row) +-- check some encoding-dependent behaviors +-- (this is mostly checking for libxml2 bugs, not our bugs...) +DO $$ +DECLARE + result text; +BEGIN + IF getdatabaseencoding() = 'UTF8' THEN + -- U+FEFF after an XML declaration is content, not a byte-order mark + result := xmlserialize(CONTENT xmlparse(CONTENT + '' || chr(65279) || 'abc') AS text INDENT); + IF result <> 'abc' THEN + RAISE WARNING 'U+FEFF was not preserved: %', result; + END IF; + -- non-ASCII characters are converted to hex entities if in data + result := xmlserialize(CONTENT xmlparse(CONTENT + 'ni' || chr(241) || 'o') AS text INDENT); + IF result <> 'niño' THEN + RAISE WARNING 'U+F1 was not converted to a hex entity: %', result; + END IF; + -- non-ASCII characters are preserved in tag attributes + result := xmlserialize(CONTENT xmlparse(CONTENT + '') AS text INDENT); + IF result <> '' THEN + RAISE WARNING 'U+F1 was not preserved in a tag: %', result; + END IF; + END IF; +END +$$; +-- IS DOCUMENT SELECT xml 'bar' IS DOCUMENT; ?column? ---------- diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql index ea0438aa45d..1d8b5e82df6 100644 --- a/src/test/regress/sql/xml.sql +++ b/src/test/regress/sql/xml.sql @@ -179,6 +179,36 @@ SELECT xmlserialize(CONTENT '42' AS text SELECT xmlserialize(DOCUMENT ' ' AS text INDENT); SELECT xmlserialize(CONTENT 'text node ' AS text INDENT); +-- check some encoding-dependent behaviors +-- (this is mostly checking for libxml2 bugs, not our bugs...) +DO $$ +DECLARE + result text; +BEGIN + IF getdatabaseencoding() = 'UTF8' THEN + -- U+FEFF after an XML declaration is content, not a byte-order mark + result := xmlserialize(CONTENT xmlparse(CONTENT + '' || chr(65279) || 'abc') AS text INDENT); + IF result <> 'abc' THEN + RAISE WARNING 'U+FEFF was not preserved: %', result; + END IF; + -- non-ASCII characters are converted to hex entities if in data + result := xmlserialize(CONTENT xmlparse(CONTENT + 'ni' || chr(241) || 'o') AS text INDENT); + IF result <> 'niño' THEN + RAISE WARNING 'U+F1 was not converted to a hex entity: %', result; + END IF; + -- non-ASCII characters are preserved in tag attributes + result := xmlserialize(CONTENT xmlparse(CONTENT + '') AS text INDENT); + IF result <> '' THEN + RAISE WARNING 'U+F1 was not preserved in a tag: %', result; + END IF; + END IF; +END +$$; + +-- IS DOCUMENT SELECT xml 'bar' IS DOCUMENT; SELECT xml 'barfoo' IS DOCUMENT; SELECT xml '' IS NOT DOCUMENT;