Repository navigation
Share one path normalizer, home resolver and repository-root walk - #1038
Open
Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
Open
Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
Conversation
Twelve hand-rolled `.`/`..` folding loops (fs::normalize_lexically, the pnpm crawler's private copy, cargo_workspace::normalize_rel, gradle::resolve_rel, sbt normalize_dir, pypi normalize_rel_path, cargo_manifest::normalize_socket_path, cargo_config::path_is_socket_owned, composer normalize_config_vendor_dir, the yarnrc modules-folder resolver, vendor/jvm/gradle resolve_dir and maven_reactor normalize) each had its own rules. They now share one segment stack in utils::relpath: resolve_rel (fail closed past a floor), normalize_rel_keeping_escapes (keep the escape so a containment check can name it), normalize_lexically and normalize_lexically_keeping_escapes for Paths. Each format keeps only its own admission check (what spellings it refuses up front). Behavior fix: the pnpm crawler's copy let a second leading `..` pop the first (PathBuf::pop removes a `..` segment), so `../../x` collapsed to `x`; the shared keep-escapes normalizer keeps every leading parent. Audit: architecture audit §3.B (path normalizers). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
utils::fs::home_dir fell back to the literal relative path `~` when HOME and USERPROFILE were unset, so every `home_dir().join(...)` probe (cargo registry and config chain, nuget, deno, ruby, python user site-packages, pipx/uv/pdm/conda/pyenv roots) resolved against the process cwd: `./~/.cargo/config.toml` inside the scanned project was read as the user's cargo config. It now returns Option<PathBuf> holding only an absolute HOME/USERPROFILE, and every caller probes nothing without one. The coursier/ivy `process_home` wrapper (the one caller that already filtered for absolute), the go crawler's and the ruby crawler's private HOME/USERPROFILE readers and update::channel's copy now route through the same resolver. Audit: B66 (fs.rs home_dir relative fallback), C19 (home resolvers). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
get's pnpm PnP hosted narrowing read pnpm-lock.yaml with a plain read_to_string, so a FIFO at that path wedged `get` in open(2); the hosted flow it claims to match already reads it through read_regular_to_string_sync. The cargo workspace manifest reader and the maven checksum-sidecar reader checked `lstat` and then opened with a blocking read, a hand-rolled copy of the regular-file guard with a check-then-open race; they keep the lstat (symlinks are still not followed) but now read through the non-blocking regular-file helpers. Audit: B74. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three ancestor walks decided "the repository this directory belongs to" with different rules. policy::find_repo_root_with_warnings honored `.git` files, the home-directory stop, GIT_CEILING_DIRECTORIES and owner trust; the VEX product detector only looked for `<dir>/.git/config` and walked to `/`; the JVM not_build_root check stopped at any `.git` but crossed the home directory and the ceilings. utils::repo_root now owns the walk (search_dirs / ancestor_search_dirs / find_git_repo) and the owner-trust rule, and resolves a checkout's git config through a `gitdir:` file and a worktree's `commondir`. All three callers route through it; the copies in policy and vex/product.rs are deleted. Behavior fixes for VEX product detection (B22): - a submodule names its own origin instead of the superproject's; - a linked worktree reads the shared repository's remotes instead of falling back to a manifest (or nothing); - a dotfiles repository at $HOME is no longer every project's product, and GIT_CEILING_DIRECTORIES and an untrusted owner are honored; - the config is read with the FIFO-safe reader. The JVM build-root search keeps walking past a checkout at the project itself (a submodule can be a module of the build above it) but now stops at the home directory and the ceilings like every other lookup. Audit: B22, §3.B (repo-root walks). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
m2_repo_path_with fell back to the literal `~` (and accepted a relative HOME), so with no usable home the Maven local repository resolved to the CWD-relative ./~/.m2/repository inside the scanned project (B66). It now returns None and JvmEnv::m2_repo is optional; every caller skips the Maven repo when there is none. The Python crawler's injected-env HOME readers (pdm/poetry config, cache and installer dirs, expand_home, pipenv's home) took an empty or relative HOME as-is. They now share utils::fs::home_from_env, the same rooted-and-non-empty rule as home_dir. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
VEX product detection now adds a warning when the nearest checkout is owned by another user, or when the walk stopped below a repository at the home directory, instead of silently falling back to the manifest. The repository walk's home stop reads HOME on Unix and USERPROFILE on Windows again (the old policy rule, which is git's), rather than the crawler home resolver's HOME-then-USERPROFILE order, so an MSYS/Cygwin HOME does not move the socket.yml stop. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
not_build_root walks canonical parents, so its refusal printed /private/var/... on macOS and \\?\C:\... on Windows. It now shows the caller's own lexical ancestor when that names the same directory, else the canonical path without the verbatim prefix. A test covers a relative project root, which the old lexical ancestors() walk could not climb. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Also drop the doc comment orphaned by the normalizer test removal in pypi_requirements. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
marked this pull request as ready for review
October 7, 2026 16:22
Tanmay Singla (Tanmay182003)
approved these changes
Oct 7, 2026
Collaborator
Author
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e13f87a. Configure here.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The architecture audit (§3.B, "Identity, equality and liveness checks") found the same path logic written many times, with rules that drift apart:
./..folding loops, each with its own rules (fail on escape, keep the escape, or ignore..; one separator or both; a floor or none). One of them had a real bug: the pnpm crawler's copy let a second leading..pop the first, becausePathBuf::popalso removes a..segment. So../../xcollapsed tox.policy::find_repo_root_with_warnings(socket.yml) honored.gitfiles, the home-directory stop,GIT_CEILING_DIRECTORIESand owner trust.vex/product.rsfind_git_config) only looked for<dir>/.git/configand walked all the way to/. Inside a submodule it named the superproject as the product; in a linked worktree it found no remote at all; a dotfiles repository at$HOMEbecame the product of every project under it.not_build_rootcheck stopped at any.git, but crossed the home directory and the ceilings.utils::fs::home_dirreturned the literal~, and every caller (~/.cargoregistry and config chain, nuget, deno, ruby, python user site-packages, pipx/uv/pdm/conda/pyenv roots) joined onto it, so the probes resolved against the working directory. The Maven default (m2_repo_path_with) had its own copy of the same fallback, so with no home the Maven local repository was./~/.m2/repositoryinside the scanned project. The Python crawler's injected-env readers took an empty or relativeHOMEas-is.getreadpnpm-lock.yamlwith a plainread_to_string.cargo_workspaceand the maven checksum-sidecar reader did an lstat, then a blocking read: a hand-rolled copy of the regular-file guard with a check-then-open race.No GitHub issue tracks these. They are audit IDs B22, B66 and B74, plus the §3.B duplication rows.
Change
utils::relpath. One segment stack (Segments) behind every lexical normalizer:resolve_rel(base, rel, floor)fails closed;normalize_rel_keeping_escapeskeeps../so a containment check can name the escape;normalize_lexically/normalize_lexically_keeping_escapesdo the same forPaths;is_anchoredis the common absolute/drive check. Each format keeps only its own admission rule (gradle~/:, sbt any.., cargo socket-owned prefix, and so on).utils::fs::home_dir() -> Option<PathBuf>returnsHOME, thenUSERPROFILE, when set and rooted, elseNone; callers probe nothing without a home.home_from_envapplies the same rule to an injected environment.process_home, the go crawler, the ruby crawler'sambient_home,update::channel, the Maven~/.m2default (JvmEnv::m2_repois nowOption; every caller skips the Maven repo when there is none), and the Python crawler's pdm/poetry/expand_homeseams.pipenv_home_dirkeeps Python's own Windows precedence but now also rejects a relative value.utils::repo_root. Holdssearch_dirs,ancestor_search_dirs,find_git_repoandGitRepo::config_path(follows agitdir:file and a worktree'scommondir), plus the owner-trust rule.DetectResult::warningsinstead of silently falling back to the manifest.not_build_rootusesancestor_search_dirs, which keeps walking past a checkout at the project itself (a submodule can still be a module of the build above it) but now stops at home and at the ceilings.--producthelp text now describes the nearest-checkout rule.getreadspnpm-lock.yamlwithread_regular_to_string_sync.cargo_workspace::read_manifestandsidecars::maven::read_sidecarkeep their lstat check (symlinks still not followed), then read through the non-blocking regular-file helpers.Behavior changes to note
HOMEon Unix andUSERPROFILEon Windows (git's rule, as the old policy code did), not the crawler resolver'sHOME-then-USERPROFILEorder. It now ignores a relative value.home_dirkeeps main's order (HOME, thenUSERPROFILE, on every platform). New: a relative or literal~value is skipped instead of used.project_rootcanonicalized, as git does), so a relative root like.now finds the reactor/Gradle/sbt build above it (the old lexicalancestors()saw none). For a symlinked project directory, the parents are those of the link target, not of the link. The refusal names the ancestor in the caller's own spelling when that spelling reaches it; otherwise it shows the canonical path without Windows'\\?\prefix.~/settings.gradle,~/pom.xmlor~/build.sbtwhen the project sits directly under$HOME(git's home rule).Duplicate copies deleted
./..folding loopsutils::relpath::Segments)utils::repo_root)utils::repo_root(policy copy deleted)ambient_home, coursierprocess_home, update::channel, mavenm2_repo_path_with, python pdm/poetry config, poetry cache/installer,expand_home)utils::fs::home_from_env;home_diris it over the process env)Home readers that remain, on purpose, because they follow another tool's own rule: the repository walk's home stop (git),
pipenv_home_dir(Python'sexpanduser), Gradle's passwd-vs-$HOMEcheck (gradle_cache::home_mismatch_with), npm's.npmrcenv (patch/redirect/npmrc.rs), the socket-cli config location (utils/socket_cli_config.rs) andupdate::state's cache-dir chain. The npm crawler's macOS global-prefix fallback (npm_crawler.rsHOMEread) is in pm:npm scope (#1008) and is left there; it already skips an emptyHOMEbut would accept a relative one.Testing
All commands ran in the worktree through
/private/tmp/claude-501/heavy-job.shwithCARGO_INCREMENTAL=0,-j4, on macOS:cargo build -p socket-patch-core -p socket-patch-cli --tests: clean.cargo clippy -p socket-patch-core -p socket-patch-cli --all-targets: no lint on any line this PR changes (checked by intersecting every clippy location with the diff against the merge base). The macOS toolchain reports lints that are already on main (for exampleunix_defaultunused inpdm_dir_candidates). Linux clippy-D warningsis left to CI.cargo test -p socket-patch-core --lib: 5595 passed, 1 failed. The failure isutils::digest::tests::production_digests_go_through_the_helpers, which already fails on main and is fixed by Fix main CI red on stale digest pending-list entries #1016.cargo test -p socket-patch-core --test crawler_maven_e2e --test crawler_gradle_e2e --test crawler_python_e2e --test crawler_cargo_e2e --test telemetry_helpers_e2e: all passed. The first round also rancrawler_{nuget,ruby,go,composer}_e2eandcovgap_crawlers_composer_crawler: all passed.cargo test -p socket-patch-cli --lib: 862 passed.cargo test -p socket-patch-cli --test e2e_socket_yml_policy --test covgap_commands_vex --test e2e_vex --test e2e_embedded_vex --test vex_terminal_output --test vendor_jvm_cli --test maven_sidecar_cli --test covgap_commands_get --test in_process_get_modes --test gradle_agent_cli --test contract_gradle_codes --test e2e_scan --test in_process_scan: all passed (11 ignored in e2e_scan). The first round also rancli_parse_scan,cli_parse_vex,policy_pypi_names,scan_pnpm_relocated_store_cwd_e2e,in_process_cargo_applyandscan_requirements_lock_only: all passed.detect_in_submodule_names_the_submoduleanddetect_in_linked_worktree_uses_the_common_configwere run against the oldproduct.rs. Both failed: the submodule gotpkg:github/acme/superprojectand the worktree gotNone.detect_below_a_home_repository_warns_and_uses_the_manifest: a dotfiles repo atHOMEabove the project gives the manifest PURL plus a warning; run fromHOMEitself, the repo is the product.home_dir_never_returns_a_relative_path(old code returned"~"),m2_repo_path_with_no_usable_home_names_no_repository(old code returned~/.m2/repository) andinjected_home_must_be_rooted(python seams; old code returnedLibrary/…/.config/pypoetryunder the CWD forHOME="").not_build_root_walks_a_relative_root_and_names_the_callers_spelling: absolute and relative project roots under a parent Gradle build give the refusal with the caller's spelling of the root.keeping_escapes_keeps_every_leading_parentcovers the old../../x→xcollapse.filter_to_installed_purls_pnpm_pnp_hosted_fifo_lock_does_not_wedge(watchdog thread that fails the test instead of hanging),read_manifest_rejects_a_fifo_without_blocking,read_sidecar_rejects_a_fifo_without_blocking. These are guards, not failing-first: a FIFO present from the start is already rejected by the lock inventory or the lstat on main. The B74 change closes the window where a FIFO is swapped in after those checks, which no test races.utils::relpathandutils::repo_rootunit tests: fold, floor, keep-escapes, anchored spellings; nearest.gitdir or file, home stop (including the reported skipped home repo), ceilings, the ancestor walk past a checkout at the start, submodule/worktree config resolution, and the owner rule.The intermediate commits were not each built on their own; the final tree was. Please squash-merge.
Deferred
repo_root(owner_trusted).HOMEfallback accepts a relativeHOME(pm:npm scope, Fix open npm issues #1008).policy::repo_relative_checked(directory → repo-relative string) andpatch::package::canonical_member_key(lowercasing tarball keys) are different operations, not normalizer copies, and are unchanged.npm_crawler.rsnormalizer hunk sits near pnpm store code; Fix 15 open pnpm issues across hosted, vendored and agent modes #1007 does not touchnpm_crawler.rstoday, and the branch merges cleanly with main.--jsontop-levelerror(scan and get emit both a string and a {code, message} object) #704, Decide: warn on and then remove scan --apply/--vendor, and whether --vex stays embedded #966, Decide: give SOCKET_FORCE per-command names so forcing a self-update doesn't also force apply and vendor #615, Decide: where patch API calls go when a token is set but the org slug can't be resolved #648, C34, E44-E47) are touched.🤖 Generated with Claude Code
Note
Medium Risk
Touches home/Maven local repo resolution and many crawler path probes across ecosystems; behavior changes are intentional (no usable home, relative roots, git product detection) but could skip caches or shift warnings in edge CI/container environments.
Overview
Centralizes duplicated path, home, and git-checkout logic so crawlers, policy, VEX, and JVM vendor checks behave consistently and fail closed on unsafe paths.
utils::relpathreplaces a dozen hand-rolled./..normalizers (composer, npm/pnpm, gradle, sbt, cargo, PyPI includes, etc.) with one stack-based implementation, including a fix where pnpm’s old normalizer collapsed../../xtox.utils::fs::home_dirnow returnsOption<PathBuf>and only accepts rootedHOME/USERPROFILE, so callers no longer probe CWD-relative~or./~/.m2(B66);JvmEnv::m2_repois optional and wired through Maven/Gradle apply and scan paths.utils::repo_rootis the single git ancestor walk (ceilings, home stop, owner trust,gitdir:/worktree config). Policy socket.yml lookup, VEX--productgit detection, and JVMnot_build_rootall use it—submodules and worktrees name themselves, dotfiles at$HOMEdon’t claim every project (with warnings), and build-root search walks physical parents so relative.still finds the reactor above.FIFO-safe reads (B74): hosted
getreadspnpm-lock.yamlviaread_regular_to_string_sync; Cargo manifest and Maven sidecar reads use the same regular-file helpers afterlstat, with new unix tests so a FIFO can’t wedge the suite.Reviewed by Cursor Bugbot for commit e13f87a. Configure here.
Generated by Claude Code