Skip to content

fix(merge): reserve the first input's unresolved item ids from later streams (#463) - #500

Open
entelecheia wants to merge 1 commit into
mainfrom
fix/463-merge-bin-shadows
Open

entelecheia wants to merge 1 commit into
mainfrom
fix/463-merge-bin-shadows

Conversation

@entelecheia

Copy link
Copy Markdown
Member

Summary

merge now reserves the first input's unresolved picture and shape-caption ItemRef spellings
before it grafts any later input, so a later input's stream cannot take a name that would answer
one of them.

Closes #463.

The bug

Document::bin_stream_index resolves an unlisted ItemRef towards the streams: exact name,
/{name} suffix, or filename stem. The graft's namespace check
(bin_namespace_name_taken) tested the opposite direction — whether an existing (or already
carried) name contains the candidate — so a later input's stream whose name answered an
unresolved primary spelling was appended unchanged.

A/B example:

  • A: a picture saying ItemRef("logo"); no manifest entry, no stream, so A alone renders
    nothing and reports BinaryAssetRemoved.
  • B: carries BinData/logo.png.

Before: after merge a b, A's picture resolved through the stem rule to B's payload, so A
rendered B's image and the merged output reported no missing asset. After: B's stream is
renamed and A's reference stays unresolved, so both writers ledger it (BinaryAssetRemoved in
the HWPX writer and in the merge loss report; PictureControlRemoved in the HWP5 report).

What changed

crates/hwp-convert/src/document_merge.rs:

  • unresolved_primary_spellings(doc) collects every modeled ItemRef spelling (body paragraphs,
    table cells, captions, generic paragraph lists) that the primary itself cannot resolve, once,
    before grafting.
  • name_answers_spelling(name, spelling) mirrors the resolver's three unlisted-id rules;
    bin_namespace_name_taken treats a candidate as taken when it matches, in addition to the
    existing containment check.
  • The reservation is threaded through graft_bin_streams for every later input, and through
    graft_bin_data/bin_stream_name_taken, because a native payload's canonical BIN#### name
    is a later stream name too: that path remints the storage id instead of the name.
  • The collision counter now goes on the file component (renamed_bin_name). A prefix on the
    whole dir/file name leaves the stem unchanged, so the resolver's stem rule would match the
    rename candidate again and the loop could not terminate.

Nothing is rewritten or refused: the primary's reference stays exactly as it was and the
writers continue to report it. Document::bin_stream_index is untouched.

Tests

Red first, on 7719de2 (current main), before the fix:

  • document_merge::tests::병합_뒤_첫_입력의_미해결_id가_뒤_입력의_스트림으로_해석되지_않는다
    failed at its intended assertion, left: Some([66, 45, 73, 77, 65, 71, 69]) (B's B-IMAGE
    bytes) vs right: None, on both graft tiers.
  • merge_missing_manifest's new cases failed at assert_binary_loss, with a report that held
    only metadata/package events and no BinaryAssetRemoved, for both .hwp and .hwpx output.
  • 병합_뒤_원본_bin_이름도_첫_입력의_미해결_id를_대신하지_않는다 failed at "the native payload must
    be reminted away from the reserved canonical name" when the remint reservation was dropped
    from graft_bin_data alone.

Added:

  • crates/hwp-convert/src/document_merge.rs: two unit tests (unlisted spelling through the
    stem rule on both tiers; canonical BIN#### remint) with fixture sanity checks that the
    primary does not carry what it names and the later input does.
  • crates/hwp-cli/tests/merge_missing_manifest.rs: hwp_merge_unlisted_primary_id_reports_binary_asset_removed
    and hwpx_merge_unlisted_primary_id_reports_binary_asset_removed build a real package whose
    picture names an id with no manifest item and no payload, then assert the published output
    reports BinaryAssetRemoved, the primary's picture never resolves to the later input's bytes,
    and the later input's picture keeps its own bytes under its renamed stream.

Existing merge tests (병합_뒤_입력의_그림은_자기_문서의_매니페스트_id로_자기_바이트를_가리킨다,
병합_뒤_충돌로_이름이_바뀐_스트림도_id로_해석된다, the #417 publication cases) pass unchanged.

Gate

== check: OK (fmt/clippy/test/crate-edges/pdf-runner/structured-corpus/claims/doc-surface/release-block/readiness-selfcheck/skip-accounting/public-parity=skipped) skipped-for-missing-fixtures=49 (optional=4) ==

…streams (#463)

A primary picture whose `ItemRef` names an id the primary does not carry (no
manifest entry, no stream) was silently bound to a later input's payload: the
graft's namespace check only asked whether an existing name contains the
candidate, while `Document::bin_stream_index` resolves a reference towards the
streams by exact name, `/name` suffix or filename stem. Merging A (an
unresolved `logo`) with B (`BinData/logo.png`) therefore made A's picture
render B's bytes and suppressed the `BinaryAssetRemoved` evidence both writers
would otherwise report.

Collect the primary's unresolved `ItemRef` spellings before grafting and treat
a candidate stream name as taken when it would answer one of them. The
colliding stream is renamed on its file component (which also leaves the stem
rule nothing to match), and a native payload is reminted by storage id instead.
The primary's reference stays unresolved; nothing is rewritten or refused.
Copilot AI balanced review requested due to automatic review settings November 10, 2026 17:34
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T17:37:39.421383Z c0b29a8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0b29a88bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1938 to +1942
a.sections[0].paragraphs.insert(
0,
hwp_model::Paragraph {
controls: vec![Control::Picture(sample_picture("logo"))],
..Default::default()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cover unresolved primary references in nested paragraph paths

The new regression tests place the primary's unresolved picture only in a section-level paragraph, although unresolved_primary_spellings must also reserve references inside table cells, picture/table/shape captions, and generic paragraph lists. These tests would still pass if the new collector stopped visiting those nested paths. The existing shape-caption test exercises a valid reference in a later input, so it does not verify this primary-reference reservation behavior. Add nested primary cases that assert the reference remains unresolved and the later input retains its own payload. This is required by the regression-test guidance in AGENTS.md, which explicitly requires visiting every affected path.

AGENTS.md reference: AGENTS.md:L171-L175

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

merge: a missing unlisted image reference in the first input can bind to a later input's stream

2 participants