docx/malformed-document-xml-truncated

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: —

Metadata

Feature iddocx/malformed-document-xml-truncated
Formatdocx
Categorymalformed-recovery
Spececma-376-5-part-1 § 17.2.2 w:document

XPath assertions

No XPath assertions declared.

Render assertions

No render assertions declared.

Generator source

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())

Manifest (expanded)

{
  "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."
    }
  ]
}

Fixture

Download malformed-document-xml-truncated.docx (18.6 KB)

Reference preview

No rendered reference is available for this case.

Spec 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.