From 3bc92d03c49fec83087ede1b99e90b3547220e2b Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:33:15 +0200 Subject: [PATCH] [PR #12148/5004c9f7 backport][stable-13] xml: preserve DOCTYPE declaration when writing XML files (#12249) xml: preserve DOCTYPE declaration when writing XML files (#12148) * fix(xml): preserve DOCTYPE declaration when writing XML files Pass `doctype=tree.docinfo.doctype` to all `ElementTree.write()` calls so lxml does not silently drop the DOCTYPE on serialization. Also replace `etree.tostring()` with BytesIO+write() in the diff and xmlstring paths for consistency. Fixes #2762 * test(xml): add integration test for DOCTYPE preservation * feat(changelog): add fragment for xml DOCTYPE fix (#12148) --------- (cherry picked from commit 5004c9f70ff20205c25d825407147d4c7a21f174) Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../fragments/12148-xml-preserve-doctype.yml | 2 + plugins/modules/xml.py | 46 +++++++++++++++---- .../xml/fixtures/ansible-xml-doctype.xml | 3 ++ .../fixtures/ansible-xml-doctype.xml.license | 3 ++ .../xml/results/test-preserve-doctype.xml | 3 ++ .../results/test-preserve-doctype.xml.license | 3 ++ tests/integration/targets/xml/tasks/main.yml | 1 + .../xml/tasks/test-preserve-doctype.yml | 46 +++++++++++++++++++ 8 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 changelogs/fragments/12148-xml-preserve-doctype.yml create mode 100644 tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml create mode 100644 tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license create mode 100644 tests/integration/targets/xml/results/test-preserve-doctype.xml create mode 100644 tests/integration/targets/xml/results/test-preserve-doctype.xml.license create mode 100644 tests/integration/targets/xml/tasks/test-preserve-doctype.yml diff --git a/changelogs/fragments/12148-xml-preserve-doctype.yml b/changelogs/fragments/12148-xml-preserve-doctype.yml new file mode 100644 index 0000000000..4381f1e91e --- /dev/null +++ b/changelogs/fragments/12148-xml-preserve-doctype.yml @@ -0,0 +1,2 @@ +bugfixes: + - "xml - preserve DOCTYPE declaration when writing modified XML files (https://github.com/ansible-collections/community.general/issues/2762, https://github.com/ansible-collections/community.general/pull/12148)." diff --git a/plugins/modules/xml.py b/plugins/modules/xml.py index 545246a3cf..66169f753b 100644 --- a/plugins/modules/xml.py +++ b/plugins/modules/xml.py @@ -813,9 +813,15 @@ def children_to_nodes(module=None, children=None, type="yaml"): def make_pretty(module, tree): - xml_string = etree.tostring( - tree, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"] + buf = BytesIO() + tree.write( + buf, + xml_declaration=True, + encoding="UTF-8", + pretty_print=module.params["pretty_print"], + doctype=tree.docinfo.doctype or None, ) + xml_string = buf.getvalue() result = dict( changed=False, @@ -830,7 +836,11 @@ def make_pretty(module, tree): if module.params["backup"]: result["backup_file"] = module.backup_local(module.params["path"]) tree.write( - xml_file, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"] + xml_file, + xml_declaration=True, + encoding="UTF-8", + pretty_print=module.params["pretty_print"], + doctype=tree.docinfo.doctype or None, ) elif module.params["xmlstring"]: @@ -859,10 +869,23 @@ def finish(module, tree, xpath, namespaces, changed=False, msg="", hitcount=0, m if result["changed"]: if module._diff: - result["diff"] = dict( - before=etree.tostring(orig_doc, xml_declaration=True, encoding="UTF-8", pretty_print=True), - after=etree.tostring(tree, xml_declaration=True, encoding="UTF-8", pretty_print=True), + before_buf = BytesIO() + orig_doc.write( + before_buf, + xml_declaration=True, + encoding="UTF-8", + pretty_print=True, + doctype=orig_doc.docinfo.doctype or None, ) + after_buf = BytesIO() + tree.write( + after_buf, + xml_declaration=True, + encoding="UTF-8", + pretty_print=True, + doctype=tree.docinfo.doctype or None, + ) + result["diff"] = dict(before=before_buf.getvalue(), after=after_buf.getvalue()) if module.params["path"] and not module.check_mode: if module.params["backup"]: @@ -872,12 +895,19 @@ def finish(module, tree, xpath, namespaces, changed=False, msg="", hitcount=0, m xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"], + doctype=tree.docinfo.doctype or None, ) if module.params["xmlstring"]: - result["xmlstring"] = etree.tostring( - tree, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"] + xmlstring_buf = BytesIO() + tree.write( + xmlstring_buf, + xml_declaration=True, + encoding="UTF-8", + pretty_print=module.params["pretty_print"], + doctype=tree.docinfo.doctype or None, ) + result["xmlstring"] = xmlstring_buf.getvalue() module.exit_json(**result) diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml new file mode 100644 index 0000000000..3b755c1f54 --- /dev/null +++ b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/xml/results/test-preserve-doctype.xml b/tests/integration/targets/xml/results/test-preserve-doctype.xml new file mode 100644 index 0000000000..72aced83d2 --- /dev/null +++ b/tests/integration/targets/xml/results/test-preserve-doctype.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/integration/targets/xml/results/test-preserve-doctype.xml.license b/tests/integration/targets/xml/results/test-preserve-doctype.xml.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/xml/results/test-preserve-doctype.xml.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/xml/tasks/main.yml b/tests/integration/targets/xml/tasks/main.yml index e69a28e578..257f29a100 100644 --- a/tests/integration/targets/xml/tasks/main.yml +++ b/tests/integration/targets/xml/tasks/main.yml @@ -79,6 +79,7 @@ - include_tasks: test-print-match.yml - include_tasks: test-xmlstring.yml - include_tasks: test-children-elements-xml.yml + - include_tasks: test-preserve-doctype.yml # Unicode tests - include_tasks: test-add-children-elements-unicode.yml diff --git a/tests/integration/targets/xml/tasks/test-preserve-doctype.yml b/tests/integration/targets/xml/tasks/test-preserve-doctype.yml new file mode 100644 index 0000000000..5c5b61400b --- /dev/null +++ b/tests/integration/targets/xml/tasks/test-preserve-doctype.yml @@ -0,0 +1,46 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Setup test fixture + copy: + src: fixtures/ansible-xml-doctype.xml + dest: /tmp/ansible-xml-doctype.xml + +- name: Set '/foo/@value' to '1' (file has DOCTYPE) + xml: + path: /tmp/ansible-xml-doctype.xml + xpath: /foo + attribute: value + value: "1" + register: set_attribute_doctype + +- name: Add trailing newline + shell: echo "" >> /tmp/ansible-xml-doctype.xml + +- name: Compare to expected result + copy: + src: results/test-preserve-doctype.xml + dest: /tmp/ansible-xml-doctype.xml + check_mode: true + diff: true + register: comparison + +- name: Test expected result (DOCTYPE is preserved) + assert: + that: + - set_attribute_doctype is changed + - comparison is not changed + +- name: Set '/foo/@value' to '1' again (idempotency) + xml: + path: /tmp/ansible-xml-doctype.xml + xpath: /foo + attribute: value + value: "1" + register: set_attribute_doctype_idempotent + +- name: Test idempotency + assert: + that: + - set_attribute_doctype_idempotent is not changed