Design doc (read in full first): docs/design/docx_io_package_split.md — §2, §4 Slice 3.
Slice 3 of 5 of the docx_io.py package split. Highest line-count but most mechanical slice — self-contained, and already exercised end-to-end by tests/test_metadata_scrubbing.py.
Scope
Move _rewrite_docx_zip() (docx_io.py:382) and its ~20 nested closures into marcut/docx/zip_postprocess.py, converting the closures to module-level (or small-class) functions taking explicit parameters instead of closing over method-local state.
The closures include (per §1 of the doc): relationship-target sanitization (_is_unc_path, _is_user_path, _is_file_path, _is_internal_url, _sanitize_target); JPEG/PNG binary metadata stripping (_strip_jpeg_metadata, _strip_png_metadata); namespace/element pruning (_remove_lang_elements, _strip_form_defaults, _strip_alternate_content, _strip_nonstandard_elements); custom-style renaming (_rename_custom_styles, _update_style_references); chart-label redaction (_clean_chart_labels); .rels/[Content_Types].xml rewriting (_scrub_rels, _update_content_types); and the final orphan-detection + ZIP-rewrite loop.
This code operates only on raw bytes / zipfile.ZipFile / lxml.etree — it must never import docx.Document. It takes a file path and a MetadataCleaningSettings and mutates the file on disk; it has no dependency on DocxMap instance state beyond those parameters.
Non-negotiable requirements
- Behavior-identical. This is the metadata-scrubbing critical path — a dropped closure or a mis-wired parameter can silently stop scrubbing a relationship target, hidden run, chart label, or image's binary metadata while
save() still "succeeds". The golden-file harness (#70) diffing to zero is the gate that proves this.
- No new
python-docx import in zip_postprocess.py — it stays a raw-bytes/zipfile/lxml pass, matching how it already behaves as a path-in/path-out post-processor.
- Preserve
_safe_fromstring usage (now imported from marcut/docx/xml_utils.py per #73 if that landed first; otherwise from its current location).
- Use the package name agreed in
#72.
Acceptance criteria
Verification loop (design doc §3.3)
PYTHONPATH=src/python python3 -m pytest tests/test_docx_io.py tests/test_metadata_scrubbing.py tests/test_pipeline.py tests/test_url_redaction.py tests/test_failure_scenarios.py tests/test_security.py -q.
- Golden-file harness — empty diff.
marcut redact against a sample-files/ fixture, diffed vs baseline.
Plus the standard gate: full pytest -q, swift build, swift test, bash scripts/release_preflight.sh.
Dependencies
Blocked by #70. Best sequenced after #73 (so it can import _safe_fromstring from xml_utils.py), but per the design doc slices are technically independent — if it lands before B2, import _safe_fromstring from its current location and update in B2.
Land as its own PR; do not combine with other issues in this set. Do not reopen design-spike #25.
Design doc (read in full first):
docs/design/docx_io_package_split.md— §2, §4 Slice 3.Slice 3 of 5 of the
docx_io.pypackage split. Highest line-count but most mechanical slice — self-contained, and already exercised end-to-end bytests/test_metadata_scrubbing.py.Scope
Move
_rewrite_docx_zip()(docx_io.py:382) and its ~20 nested closures intomarcut/docx/zip_postprocess.py, converting the closures to module-level (or small-class) functions taking explicit parameters instead of closing over method-local state.The closures include (per §1 of the doc): relationship-target sanitization (
_is_unc_path,_is_user_path,_is_file_path,_is_internal_url,_sanitize_target); JPEG/PNG binary metadata stripping (_strip_jpeg_metadata,_strip_png_metadata); namespace/element pruning (_remove_lang_elements,_strip_form_defaults,_strip_alternate_content,_strip_nonstandard_elements); custom-style renaming (_rename_custom_styles,_update_style_references); chart-label redaction (_clean_chart_labels);.rels/[Content_Types].xmlrewriting (_scrub_rels,_update_content_types); and the final orphan-detection + ZIP-rewrite loop.This code operates only on raw bytes /
zipfile.ZipFile/lxml.etree— it must never importdocx.Document. It takes a filepathand aMetadataCleaningSettingsand mutates the file on disk; it has no dependency onDocxMapinstance state beyond those parameters.Non-negotiable requirements
save()still "succeeds". The golden-file harness (#70) diffing to zero is the gate that proves this.python-docximport inzip_postprocess.py— it stays a raw-bytes/zipfile/lxmlpass, matching how it already behaves as apath-in/path-out post-processor._safe_fromstringusage (now imported frommarcut/docx/xml_utils.pyper#73if that landed first; otherwise from its current location).#72.Acceptance criteria
_rewrite_docx_zip+ closures moved tomarcut/docx/zip_postprocess.pyas explicit-parameter functions; nodocx.Documentimport there.DocxMap._postprocess_zip/save()call into the new module; publicDocxMapsurface unchanged.tests/test_metadata_scrubbing.pypasses with only import-path edits.#70) diffs to zero (this is the primary safety proof for this slice).marcut redactrun diffed against pre-slice baseline → no difference.Verification loop (design doc §3.3)
PYTHONPATH=src/python python3 -m pytest tests/test_docx_io.py tests/test_metadata_scrubbing.py tests/test_pipeline.py tests/test_url_redaction.py tests/test_failure_scenarios.py tests/test_security.py -q.marcut redactagainst asample-files/fixture, diffed vs baseline.Plus the standard gate: full
pytest -q,swift build,swift test,bash scripts/release_preflight.sh.Dependencies
Blocked by
#70. Best sequenced after#73(so it can import_safe_fromstringfromxml_utils.py), but per the design doc slices are technically independent — if it lands before B2, import_safe_fromstringfrom its current location and update in B2.Land as its own PR; do not combine with other issues in this set. Do not reopen design-spike #25.