Skip to content

A fragment on an inlined reference is dropped, so an SVG sprite selector is lost - #804

Merged
xroche merged 11 commits into
masterfrom
fix/inline-fragment-766
Jul 27, 2026
Merged

A fragment on an inlined reference is dropped, so an SVG sprite selector is lost#804
xroche merged 11 commits into
masterfrom
fix/inline-fragment-766

Conversation

@xroche

@xroche xroche commented Jul 27, 2026

Copy link
Copy Markdown
Owner

--single-file resolves a reference by cutting it at #, then substitutes the data: URI it built, so the fragment is gone. On an SVG that is a rendering bug rather than a broken link: sprite.svg#icon picks one element out of a sprite sheet, and without the selector the browser draws the whole sheet. The page still loads, which is why this is easy to miss.

The fragment now comes back onto both replacements, the data: URI and the relative path an over-cap asset falls back to inside an inlined stylesheet. It gets percent-escaped on the way out, because a ) inside a fragment would otherwise close an unquoted CSS url() token; # joined the escape set on the path side for the same reason. Escapes the document already carried (%, &) stay as they are, since encoding them twice changes which id the browser looks for. The query is still dropped: it named the remote resource, not the mirrored file. The lookup keeps cutting at #, so which file gets found is unchanged.

The -#test=singlefile engine self-test covers all four reference shapes, the rebase fallback, and one fragment per member of the escape set, so dropping any single character from that set turns the suite red. A new crawl test covers the whole thing end to end. Both go red on master.

Closes #766

xroche and others added 4 commits July 27, 2026 13:41
sf_resolve cuts a reference at '#' to find the mirrored file, and sf_inline
then replaced the whole reference with the data: URI it built, so the fragment
never came back. On an SVG that changes what renders: "sprite.svg#icon" selects
one element, and without the selector the browser draws the whole sheet.

Re-attach it to both replacements, the data: URI and the rebased path an
over-cap asset falls back to. The query stays dropped: it named the remote
resource, not the mirrored file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
The fragment is copied out of the document, so its '%' and '&' are the
document's own escapes; percent-encoding them again turns "#a&amp;b" into a
lookup for "a&amp;b". A mirrored path is the opposite case, a raw filesystem
name that has to be escaped, '#' included now that a fragment can follow it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
xroche and others added 7 commits July 27, 2026 14:58
Only ')' and the '%'/'&' pass-through were exercised, so dropping '"', a
quote, a paren, a backslash, '<'/'>', whitespace or the high-byte rule from
sf_append_escaped left the suite green. The two that would be a real
injection are the '"' that ends the attribute the rewriter re-quotes and the
whitespace that ends an unquoted url() token; the fixture now carries one
fragment per class, and each one dies to its own mutant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
…-766

Signed-off-by: Xavier Roche <roche@httrack.com>

# Conflicts:
#	tests/Makefile.am
…-766

Signed-off-by: Xavier Roche <roche@httrack.com>

# Conflicts:
#	tests/Makefile.am
Fix the TESTS-tail union merge's missing continuation backslash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Register 112_local-single-file-fragment.test in tests-list.mk after
the #845 TESTS-list restructure moved it out of tests/Makefile.am.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche
xroche merged commit 63af0dc into master Jul 27, 2026
22 checks passed
@xroche
xroche deleted the fix/inline-fragment-766 branch July 27, 2026 18:26
xroche added a commit that referenced this pull request Aug 7, 2026
Resolves three collisions the branch had with what landed since it was cut.

#804 kept a fragment on an inlined reference by parsing it back out of the
reference and re-escaping it. Under the mark design the mark covers the
reference alone, so the fragment is document text the pass never touches and
reaches the data: URI byte-identical; sf_fragment/sf_append_fragment go with
the scanner they were written for. The query has to go the other way -- glued
to base64 it corrupts the payload -- so htsparse now writes the mark after it
and the marked span carries it.

#1011 added a private hts_random_bytes to htsweb.c while this branch added one
to htstools.c. htsserver links libhttrack but cannot see a hidden symbol inside
it, so the two now share htsrandom.c: master's runtime-resolved RtlGenRandom on
Windows, this branch's getrandom-then-/dev/urandom on POSIX.

Master's test 94 then caught a bug the branch shipped: sf_rel_is_stylesheet()
bounded its outer scan at the tag's '>' but handed strstrcase the raw buffer,
so a <link rel="canonical"> on a page carrying any later rel="stylesheet" was
classed as one and the pass inlined the page it named. rel is now matched as a
token inside the value rech_endtoken() delimits. Only a crawl test sees this --
the engine self-test writes the marks it feeds the pass, so the class is an
input there rather than something htsparse decided.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
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.

A fragment on an inlined reference is dropped, so an SVG sprite selector is lost

1 participant