#2018

docx: merge_runs.py drops run-local namespace declarations and writes invalid XML

OpenBichengWangopened 16d ago

skills/docx/scripts/merge_runs.py removes namespace declarations on a source run when merging it into the preceding run. If a retained child uses that namespace prefix, the script reports success but writes XML with an unbound prefix. The direct .docx mode overwrites the input archive with the same invalid document part.

Reproduced at dbd4588f9e1033efb41dad4bef2f7947c8993d44 on Python 3.14.7, defusedxml 0.7.1 and lxml 6.1.3, with dependencies resolved offline.

Reproduction

Run from the repository root with defusedxml installed:

import subprocess
import sys
import tempfile
from pathlib import Path
from xml.etree import ElementTree as ET

w = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
with tempfile.TemporaryDirectory() as tmp:
    root = Path(tmp)
    (root / "word").mkdir()
    doc = root / "word/document.xml"
    doc.write_text(f'''<w:document xmlns:w="{w}">
      <w:body><w:p>
        <w:r><w:t>Hello</w:t></w:r>
        <w:r xmlns:q="{w}"><q:tab/><w:t>world</w:t></w:r>
      </w:p></w:body>
    </w:document>''', encoding="utf-8")
    ET.parse(doc)  # Input is well-formed.
    subprocess.run([
        sys.executable, "skills/docx/scripts/merge_runs.py", str(root)
    ], check=True)
    print(doc.read_text(encoding="utf-8"))
    ET.parse(doc)  # ParseError: unbound prefix

The command exits 0 and prints Merged 1 runs. The output contains <q:tab/>, but its xmlns:q declaration is gone.

Expected behavior and validation

Merging these runs should preserve the tab and namespace binding, leaving a parseable document. Namespace declarations are valid on the run itself; moving the declaration to w:document is a workaround, and that control still produces valid XML after merging.

I also validated the input against the repository's wml.xsd with lxml: it passes. Both unpacked-directory and direct .docx modes reproduce the unbound-prefix failure; both corresponding controls with xmlns:q on the document root pass schema validation after merging. The archive fixture included [Content_Types].xml and _rels/.rels pointing to word/document.xml.

The loss occurs when _merge_run_content moves child elements into the target run and _merge_runs_in removes the source run without retaining its namespace declarations. Preserving the effective bindings on moved children, including conflicting prefix bindings, or declining such merges would avoid writing invalid XML.

This is a bug report rather than a patch because the DOCX skill's license restricts derivative works. The existing comment-part namespace reports (#1821/#1824) concern a different script and do not cover this run-merging failure.

Activity

4
pradeep1401200416d ago

This namespace binding problem can be reproduced. The critical problem is that merge_runs.py succeeds in generating a DOCX file despite an unbound namespace prefix being used in its XML content. Before providing a patch, can a maintainer specify the acceptable contribution process considering the license limitation for the DOCX skill? I am able to look into regression tests for unpacked directory and .docx file modes.

pradeep1401200416d ago

I’d like to help investigate and resolve this issue.

donaldzitzer406-lang15d ago

skills/docx/scripts/merge_runs.py removes namespace declarations on a source run when merging it into the preceding run. If a retained child uses that namespace prefix, the script reports success but writes XML with an unbound prefix. The direct .docx mode overwrites the input archive with the same invalid document part.

Reproduced at dbd4588f9e1033efb41dad4bef2f7947c8993d44 on Python 3.14.7, defusedxml 0.7.1 and lxml 6.1.3, with dependencies resolved offline.

Reproduction

Run from the repository root with defusedxml installed:

import subprocess
import sys
import tempfile
from pathlib import Path
from xml.etree import ElementTree as ET

w = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
with tempfile.TemporaryDirectory() as tmp:
    root = Path(tmp)
    (root / "word").mkdir()
    doc = root / "word/document.xml"
    doc.write_text(f'''<w:document xmlns:w="{w}">
      <w:body><w:p>
        <w:r><w:t>Hello</w:t></w:r>
        <w:r xmlns:q="{w}"><q:tab/><w:t>world</w:t></w:r>
      </w:p></w:body>
    </w:document>''', encoding="utf-8")
    ET.parse(doc)  # Input is well-formed.
    subprocess.run([
        sys.executable, "skills/docx/scripts/merge_runs.py", str(root)
    ], check=True)
    print(doc.read_text(encoding="utf-8"))
    ET.parse(doc)  # ParseError: unbound prefix

The command exits 0 and prints Merged 1 runs. The output contains <q:tab/>, but its xmlns:q declaration is gone.

Expected behavior and validation

Merging these runs should preserve the tab and namespace binding, leaving a parseable document. Namespace declarations are valid on the run itself; moving the declaration to w:document is a workaround, and that control still produces valid XML after merging.

I also validated the input against the repository's wml.xsd with lxml: it passes. Both unpacked-directory and direct .docx modes reproduce the unbound-prefix failure; both corresponding controls with xmlns:q on the document root pass schema validation after merging. The archive fixture included [Content_Types].xml and _rels/.rels pointing to word/document.xml.

The loss occurs when _merge_run_content moves child elements into the target run and _merge_runs_in removes the source run without retaining its namespace declarations. Preserving the effective bindings on moved children, including conflicting prefix bindings, or declining such merges would avoid writing invalid XML.

This is a bug report rather than a patch because the DOCX skill's license restricts derivative works. The existing comment-part namespace reports (#1821/#1824) concern a different script and do not cover this run-merging failure.

donaldzitzer406-lang15d ago

I’d like to help investigate and resolve this issue.