Skip to content

id(web): link/image destinations containing a tab or newline are corrupted on save — tab becomes %09, newline destroys the link (the #1019 fix only handles spaces) #1423

Closed

Description

@dezren39

Summary

The #1019 fix for spaced link/image destinations only triggers on a literal space. Destinations containing a tab (authored or editor-originated) or a newline (editor-originated) take the unprotected path and are destroyed on save:

  • Tab: [x](</a\tb>) parses to a link with href /a\tb, but saves as [x](/a% 9b) — comrak emits a %, a space, and a partial hex digit, which is not valid percent-encoding at all. On reopen the link is gone: the saved text parses as plain text. Images (![a](</x\tb.png>) → ![a](/x% 9b.png)) and reference definitions ([r]: </a\tb>) fail identically.
  • Newline (PM-originated): a link mark whose href contains \n (e.g. set via the editor) saves as [x](/a% Ab) and likewise reopens as plain text; an image whose src contains \n saves as ![a](/x% Ab.png) with the same result.

Evidence

Verified on main 37418e56 with a verbatim-codec probe crate (comrak 0.56.0, compiled and run on the laptop; logs hidden_files/probe-2026-10-27-1027-laneA-out.log (probe8) and the probe9 run, cases probe-2026-10-27-1027-laneA-probe8.rs / probe9.rs):

=== angle-dest-tab pm_same=false
IN : "[x](</a\tb>)\n"          PM: link mark href "/a\tb"
OUT: "[x](/a% 9b)\n"          PM2: plain text "[x](/a% 9b)"  <- link destroyed, non-fixpoint
=== image-angle-dest-tab / ref-def-angle-tab / pm-link-href-tab / pm-image-src-tab: identical pattern
=== pm-image-src-newline pm_same=false
PM : image src "/x\nb.png"
OUT: "![a](/x% Ab.png)\n"     PM2: plain text                <- image destroyed
(PM link href "/a\nb" behaves the same: OUT "[x](/a% Ab)\n", PM2 plain text.)

A direct comrak parse→render control produces the same % 9 output for the tab case, so the mangling itself is the renderer's — the codec defect is that its #1019 protection does not cover these destinations, exactly as it did not cover spaces before #1019.

Scope correction vs. the original filing: an authored angle-bracket destination containing a raw newline ([x](</a\nb>)) is not parsed as a link by comrak in the first place (it is plain text on both parse and save), so there is no authored-newline corruption — the newline case is editor-originated hrefs/srcs only. The tab case affects authored files directly.

Root cause

pkgs/id/src/web/markdown.rs:1091 — substitute_spaced_destinations (the #1019 placeholder mechanism, :1081-:1112) skips any destination that does not contain a space:

if !url.contains(' ') {
    continue;
}

Tab- and newline-bearing destinations therefore reach comrak's renderer unprotected. The placeholder-restore step writes the raw destination back in angle form <{raw}> (:1070-:1072), which represents tabs byte-identically (the probe's first parse proves [x](</a\tb>) parses to exactly that href). One caution: a destination containing a newline and literal document text matching the placeholder pattern interacts with #1348 (placeholder collision); the widened trigger should keep the placeholder tokens collision-safe.

Why it matters

Angle-bracket destinations exist precisely for destinations the bare form cannot represent. A filename containing a tab is unusual but legal on Linux, and such a document is silently corrupted by simply opening it in the id web editor and saving — the link/image does not just get rewritten, it stops being a link/image at all. Same silent-corruption class as #1019, one whitespace character over.

Acceptance criteria

Relates to: #1019 (space case, fixed), #1348 (placeholder collision).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 27, 2026
  2. developing-today commented on Oct 27, 2026

    @developing-today
    Contributor
  3. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · correction

    • Agent: new-issue-creator (lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-27 09:44 CDT
    • Found via: filed this run; evidence corrected this run
    • Correction: the original body overstated two points; the issue body has been edited to the verified record. (1) The tab case saves as [x](/a% 9b) — a %, a space, and a partial hex digit — not %09, and the link/image is destroyed on reopen (plain text), not merely mutated. Verified for authored angle links/images/reference definitions and editor-originated hrefs/srcs (probe8 + probe9, laptop, main 37418e5). (2) The newline case is editor-originated only: an authored [x](</a\nb>) is not parsed as a link by comrak at all (plain text both ways), so there is no authored-newline corruption; a PM link href / image src containing \n does save as the broken % A… form and loses the link/image on reopen. The core defect and fix direction are unchanged: substitute_spaced_destinations (markdown.rs:1091) only protects destinations containing a literal space. Queue packet 1425 carries the original wording in its body text; this comment + the edited issue body are canonical.
  5. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 27, 2026
  6. 177 remaining items

  7. added
    status/parkedDeliberately paused with a recorded resume condition
    on Oct 28, 2026
  8. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

    • Agent: queue worker (repo-worker-code), lane C
    • Session / ID: queue task 1489
    • Found via: queue claim (resumed from parked awaiting_ci)
    • Task: PR 1543's run 40639431029 is moving again — at the 08:26Z recheck after a 5-minute poll: 22 SUCCESS, 3 in progress, 6 queued, 0 failures (pending: rustfmt, web-fmt, test-unit, test-shard-3, crash-gates, doc, aggregate). I did NOT cancel+rerun: the checkpoint's premise (run untouched) no longer holds, and a rerun would discard 22 green checks.
    • Expectation: parked 20 min; resume = merge PR 1543 when the run finishes green, complete fixed_merged
    • Links: PR fix(id): widen spaced-destination protection to tab/newline destinations (#1423) #1543 @ c5e0491
  9. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 28, 2026
  10. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: queue worker (repo-worker-code), lane C
    • Session / ID: queue task 1489 (second resume this shift)
    • Found via: queue claim
    • Task: PR 1543's run has finished its constituents — all 31 checks SUCCESS incl test-unit and all test shards; only the aggregate summary job remains queued. Merging on the verified constituents if the summary job stalls.
    • Expectation: done = PR 1543 merged, task completed fixed_merged
    • Links: PR fix(id): widen spaced-destination protection to tab/newline destinations (#1423) #1543 @ c5e0491
  11. added a commit that references this issue on Oct 28, 2026
  12. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions