Skip to content

Normalize build-time paths embedded into fs.c and main.c - #66

Merged
ahogappa merged 5 commits into
masterfrom
claude/kompo-gem-path-generation-3okpzr
Aug 1, 2026
Merged

Normalize build-time paths embedded into fs.c and main.c#66
ahogappa merged 5 commits into
masterfrom
claude/kompo-gem-path-generation-3okpzr

Conversation

@ahogappa

@ahogappa ahogappa commented Jul 31, 2026

Copy link
Copy Markdown
Owner

kompo-vfs compares embedded paths as raw bytes, so any path that reaches
fs.c or main.c unnormalized silently breaks lookups rather than failing
loudly. Two values were escaping the File.expand_path that PATHS entries
already go through.

  • WD[]: WorkDir#run assigned the cached work_dir from metadata.json raw,
    even though valid_tmpdir_path? validated the expanded form. kompo-vfs
    uses WD as a byte prefix of every path, so a trailing slash there makes
    every lookup miss and disables the VFS entirely. Normalize on read, and
    again in MakeFsC where WD is emitted (that prefix also drives
    .kompoignore matching).

  • main.c entrypoint: CopyProjectFiles built entrypoint_path with a raw
    File.join of the CLI value, so -e lib/../main.rb embedded a ".."
    component. kompo-vfs derives WORKING_DIR from Path::parent() of that
    string and parent() does not collapse "..". Normalize at the source so
    it stays byte-identical to the matching PATHS entry; MakeMainC keeps
    sanitizing rather than normalizing, so junk input is still escaped
    instead of raising.

Also deduplicate on the embedded path instead of the source path in
MakeFsC#add_file. The ruby-install-dir replacement can map two source
paths onto one embedded path, and checking beforehand would emit that
path twice in PATHS, orphaning the earlier bytes in FILES.

Document that PATHS emission order (per-directory, DFS pre-order with
sorted siblings) is a contract kompo-vfs relies on for locality, not an
implementation detail to be sorted globally.

The fs.c format is unchanged: PATHS separators and trailing NUL, FILES
concatenation, FILES_SIZES as N+1 cumulative offsets, emission order, and
the COMPRESSED_SIZES dummy are all byte-identical before and after.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_012bpbZACT4YWiN8ucX6ZvyV

Summary by CodeRabbit

  • Bug Fixes

    • Improved normalization of entrypoints and working directories.
    • Prevented duplicate embedded files when source paths resolve to the same destination.
    • Rejected non-canonical or symlinked working-directory paths.
    • Ensured cached directories with trailing slashes are handled consistently.
    • Improved validation of paths that could escape the temporary directory.
  • Tests

    • Added coverage for path normalization, directory validation, symlink handling, and embedded-file deduplication.

claude added 3 commits July 31, 2026 23:10
kompo-vfs compares embedded paths as raw bytes, so any path that reaches
fs.c or main.c unnormalized silently breaks lookups rather than failing
loudly. Two values were escaping the File.expand_path that PATHS entries
already go through.

- WD[]: WorkDir#run assigned the cached work_dir from metadata.json raw,
  even though valid_tmpdir_path? validated the expanded form. kompo-vfs
  uses WD as a byte prefix of every path, so a trailing slash there makes
  every lookup miss and disables the VFS entirely. Normalize on read, and
  again in MakeFsC where WD is emitted (that prefix also drives
  .kompoignore matching).

- main.c entrypoint: CopyProjectFiles built entrypoint_path with a raw
  File.join of the CLI value, so `-e lib/../main.rb` embedded a ".."
  component. kompo-vfs derives WORKING_DIR from Path::parent() of that
  string and parent() does not collapse "..". Normalize at the source so
  it stays byte-identical to the matching PATHS entry; MakeMainC keeps
  sanitizing rather than normalizing, so junk input is still escaped
  instead of raising.

Also deduplicate on the embedded path instead of the source path in
MakeFsC#add_file. The ruby-install-dir replacement can map two source
paths onto one embedded path, and checking beforehand would emit that
path twice in PATHS, orphaning the earlier bytes in FILES.

Document that PATHS emission order (per-directory, DFS pre-order with
sorted siblings) is a contract kompo-vfs relies on for locality, not an
implementation detail to be sorted globally.

The fs.c format is unchanged: PATHS separators and trailing NUL, FILES
concatenation, FILES_SIZES as N+1 cumulative offsets, emission order, and
the COMPRESSED_SIZES dummy are all byte-identical before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bpbZACT4YWiN8ucX6ZvyV
Follow-up cleanup on the path-normalization change. Same behavior, less
duplication, and the invariant is enforced instead of described.

- WorkDir gains a private canonical_path that both branches of #run funnel
  through, so the cached and freshly-created paths are normalized to the
  same strength. Previously the mktmpdir branch resolved symlinks while
  the cache branch only expanded lexically. Normalizing now happens after
  valid_tmpdir_path?, which removes the start_with?("/") guard that only
  existed to preserve that check's relative-path rejection, and the double
  File.expand_path it caused.

- MakeFsC no longer re-expands WorkDir.path. It was the only one of 20+
  WorkDir.path call sites to do so, and it is redundant now that the
  source guarantees the shape. build_template_context asserts the
  invariant instead: an unnormalized work_dir would otherwise ship a
  binary whose VFS silently matches nothing, so failing the build is
  strictly better than quietly repairing it.

- Test coverage moves to where the behavior lives: work_dir_test.rb now
  covers an unnormalized metadata.json (the real reported input), and the
  MakeFsC tests cover what MakeFsC owns - that WD[] is emitted as a prefix
  of every PATHS entry, and that a bad work_dir is rejected.

- decode_embedded_paths, decode_wd, and the open-coded COMPRESSED_FILES
  decode share one decode_byte_array helper, so the fs.c array literal
  format is described in one place.

- Trim comments that were stated two or three times over, and restore the
  "Prune certain directories" label that a comment rewrite had dropped.

fs.c output is byte-identical to before the original change: declarations,
PATHS separators and trailing NUL, emission order, FILES_SIZES as N+1
cumulative offsets, and the COMPRESSED_SIZES dummy all verified unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bpbZACT4YWiN8ucX6ZvyV
Comments should carry what the code cannot: the kompo-vfs contract, the
reason an ordering matters, why something is deliberately not done.

Removed the mechanical narration - "entries land in add_file call order",
"directories are never emitted as PATHS entries", "dedup after the rewrite,
not before", "the single place work_dir is canonicalized" - and the doc
comments on decode_byte_array/decode_embedded_paths/decode_wd, whose names
already say it. Restored the original "Prune certain directories" label a
previous rewrite had replaced.

What is left states only non-obvious facts: that kompo-vfs assigns node IDs
in PATHS order, that Path::parent() does not collapse "..", that
File.expand_path raises on the junk c_string_escape exists to absorb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bpbZACT4YWiN8ucX6ZvyV
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ahogappa, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 726be06e-e16f-43bf-a94b-e1caa9b00fff

📥 Commits

Reviewing files that changed from the base of the PR and between f71e421 and 916227d.

📒 Files selected for processing (1)
  • test/tasks/work_dir_test.rb

Walkthrough

The change normalizes entrypoint and work-directory paths, validates canonical paths during C template generation, and deduplicates embedded paths after installation-directory mapping. Tests cover these behaviors and update byte-array decoding helpers.

Changes

Path handling and generated filesystem paths

Layer / File(s) Summary
Work-directory canonicalization
lib/kompo/tasks/work_dir.rb, test/tasks/work_dir_test.rb
Cached and new work directories use canonical paths. Tests cover trailing slashes and symlink escapes.
Entrypoint path normalization
lib/kompo/tasks/copy_project_files.rb, lib/kompo/tasks/make_main_c.rb, test/tasks/copy_files_test.rb
Entrypoint destinations resolve parent-directory segments before copying. Comments document the normalized path contract. Tests verify the copied destination.
Embedded path validation and deduplication
lib/kompo/tasks/make_fs_c.rb, test/tasks/make_c_test.rb
Template generation rejects non-canonical work directories. Embedded paths are deduplicated after mapping. Tests cover WD prefixes, collisions, decoding helpers, and configurable installation paths.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

A rabbit checks each path with care,
Canonical work dirs wait there.
Entrypoints lose their ..,
Duplicate paths leave no trace.
C files keep their order fair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: normalizing build-time paths embedded in fs.c and main.c.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/kompo-gem-path-generation-3okpzr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/kompo/tasks/make_fs_c.rb`:
- Around line 184-199: Update the embedded_path rewrite condition around
`@current_ruby_install_dir` so replacement occurs only when path equals the
install directory or begins with it followed by File::SEPARATOR; preserve
unchanged paths for sibling directory names. Add a regression case covering a
sibling install-directory path and verify it is not rewritten.

In `@lib/kompo/tasks/work_dir.rb`:
- Around line 107-109: Update canonical_path in lib/kompo/tasks/work_dir.rb at
lines 107-109 to resolve the deepest existing ancestor with File.realpath,
append any missing components, and revalidate the resolved result against
File.realpath(Dir.tmpdir) before accepting or creating it; update
lib/kompo/tasks/make_fs_c.rb at lines 221-223 to compare the existing work
directory using File.realpath(`@work_dir`) instead of File.expand_path(`@work_dir`);
add regression coverage for cached and mocked MakeFsC work directories located
beneath a temp-directory symlink.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a986606d-637d-49e7-af4b-de9769139835

📥 Commits

Reviewing files that changed from the base of the PR and between 11d3c74 and 65b5446.

📒 Files selected for processing (7)
  • lib/kompo/tasks/copy_project_files.rb
  • lib/kompo/tasks/make_fs_c.rb
  • lib/kompo/tasks/make_main_c.rb
  • lib/kompo/tasks/work_dir.rb
  • test/tasks/copy_files_test.rb
  • test/tasks/make_c_test.rb
  • test/tasks/work_dir_test.rb

Comment thread lib/kompo/tasks/make_fs_c.rb
Comment thread lib/kompo/tasks/work_dir.rb
A cached work_dir of "/tmp/link/work" satisfied valid_tmpdir_path? no matter
where link pointed, because the check only ever saw the lexical path. When the
leaf did not exist yet, canonical_path had no path to resolve and returned the
alias unchanged, so FileUtils.mkdir_p created the work directory - and its
marker file - outside the system temp directory. Confirmed by running the new
test against the previous commit: it wrote .kompo_work_dir_marker into the
repository root.

- Canonicalize before validating, so the tmpdir check sees where the path
  actually lands. Absolute-only, so a relative path is still rejected rather
  than resolved against the cwd.
- canonical_path now resolves the deepest existing ancestor when the path
  itself does not exist, which is the case the symlink slipped through.
- MakeFsC asserts against File.realpath rather than File.expand_path, so an
  aliased work_dir is caught too - expand_path cannot see a symlink.

The unbounded start_with? in add_file's install-dir rewrite was raised in
review as a sibling-directory hazard. It is real as a pattern but unreachable:
InstallRuby sets original_ruby_install_dir to ruby_install_dir on every code
path, so the branch never runs. Left alone rather than hardening dead code.

fs.c output remains byte-identical: declarations, PATHS separators and trailing
NUL, emission order, FILES_SIZES as N+1 cumulative offsets, COMPRESSED_SIZES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bpbZACT4YWiN8ucX6ZvyV

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/tasks/work_dir_test.rb`:
- Around line 56-75: Update
test_work_dir_rejects_cached_work_dir_behind_an_escaping_symlink to use a unique
test-owned symlink target guaranteed to be outside Dir.tmpdir, and ensure that
target is removed during cleanup even if assertions fail. Replace the raw
path.start_with? assertion with a separator-aware containment check based on the
resolved Dir.tmpdir path, preserving the rejection and no-creation assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b9f3b6e3-e1dc-43d9-8d6e-8b51baae2b73

📥 Commits

Reviewing files that changed from the base of the PR and between 65b5446 and f71e421.

📒 Files selected for processing (4)
  • lib/kompo/tasks/make_fs_c.rb
  • lib/kompo/tasks/work_dir.rb
  • test/tasks/make_c_test.rb
  • test/tasks/work_dir_test.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/kompo/tasks/make_fs_c.rb
  • lib/kompo/tasks/work_dir.rb
  • test/tasks/make_c_test.rb

Comment thread test/tasks/work_dir_test.rb
The escape target was the repository checkout, reached via File.expand_path
("../..", __dir__). That is only outside Dir.tmpdir by coincidence - a checkout
below /tmp would make the cached path legitimately valid and fail the test for
the wrong reason. It also meant a regression wrote into the working tree: running
this test against the pre-fix commit created kompo_escape_probe/ with a marker
file in the repository root.

Point TMPDIR at a directory inside the per-test tmpdir instead, so both ends of
the escape live under with_tmpdir. The target stays writable, so a regression
still really creates the directory and is still caught, but the fallout is
cleaned up with the rest of the fixture even when an assertion fails.

Also make the containment assertion separator-aware, so a sibling like
/tmp_evil cannot satisfy it.

Verified both directions: passes on HEAD, fails on HEAD~1 at the same
"directory was created outside the temp dir" assertion, and leaves the working
tree clean either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bpbZACT4YWiN8ucX6ZvyV
@ahogappa
ahogappa merged commit 3871afe into master Aug 1, 2026
5 checks passed
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.

2 participants