A docx whose document.xml references a relationship id (rId999) that does not exist in word/_rels/document.xml.rels. The library must still load the package without raising, and the dangling link's run text must remain readable; only dereferencing the link's target is allowed to fail.
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/malformed-missing-rel |
|---|---|
| Format | docx |
| Category | malformed-recovery |
| Spec | ecma-376-5-part-2 § 9.2 pkg:Relationship |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
dangling-link-text-presentword/document.xml | existThe fixture's body carries a <w:hyperlink r:id='rId999'> whose inner run text is 'dangling link target' — confirms the corruption is in place. | //w:hyperlink[@r:id='rId999']/w:r/w:t[normalize-space()='dangling link target'] | — | — |
no-matching-relationshipword/_rels/document.xml.rels | absentThe relationships part must NOT carry a matching rId999 entry — otherwise the fixture isn't actually malformed. | //rel:Relationship[@Id='rId999'] | — | — |
No render assertions declared.
scripts/gen_malformed_missing_rel_docx.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/malformed-missing-rel.docx``.
Known-good docx with one hyperlink injected into the body referencing
``rId999`` — a relationship id that is NOT present in
``word/_rels/document.xml.rels``. python-docx must still load the
package (the hyperlink's run text remains readable), and the caller
must fail with a ``KeyError`` only if it asks for the hyperlink's
address — NOT at Document construction.
"""
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 REPRODUCIBLE_DT, rewrite_zip
_REPO_ROOT = Path(__file__).resolve().parent.parent
_OUT = _REPO_ROOT / "fixtures" / "docx" / "malformed-missing-rel.docx"
_HYPERLINK_INJECT = (
b'<w:p><w:hyperlink r:id="rId999"><w:r><w:t>'
b"dangling link target</w:t></w:r></w:hyperlink></w:p>"
)
def _inject(name: str, data: bytes) -> bytes:
if name != "word/document.xml":
return data
# Inject the dangling hyperlink paragraph at the start of the body.
# Use the opening <w:body> tag as an anchor — every python-docx
# generated document writes it on one line with no attributes.
marker = b"<w:body>"
idx = data.find(marker)
if idx < 0:
raise RuntimeError("could not locate <w:body> in document.xml")
insert_at = idx + len(marker)
return data[:insert_at] + _HYPERLINK_INJECT + data[insert_at:]
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("anchor paragraph (keeps document non-empty)")
doc.save(good, reproducible=True)
rewrite_zip(good, _OUT, edit=_inject)
finally:
good.unlink(missing_ok=True)
print(_OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"id": "docx/malformed-missing-rel",
"title": "Malformed input: dangling relationship reference",
"format": "docx",
"category": "malformed-recovery",
"summary": "A docx whose document.xml references a relationship id (rId999) that does not exist in word/_rels/document.xml.rels. The library must still load the package without raising, and the dangling link's run text must remain readable; only dereferencing the link's target is allowed to fail.",
"roles": [
"authoring"
],
"spec": {
"source": "ecma-376-5-part-2",
"clause": "9.2",
"element": "pkg:Relationship",
"notes": "OPC packaging rules require every rel referenced from a part to resolve to a Relationship element inside the part's _rels sidecar. A dangling rel is a spec violation; a tolerant reader loads the package and fails lazily on the specific lookup rather than refusing the whole document."
},
"fixtures": {
"machine": "docx/malformed-missing-rel"
},
"generator": {
"python": "scripts/gen_malformed_missing_rel_docx.py"
},
"assertions": [
{
"id": "dangling-link-text-present",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
},
"xpath": "//w:hyperlink[@r:id='rId999']/w:r/w:t[normalize-space()='dangling link target']",
"must": "exist",
"description": "The fixture's body carries a <w:hyperlink r:id='rId999'> whose inner run text is 'dangling link target' — confirms the corruption is in place."
},
{
"id": "no-matching-relationship",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"rel": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "//rel:Relationship[@Id='rId999']",
"must": "absent",
"description": "The relationships part must NOT carry a matching rId999 entry — otherwise the fixture isn't actually malformed."
}
],
"behavior_assertions": [
{
"id": "default-load-succeeds",
"kind": "library_loads",
"library": "python-docx",
"method": "docx.Document",
"args": [
"{fixture_path}"
],
"description": "python-docx tolerates dangling relationships at load time. The call must return without raising."
},
{
"id": "recover-load-succeeds",
"kind": "library_loads",
"library": "python-docx",
"method": "docx.Document",
"args": [
"{fixture_path}"
],
"kwargs": {
"recover": true
},
"description": "recover=True must also succeed, returning a Document whose paragraphs are iterable and whose dangling-link run text is preserved."
}
]
}
No rendered reference is available for this case.