Skip to content

fix(includes): catch a file that includes itself through a symlink - #1339

Merged
yumike merged 1 commit into
mainfrom
fix/include-cycle-identity
Oct 29, 2026
Merged

yumike merged 1 commit into
mainfrom
fix/include-cycle-identity

Conversation

@yumike

@yumike yumike commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Spec PR 6 (case-sensitive paths, "Include cycles"). A file that includes itself through a symlink is now caught on its first repeat. Before, a.md including link.md -> a.md rendered a.md's body once more before the written-path check caught it. When the path differed at every level, only the 64-include cap stopped it.

  • IncludeReader::read returns IncludedFile { text, canonical_path }, and Storage::read_include returns IncludeText with the same fields. FsStorage::confine now returns the canonical file and its path relative to the canonical project. The same strip_prefix is the inside-the-project check, so the relative path has no fallback. The mock storage, the S3 published map and the test readers return the path they were given.
  • The walker's open-file stack keeps both the written project path and the canonical path. The exact written-path check still runs before the read, with the same wording. A canonical-path match after the (memoized) read reports both spellings: include `docs/link.md` is `docs/a.md`, which includes itself. Warnings, RenderResult::includes and relative-path resolution keep the written spelling.
  • The page's own canonical path is its source path. The scan skips symlinks, so a page has a second path only when the docs folder or the README is itself a symlink. A cycle back through that path renders the page once more before the exact check catches it. This is noted on Source.
  • Linked downloads (rw-site included_files) now collect only the includes the render kept (RenderResult::includes). Before, a file the walker read and then refused, as a cycle or past the byte cap, still had its links resolved from the refused path. With docs/sub/link.md -> ../a.md, a.md's [r](report.pdf) allowlisted docs/sub/report.pdf, which no rendered page links.
  • Hard links stay distinct (out of scope in the spec; the 64-include cap bounds them).
  • docs/includes.md: a symlink back to the file counts as a cycle, and the warnings list shows the new form. No CHANGELOG entry: includes are unreleased, and the Added entry doesn't describe cycles.

Validation

  • rw-renderer: a_file_that_includes_itself_through_a_symlink_is_caught_on_its_first_repeat. An in-memory reader maps link paths to their targets (runs on every OS) and covers a page aliasing itself, a file aliasing itself directly and through another file, and an exact repeat reached through a link. It also asserts that each file is read once. With the canonical check disabled, this test fails.
  • rw-storage-fs (unix): read_include through docs/alias.md -> index.md and a symlinked folder docs/shared -> ../partials reports docs/index.md and partials/note.md. Non-symlinked reads report their own path.
  • rw-site (unix): a_page_that_includes_itself_through_a_symlink_renders_once. With a real docs/link.md -> a.md, the page renders Once. once and warns as a cycle. With the canonical check disabled, it fails: the body renders twice and the exact-repeat warning comes one level deeper.
  • rw-site (unix): an_include_refused_as_a_cycle_links_nothing. docs/report.pdf is served and docs/sub/report.pdf is not. Without the RenderResult::includes filter, this test fails.
  • make test, make lint and make format pass. No e2e tests were run.

🤖 Generated with Claude Code

@codspeed

codspeed Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 2 untouched benchmarks
⏩ 11 skipped benchmarks1


Comparing fix/include-cycle-identity (28a4eda) with main (7b7c250)2

Open in CodSpeed

Footnotes

  1. 11 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (e832538) during the generation of this report, so 7b7c250 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

IncludeReader::read and Storage::read_include return the file's canonical project path with its text, and the walker finds cycles by it, so `a.md` including `link.md -> a.md` warns on the first repeat instead of rendering a.md once more. Warnings and relative paths keep the written spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yumike
yumike force-pushed the fix/include-cycle-identity branch from 7371a97 to 28a4eda Compare October 29, 2026 03:55
@yumike
yumike merged commit bf66625 into main Oct 29, 2026
30 checks passed
@yumike
yumike deleted the fix/include-cycle-identity branch October 29, 2026 04:20
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.

1 participant