From 266446b498d5ce5b89605032135084f0fbba7fa9 Mon Sep 17 00:00:00 2001 From: Jim Jones Date: Tue, 26 May 2026 11:18:44 +0200 Subject: [PATCH v25] Add xmlcanonicalize function This adds xmlcanonicalize(doc xml, keep_comments boolean DEFAULT true), which transforms a well-formed XML document into its canonical form according to the W3C Canonical XML 1.1 specification. The canonical form provides a standardized, byte-for-byte reproducible representation useful for document comparison and digital signatures. Author: Jim Jones Reviewed-by: Andrew Dunstan Reviewed-by: Tom Lane Reviewed-by: Pavel Stehule Reviewed-by: vignesh C Reviewed-by: Oliver Ford Reviewed-by: newtglobal postgresql_contributors Reviewed-by: Chapman Flack Discussion: https://www.postgresql.org/message-id/flat/67fa8560-8d61-5d06-8178-fc9c7684db90%40uni-muenster.de --- doc/src/sgml/func/func-xml.sgml | 44 ++++++++++ src/backend/utils/adt/xml.c | 77 ++++++++++++++++ src/include/catalog/pg_proc.dat | 4 + src/test/regress/expected/xml.out | 132 ++++++++++++++++++++++++++++ src/test/regress/expected/xml_1.out | 85 ++++++++++++++++++ src/test/regress/expected/xml_2.out | 132 ++++++++++++++++++++++++++++ src/test/regress/sql/xml.sql | 53 +++++++++++ 7 files changed, 527 insertions(+) diff --git a/doc/src/sgml/func/func-xml.sgml b/doc/src/sgml/func/func-xml.sgml index 511bc90852a..cdf053ae1ed 100644 --- a/doc/src/sgml/func/func-xml.sgml +++ b/doc/src/sgml/func/func-xml.sgml @@ -61,6 +61,50 @@ SELECT xmltext('< foo & bar >'); + + <literal>xmlcanonicalize</literal> + + + xmlcanonicalize + + + +xmlcanonicalize ( doc xml [, keep_comments boolean DEFAULT true] ) text + + + + This function transforms a given XML document into its canonical form, + as defined by the W3C Canonical XML 1.1 Specification, which standardizes the document's + structure and syntax to facilitate comparison and digital signatures. + The keep_comments parameter controls whether XML comments from the input document are preserved or discarded. + If omitted, it defaults to true. + + + + The canonical form is always encoded in UTF-8, as required by the + W3C specification. In databases that do not use UTF-8 encoding, + documents containing characters that cannot be represented in the + database encoding will produce an encoding error. + + + + Example: +42'::xml); + xmlcanonicalize +----------------------------------------------------------------------------- + 42 +(1 row) + +SELECT xmlcanonicalize('42'::xml, false); + xmlcanonicalize +----------------------------------------------------------- + 42 +(1 row) +]]> + + + <literal>xmlcomment</literal> diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c index 2c7f778cfdb..4c6c5d0355e 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -46,6 +46,7 @@ #include "postgres.h" #ifdef USE_LIBXML +#include #include #include #include @@ -566,6 +567,82 @@ xmltext(PG_FUNCTION_ARGS) #endif /* not USE_LIBXML */ } +/* + * Canonicalizes the given XML document according to the W3C Canonical XML 1.1 + * specification, using libxml2's xmlC14NDocDumpMemory(). + * + * The input XML must be a well-formed document (not a fragment). The + * canonical form is deterministic and useful for digital signatures and + * comparing logically equivalent XML. + * + * The second argument determines whether comments are preserved + * (true) or omitted (false) in the canonicalized output. + */ +Datum +xmlcanonicalize(PG_FUNCTION_ARGS) +{ +#ifdef USE_LIBXML + xmltype *arg = PG_GETARG_XML_P(0); + bool keep_comments = PG_GETARG_BOOL(1); + text *result; + xmlChar *volatile xmlbuf = NULL; + int nbytes = 0; + volatile xmlDocPtr doc = NULL; + PgXmlErrorContext *xmlerrcxt; + + /* Set up XML error context for proper libxml2 error integration */ + xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); + + PG_TRY(); + { + char *converted; + + /* Parse the input as a full XML document */ + doc = xml_parse(arg, XMLOPTION_DOCUMENT, true, + GetDatabaseEncoding(), NULL, NULL, NULL); + + /* Canonicalize the entire document using C14N 1.1 */ + nbytes = xmlC14NDocDumpMemory(doc, NULL, XML_C14N_1_1, + NULL, keep_comments, + (xmlChar **) &xmlbuf); + + if (nbytes < 0 || xmlbuf == NULL || xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not canonicalize XML document"); + + /* + * C14N always produces UTF-8 output regardless of the database + * encoding. Convert to the server encoding so the result is a + * valid text value. + */ + converted = pg_any_to_server((char *) xmlbuf, nbytes, PG_UTF8); + + result = cstring_to_text(converted); + if (converted != (char *) xmlbuf) + pfree(converted); + } + PG_CATCH(); + { + if (doc) + xmlFreeDoc((xmlDocPtr) doc); + if (xmlbuf) + xmlFree((xmlChar *) xmlbuf); + + pg_xml_done(xmlerrcxt, true); + PG_RE_THROW(); + } + PG_END_TRY(); + + xmlFreeDoc((xmlDocPtr) doc); + xmlFree((xmlChar *) xmlbuf); + pg_xml_done(xmlerrcxt, false); + + PG_RETURN_TEXT_P(result); +#else + NO_XML_SUPPORT(); + return 0; +#endif /* not USE_LIBXML */ +} /* * TODO: xmlconcat needs to merge the notations and unparsed entities diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index be157a5fbe9..7915d49f169 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -9283,6 +9283,10 @@ { oid => '3813', descr => 'generate XML text node', proname => 'xmltext', prorettype => 'xml', proargtypes => 'text', prosrc => 'xmltext' }, +{ oid => '3814', descr => 'generate the canonical form of an XML document', + proname => 'xmlcanonicalize', prorettype => 'text', proargtypes => 'xml bool', + proargnames => '{doc,keep_comments}', proargdefaults => '{true}', + prosrc => 'xmlcanonicalize' }, { oid => '2923', descr => 'map table contents to XML', proname => 'table_to_xml', procost => '100', provolatile => 's', diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out index 449733f8ae9..a4a0e2361d8 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -1879,3 +1879,135 @@ SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); x<P>73</P>0.42truej (1 row) +-- xmlcanonicalize +CREATE TABLE xmlcanonicalize_test (doc xml); +INSERT INTO xmlcanonicalize_test VALUES + (' + + + ]> + + + + + + + &val; + + + + + + 1 + + + 321 + + + + + + + + "0" && value<"10" ?"valid":"error"]]> + '); +SELECT xmlcanonicalize(doc, true) FROM xmlcanonicalize_test; + xmlcanonicalize +------------------------------------------------------------------------------------------------------------------------------------------------- + + + + + + + + + + + 42 + + + + + + + + + + + + 1 + + + + + + 321 + + + + + + + + + + + + + + + + value>"0" && value<"10" ?"valid":"error" + + + + +(1 row) + +SELECT xmlcanonicalize(doc, false) FROM xmlcanonicalize_test; + xmlcanonicalize +------------------------------------------------------------------------------------------------------------------------------------------------- + + + + + + + + + 42 + + + + + + + + + + + + 1 + + + + + + 321 + + + + + + + + + + + + + + + + value>"0" && value<"10" ?"valid":"error" + + +(1 row) + +SELECT xmlcanonicalize(doc, true) = xmlcanonicalize(doc) FROM xmlcanonicalize_test; + ?column? +---------- + t +(1 row) + +SELECT xmlcanonicalize(xmlcanonicalize(doc, true)::xml, true) = xmlcanonicalize(doc, true) FROM xmlcanonicalize_test; + ?column? +---------- + t +(1 row) + +SELECT xmlcanonicalize(doc, NULL) FROM xmlcanonicalize_test; + xmlcanonicalize +----------------- + +(1 row) + +SELECT xmlcanonicalize(NULL, true); + xmlcanonicalize +----------------- + +(1 row) + +\set VERBOSITY terse +SELECT xmlcanonicalize('', true); +ERROR: invalid XML document +SELECT xmlcanonicalize(' ', true); +ERROR: invalid XML document +SELECT xmlcanonicalize('foo', true); +ERROR: invalid XML document +SELECT xmlcanonicalize(''); +ERROR: invalid XML document +SELECT xmlcanonicalize(' '); +ERROR: invalid XML document +SELECT xmlcanonicalize('foo'); +ERROR: invalid XML document +\set VERBOSITY default diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out index a962fce36b9..27af760ae59 100644 --- a/src/test/regress/expected/xml_1.out +++ b/src/test/regress/expected/xml_1.out @@ -1494,3 +1494,88 @@ ERROR: unsupported XML feature LINE 1: SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j':... ^ DETAIL: This functionality requires the server to be built with libxml support. +-- xmlcanonicalize +CREATE TABLE xmlcanonicalize_test (doc xml); +INSERT INTO xmlcanonicalize_test VALUES + (' + + + ]> + + + + + + + &val; + + + + + + 1 + + + 321 + + + + + + + + "0" && value<"10" ?"valid":"error"]]> + '); +ERROR: unsupported XML feature +LINE 2: (' + ^ +DETAIL: This functionality requires the server to be built with libxml support. +SELECT xmlcanonicalize(doc, true) FROM xmlcanonicalize_test; + xmlcanonicalize +----------------- +(0 rows) + +SELECT xmlcanonicalize(doc, false) FROM xmlcanonicalize_test; + xmlcanonicalize +----------------- +(0 rows) + +SELECT xmlcanonicalize(doc, true) = xmlcanonicalize(doc) FROM xmlcanonicalize_test; + ?column? +---------- +(0 rows) + +SELECT xmlcanonicalize(xmlcanonicalize(doc, true)::xml, true) = xmlcanonicalize(doc, true) FROM xmlcanonicalize_test; + ?column? +---------- +(0 rows) + +SELECT xmlcanonicalize(doc, NULL) FROM xmlcanonicalize_test; + xmlcanonicalize +----------------- +(0 rows) + +SELECT xmlcanonicalize(NULL, true); + xmlcanonicalize +----------------- + +(1 row) + +\set VERBOSITY terse +SELECT xmlcanonicalize('', true); +ERROR: unsupported XML feature at character 24 +SELECT xmlcanonicalize(' ', true); +ERROR: unsupported XML feature at character 24 +SELECT xmlcanonicalize('foo', true); +ERROR: unsupported XML feature at character 24 +SELECT xmlcanonicalize(''); +ERROR: unsupported XML feature at character 24 +SELECT xmlcanonicalize(' '); +ERROR: unsupported XML feature at character 24 +SELECT xmlcanonicalize('foo'); +ERROR: unsupported XML feature at character 24 +\set VERBOSITY default diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out index d7c90725cfc..570e17a0417 100644 --- a/src/test/regress/expected/xml_2.out +++ b/src/test/regress/expected/xml_2.out @@ -1865,3 +1865,135 @@ SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); x<P>73</P>0.42truej (1 row) +-- xmlcanonicalize +CREATE TABLE xmlcanonicalize_test (doc xml); +INSERT INTO xmlcanonicalize_test VALUES + (' + + + ]> + + + + + + + &val; + + + + + + 1 + + + 321 + + + + + + + + "0" && value<"10" ?"valid":"error"]]> + '); +SELECT xmlcanonicalize(doc, true) FROM xmlcanonicalize_test; + xmlcanonicalize +------------------------------------------------------------------------------------------------------------------------------------------------- + + + + + + + + + + + 42 + + + + + + + + + + + + 1 + + + + + + 321 + + + + + + + + + + + + + + + + value>"0" && value<"10" ?"valid":"error" + + + + +(1 row) + +SELECT xmlcanonicalize(doc, false) FROM xmlcanonicalize_test; + xmlcanonicalize +------------------------------------------------------------------------------------------------------------------------------------------------- + + + + + + + + + 42 + + + + + + + + + + + + 1 + + + + + + 321 + + + + + + + + + + + + + + + + value>"0" && value<"10" ?"valid":"error" + + +(1 row) + +SELECT xmlcanonicalize(doc, true) = xmlcanonicalize(doc) FROM xmlcanonicalize_test; + ?column? +---------- + t +(1 row) + +SELECT xmlcanonicalize(xmlcanonicalize(doc, true)::xml, true) = xmlcanonicalize(doc, true) FROM xmlcanonicalize_test; + ?column? +---------- + t +(1 row) + +SELECT xmlcanonicalize(doc, NULL) FROM xmlcanonicalize_test; + xmlcanonicalize +----------------- + +(1 row) + +SELECT xmlcanonicalize(NULL, true); + xmlcanonicalize +----------------- + +(1 row) + +\set VERBOSITY terse +SELECT xmlcanonicalize('', true); +ERROR: invalid XML document +SELECT xmlcanonicalize(' ', true); +ERROR: invalid XML document +SELECT xmlcanonicalize('foo', true); +ERROR: invalid XML document +SELECT xmlcanonicalize(''); +ERROR: invalid XML document +SELECT xmlcanonicalize(' '); +ERROR: invalid XML document +SELECT xmlcanonicalize('foo'); +ERROR: invalid XML document +\set VERBOSITY default diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql index a771a441c36..ac94dedc7dd 100644 --- a/src/test/regress/sql/xml.sql +++ b/src/test/regress/sql/xml.sql @@ -677,3 +677,56 @@ SELECT xmltext(' '); SELECT xmltext('foo `$_-+?=*^%!|/\()[]{}'); SELECT xmltext('foo & <"bar">'); SELECT xmltext('x'|| '

73

'::xml || .42 || true || 'j'::char); + +-- xmlcanonicalize +CREATE TABLE xmlcanonicalize_test (doc xml); +INSERT INTO xmlcanonicalize_test VALUES + (' + + + ]> + + + + + + + &val; + + + + + + 1 + + + 321 + + + + + + + + "0" && value<"10" ?"valid":"error"]]> + '); + +SELECT xmlcanonicalize(doc, true) FROM xmlcanonicalize_test; +SELECT xmlcanonicalize(doc, false) FROM xmlcanonicalize_test; +SELECT xmlcanonicalize(doc, true) = xmlcanonicalize(doc) FROM xmlcanonicalize_test; +SELECT xmlcanonicalize(xmlcanonicalize(doc, true)::xml, true) = xmlcanonicalize(doc, true) FROM xmlcanonicalize_test; +SELECT xmlcanonicalize(doc, NULL) FROM xmlcanonicalize_test; +SELECT xmlcanonicalize(NULL, true); + +\set VERBOSITY terse +SELECT xmlcanonicalize('', true); +SELECT xmlcanonicalize(' ', true); +SELECT xmlcanonicalize('foo', true); +SELECT xmlcanonicalize(''); +SELECT xmlcanonicalize(' '); +SELECT xmlcanonicalize('foo'); +\set VERBOSITY default -- 2.54.0