A docx whose word/document.xml has its final 20 bytes chopped off, leaving the XML not well-formed. Default load() raises an XML-level parse error; recover=True must return a Document whose salvageable paragraphs are still iterable.
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/malformed-document-xml-truncated |
|---|---|
| Format | docx |
| Category | malformed-recovery |
| Spec | ecma-376-5-part-1 § 17.2.2 w:document |
No XPath assertions declared.
No render assertions declared.
scripts/gen_malformed_document_xml_truncated_docx.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/malformed-document-xml-truncated.docx``.
Known-good docx whose ``word/document.xml`` has its trailing bytes
chopped off, leaving the XML syntactically invalid (closing tags
missing). Default ``Document()`` must propagate an error (either a
typed wrapper, or lxml's ``XMLSyntaxError`` — today it's the latter,
which is a library bug: the recovery path exists, so the default path
should wrap the parse error in something descriptive). ``recover=True``
must return a Document whose salvageable paragraphs are still
iterable.
The corruption: drop the final 20 bytes (enough to eat the closing
``</w:document>`` plus the prior ``</w:body>``). The xml-declaration
line at the top stays intact so lxml's parser enters XML mode before
it hits the truncation.
"""
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
from docx import Document
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _malformed_common import rewrite_zip
_REPO_ROOT = Path(__file__).resolve().parent.parent
_OUT = _REPO_ROOT / "fixtures" / "docx" / "malformed-document-xml-truncated.docx"
_TRUNCATE_BYTES = 20
def _truncate(name: str, data: bytes) -> bytes:
if name == "word/document.xml":
return data[:-_TRUNCATE_BYTES]
return data
def main() -> int:
_OUT.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
good = Path(tmp.name)
try:
doc = Document()
doc.add_paragraph("salvageable paragraph")
doc.add_paragraph("second salvageable paragraph")
doc.save(good, reproducible=True)
rewrite_zip(good, _OUT, edit=_truncate)
finally:
good.unlink(missing_ok=True)
print(_OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"id": "docx/malformed-document-xml-truncated",
"title": "Malformed input: document.xml truncated (not well-formed)",
"format": "docx",
"category": "malformed-recovery",
"summary": "A docx whose word/document.xml has its final 20 bytes chopped off, leaving the XML not well-formed. Default load() raises an XML-level parse error; recover=True must return a Document whose salvageable paragraphs are still iterable.",
"roles": [
"authoring"
],
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.2.2",
"element": "w:document",
"notes": "The word/document.xml part must be a well-formed XML 1.0 document. Truncation breaks well-formedness and is the classic 'partial write / crash mid-save' failure mode."
},
"fixtures": {
"machine": "docx/malformed-document-xml-truncated"
},
"generator": {
"python": "scripts/gen_malformed_document_xml_truncated_docx.py"
},
"behavior_assertions": [
{
"id": "default-load-raises",
"kind": "library_raises",
"library": "python-docx",
"method": "docx.Document",
"args": [
"{fixture_path}"
],
"expected_exception": [
"PackageNotFoundError",
"OpcError",
"XMLSyntaxError"
],
"forbidden_exception": "XMLSyntaxError",
"description": "Default (non-recover) load must raise. The raw lxml XMLSyntaxError is listed as an accepted class so the assertion is satisfiable in principle; forbidden_exception flags the case where the library forwards lxml's exception verbatim rather than wrapping it in a docx-level typed error. python-docx today leaks the raw XMLSyntaxError — a latent bug."
},
{
"id": "recover-load-succeeds",
"kind": "library_loads",
"library": "python-docx",
"method": "docx.Document",
"args": [
"{fixture_path}"
],
"kwargs": {
"recover": true
},
"description": "recover=True must return a Document whose salvageable paragraphs are iterable. The two pre-truncation paragraphs should survive."
}
]
}
No rendered reference is available for this case.