From 87bde4cd07a73601e913024268f8dfceae885de9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 22:24:56 +0000 Subject: [PATCH 1/6] Start fix for #391, #392, #509 Assisted-by: Claude Code:claude-opus-5-5 From 889c0f437afb2d6a7ea10482706d352bb957daf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 22:35:49 +0000 Subject: [PATCH 2/6] Gate Go replaces on the build-selected version A Go replace names an exact module version. When the build list selects another version (a later go get, or a go 1.16 graph where a transitive dependency asks for a newer one), the replace is ignored and the build links the unpatched module. Agent apply wrote such a replace and reported it applied, apply --check passed it in go 1.16 graphs, vex attested not_affected, and the hosted gate took a go.sum /go.mod-only line as proof the version was built. All four now ask one helper that reads go.mod and go.sum: require for go 1.17+, and for older graphs the highest of require and the go.sum zip-line versions. apply refuses, apply --check flags, vex omits with go_version_not_selected, and hosted warns and skips. Fixes #391, #392, #509. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/CLI_CONTRACT.md | 8 +- crates/socket-patch-cli/src/commands/vex.rs | 50 ++++ .../tests/covgap_commands_vex.rs | 80 ++++++ .../src/patch/redirect/golang_local.rs | 205 +++++++++++++--- .../src/patch/redirect/mod.rs | 151 ++++++++---- .../src/vendor/go_build_list.rs | 228 ++++++++++++++++++ .../src/vendor/go_consumer_sync.rs | 7 + .../src/vendor/go_mod_edit.rs | 20 ++ .../src/vendor/go_sum_edit.rs | 12 + crates/socket-patch-core/src/vendor/mod.rs | 1 + docs/ecosystems.md | 10 + 11 files changed, 694 insertions(+), 78 deletions(-) create mode 100644 crates/socket-patch-core/src/vendor/go_build_list.rs diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 17dd3fe45..5997e47ca 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -214,7 +214,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc **Vendored entries and the rest of the CLI.** Because nothing is in the manifest, vendored patches are invisible to `apply` (nothing to apply in place) but fully visible to `list` (listed from the ledger, labeled `Mode: vendored (recorded in .socket/vendor/state.json)` in human mode, exit 0 on a vendored-only project), `vex` (attested from the embedded records while a lockfile still wires the artifact — see "Manifest-less VEX"), `repair` (health-checked and rebuilt from the ledger), and `scan --prune` (lockfile-driven reconcile). They are exempt from standalone `vendor`'s manifest reconcile (`reconcile_dropped` never touches `detached` entries) and exit via `remove ` (which reverts them), `vendor --revert`, or `rollback`, whose vendored leg reverts every in-scope ledger entry (unscoped and identifier-scoped runs; path-scoped runs reach them only when an installed copy matches). -`scan --mode hosted` swaps the in-place apply for the registry-redirect pipeline: discover → resolve hosted-patch references (grant token + integrity + per-dep registry override) → rewrite ONLY the patched dependencies' lockfile / registry-config entries to point at the hosted packages. A dep counts as **redirected** only when its hosted-artifact URL (or per-dep registry index URL) actually landed in a project file — a granted reference whose rewriter found nothing to edit is neither counted nor attested. **No ledger (v5.0)**: hosted mode writes ONLY the lockfile / registry-config edits — `.socket/vendor/redirect-state.json` is never written (on success or failure), and a pre-v5 one on disk is ignored (never read for planning, never quarantined, left byte-identical). The lockfiles are the only record of a hosted patch: `list`, `vex`, `rollback`, `remove`, `vendor` and `repair` all discover the hosted pins from them (a hosted URL counts only on `https://patch.socket.dev` or the `--patch-server-url` / `SOCKET_PATCH_SERVER_URL` origin), and commit-ready output is just the lockfile / config changes. Cargo and golang are confirmed only by their rewriter's own report (`confirmed_cargo_uuids` / `confirmed_golang_uuids`): a golang dep counts only when its go.mod `replace M V => patch.socket.dev/gopatch/ ` and both go.sum lines are in place, never because the patch-server origin or leftover go.sum lines appear somewhere. Gradle is confirmed the same way (`confirmed_gradle_uuids`): only when the final files hold the owned script, the index row, the live apply line in every build's settings file and the suffixed version in every lock entry of the GA (see [Gradle builds](#gradle-builds-v50)). A golang module that go.mod does not require and go.sum does not list at the patched version is outside the build graph and is refused with `redirect_golang_not_in_module_graph` (nothing written). Only the exact module `patch.socket.dev/gopatch/` is socket-owned; any other module path is refused with `redirect_golang_untrusted_module_path`. A vendored golang module is taken over like cargo and the npm family: its vendor wiring, committed copy and ledger entry are reverted first (`redirect_takeover_reverted_vendored`). A vendored PyPI package (requirements.txt, Poetry, Pipenv, uv, Hatch, PDM, pylock) is taken over the same way: its vendored wiring is restored to the recorded registry entry, its ledger entry and wheel are removed, and it is redirected in the same commit. The Python rewriters treat any non-registry source as user-authored, so without the revert they refused socket-patch's own vendored source and left the project vendored. A takeover revert that leaves vendored wiring in place is refused with `redirect_vendored_revert_failed`. That covers a drift-skipped record (`vendor_lock_entry_drifted`) and a reverted file that still references the artifact (`vendor_revert_residual_reference`). The ledger entry and artifact are kept, and the package stays vendored and skipped. `--dry-run` predicts the same refusal from the same signals instead of previewing `redirect_would_revert_vendored`. The hosted requirements.txt rewriter only rewrites an existing pin in the root `requirements.txt`, so a vendored requirements.txt package whose wiring is a pin in a `-r` include or a `(transitive)` line vendored mode appended is kept vendored, wet and `--dry-run` alike, with `redirect_requirements_takeover_unreachable` (a top-level `warnings[]` entry, and the `errorCode` of the package's `skipped` event). Its wiring, ledger entry and wheel are kept, so it stays vendored and patched (exit 0). The uv and Poetry rewriters are covered the same way: a vendored uv package whose recorded pre-vendor `uv.lock` entry is at another version than the patch (vendored uv pins the entry down to the patch's version; the revert brings the lock's own version back, and hosted mode only pins the version the lock resolves) is kept vendored with `redirect_uv_takeover_version_unreachable`, and a vendored Poetry package on a Poetry 0.x lock (which hosted mode refuses outright) is kept vendored with `redirect_poetry_lock_unsupported`. **Staged takeover (v5.0)**: the takeover is atomic. Each purl's vendored revert is staged in memory (the run's group commit, the same journaled commit vendored mode uses), the hosted rewrite plans against the reverted project, and a staged purl the rewrite does not pin is retracted: every staged revert is undone, that purl keeps its vendored wiring, ledger entry and artifact byte-identical, and the rest are staged and rewritten again. A retracted purl is a `skipped` event whose `errorCode` is the cause: its existing skip reason (unavailable wheel metadata, …), the rewriter warning that names the package (`redirect_yarn_berry_missing_checksum`, `redirect_pypi_platform_wheel`, …), `redirect_requirements_takeover_unreachable`, the rewrite's lock-level refusal (`redirect_yarn_berry_mixed_line_endings`, `redirect_bun_lock_unsupported`, a Gradle planner refusal, …) or `redirect_takeover_not_pinned`; that warning is reported in `warnings[]`, followed by `redirect_takeover_kept_vendored` naming the package. Exit 0: the package stays vendored and patched, never unpatched in both modes. The reverts, the hosted pins and the vendored ledger then reach the disk in one commit, and the reverted artifacts are deleted only after it: a refusal or write failure before the commit writes nothing, a failed commit puts back the files it replaced, and a commit interrupted after its journal was written is finished by the next command that takes the apply lock. `--dry-run` runs the same steps and drops the staged state instead of committing, so its `verified` events and warnings are the wet run's `applied` events and warnings. A hosted run with no takeover stages its writes the same way but commits them without the journal (it writes nothing under `.socket/`): a file that fails to be replaced puts back the ones already replaced, so a failed run leaves no lock half-redirected. Re-runs over already-rewritten output plan from the current lock text and are idempotent (exit 0, lock unchanged). **Lock (v5.0)**: the hosted engine acquires `<.socket>/apply.lock` before its first wet write (the staged takeover reverts) — not on `--dry-run`, and not when the run would write nothing (zero redirects, all skipped) — so previews and no-op runs never create `.socket/`; contention is `lock_held` and a lock-file I/O fault (a read-only project root, a file squatting on `.socket/`) is `lock_io` — both exit 1, refused BEFORE any project file is written, and rendered like every other lock holder: human `Error (): ` on stderr (+ the `--lock-timeout` hint for a live holder); JSON prints the envelope with `status: "error"` and `error: {code: "lock_held" | "lock_io", message}` (no `redirect` payload: nothing was rewritten). **Takeover symlink pre-check (v5.0)**: a vendored→hosted takeover whose recorded wiring file is a symlink is refused up front with `redirect_symlinked_file_unsupported` — wet and `--dry-run` alike, before any revert — so "nothing was written" holds. **Human mode (v5.0)**: hosted `scan` prints the results table and update detection like the other modes, then rewrites without a prompt (scan never prompts); `--dry-run` previews through the engine, and a detail fetch that leaves nothing to redirect enters the engine as a no-op (`Redirected 0 packages; rewrote 0 files.`, no lock, no `.socket/`). The detail fetch prints the same progress counter and per-package `Warning: could not fetch details for …` lines as the agent arm. An EMPTY hosted discovery prints `No patches available for installed packages.` and exits 0 without entering the engine; a discovery whose every offer is paid-tier for an org without paid access prints the table's paid nudge, then `No downloadable patches (paid subscription required).`, and exits 0 without entering the engine (parity with the agent/vendored arms). Under `--json` (v5.0, MAJOR) the outcome is recorded into the scan envelope: one event per selected patch, sorted by purl, each with `details.mode: "hosted"` — `applied` for a confirmed pin (`verified` under `--dry-run`), `skipped` with the skip reason as `errorCode` (and its detail as `reason`), or `skipped` / `redirect_unconfirmed` for a patch that was granted but no lockfile entry pinning it could be rewritten (the human output's `Not hosted : …` line) — plus a `redirect: {mode: "hosted", rewrittenFiles}` payload (the files rewritten, or that would be). A `skipped` event does not change `status` or the exit code (the hosted exit policy is an open decision, #704). Rewriter warnings carry stable `redirect_*` codes (e.g. `redirect_npm_no_lockfile`, `redirect_gradle_manual_snippet`, `redirect_golang_unsupported`); new codes are additive (MINOR). v5.0 additive codes: `redirect_composer_no_lockfile` / `redirect_gem_no_gemfile` (composer / gem: neither manifest nor lock present — once per run, after the intake gates), `redirect_gem_bundle_gemfile_unsupported` (gem: `BUNDLE_GEMFILE` — `BUNDLE_GEMFILE:` in the bundler app config, which outranks the environment variable as in `Bundler::Settings`, else the environment variable — names a manifest other than the project's `Gemfile` / `gems.rb`, so no gem is redirected or attested; a value naming one of those two selects that pair even when the other spelling is present), `redirect_gem_bundle_lockfile_unsupported` (gem: bundler 4's custom lockfile — the `BUNDLE_LOCKFILE` environment variable, else `BUNDLE_LOCKFILE:` in the bundler app config, else in the global config — names a lock other than the loaded pair's own `Gemfile.lock` / `gems.locked`, so no gem is redirected or attested rather than pinning a lock bundler ignores), `redirect_gem_twin_manifest_ambiguous` (gem: a `Gemfile` + `gems.rb` twin under default discovery; bundler 1.x loads the `Gemfile` and bundler ≥ 2 loads `gems.rb`, and a lock's `BUNDLED WITH` records which bundler wrote it, not which one installs it, so neither pair is wired or attested — remove the unused spelling or set `BUNDLE_GEMFILE` to the one in use), `redirect_gem_mirror_overrides_source` (gem: Bundler's all-source, exact patch-source or patch-hostname mirror can route the per-dep `source` block to an unpatched upstream gem. Intake reads the app config (`BUNDLE_APP_CONFIG`, where a set-but-empty value selects `/config`, honoring `BUNDLE_IGNORE_CONFIG`) and all `BUNDLE_MIRROR__...` variables visible to the scan; app config overrides the environment per encoded key, then `mirror.all` takes precedence over exact source, which takes precedence over hostname. URI matching follows Bundler's whole-URI case folding, default-port/trailing-slash normalization and single slash key alias, not URL prefixes. An exact-source fallback-timeout key without a mirror URL shadows the hostname mirror and fetches that source directly; a configured URL is conservatively refused even if a timeout could bypass an unreachable mirror at install time. Like `redirect_gem_bundle_gemfile_unsupported`, the gate leaves the Gemfile pair byte-identical and confirms no gem redirect. On an embedded `scan --vex`, rediscovered older hosted gem pins may attest only from verified installed bytes: a missing tree is not excused by the lockfile, and `--vex-no-verify` omits those hosted gems with `mirror_overrides_source` rather than trusting their intercepted source. Agent/vendored evidence, unrelated ecosystems and standalone VEX behavior are unchanged. Details identify the setting form and its app/environment origin without printing mirror values or source URLs, which may contain credentials. Remove the applicable all/source/hostname setting (including any slash alias) from that origin and reuse its existing mirror URL under `mirror.https://rubygems.org` to clear the refusal; an environment setting must be unset in the scan/install environment. User-global Bundler config and mirrors set only in a later install environment are not inspected; keep those mirrors scoped to the upstream source too), `redirect_maven_no_pom` (no `pom.xml` and no Gradle build), `redirect_nuget_lock_unparseable` (a present-but-corrupt `packages.lock.json` — warned once, nothing mutated; an absent lock still proceeds; a leading UTF-8 BOM, which dotnet reads past, is not corrupt and is kept on rewrite), `redirect_nuget_lock_other_version` (the lock also resolves the patched id at another version in some target framework: the exact-id `packageSourceMapping` would route that framework to a feed that serves only the patched version, so the dep is skipped with nothing written; only lock entries at the patched version are ever re-pinned, and `resolved` is never rewritten), `redirect_nuget_mapping_set_aside` (v5.0 #462: another source's `packageSourceMapping` also named the patched id exactly — Visual Studio's mapping UI writes such lists — which ties with the Socket source so NuGet would take the package from whichever feed answers first; that pattern, or its whole `` when it was the only one, is commented out in a `` comment while the patch is wired, and `remove` / `rollback` put it back byte-exact), `redirect_nuget_mapping_conflict` (such a pattern sits in markup that cannot go in a comment; the dep is skipped, nothing written). v5.0 #354: when the rewriter creates the `packageSourceMapping`, its `*` catch-all also names the sources NuGet merges in from the user config and every parent directory's config (honoring ``), since NuGet drops every source no pattern names; a fresh config only seeds `nuget.org` when those inherited configs have it (a parent that cleared it for a mirror keeps that choice); when an inherited config already has a `packageSourceMapping` (NuGet merges those too), no catch-all is written at all — only the Socket pattern — so a source the parent restricts is never widened; the exclusivity set-aside exempts only the source the run wires, so a `socket-patch-*` lookalike key or a stale uuid naming the id is set aside too, `redirect_cargo_lock_pkg_ambiguous` (several same-name+version `[[package]]` blocks and none carries the index `source` — transactional skip), `redirect_cargo_dep_overridden` (the crate does not resolve to crates.io because the user overrides it — its one `Cargo.lock` block carries a git / other-registry `source`, as a `[patch]` git or registry override in the root manifest or any cargo config leaves it, or has no `source` while the root `Cargo.toml` / project `.cargo/config*` holds a `[patch]` path entry for it; with no `Cargo.lock`, the same `[patch.crates-io]` / `[patch."https://github.com/rust-lang/crates.io-index"]` entry for the crate, by key or `package = "…"`, in the root `Cargo.toml` or the project `.cargo/config*` — transactional skip, nothing rewritten: a Socket pin would silently replace the user's fork and the then-unused `[patch]` entry breaks `cargo --locked`). Also additive: `redirect_gem_version_not_locked` (gem: no `GEM` section of the lock lists the crawled `name (version)`, for example a version another project installed into the shared gem home; the gem is skipped with nothing written, so the user's declared constraint and the locked version are never overwritten), `redirect_gem_no_lockfile` (gem: the project has a `Gemfile` / `gems.rb` but no `Gemfile.lock` / `gems.locked`, so nothing records which version it resolves and the crawled version may be another project's copy in the shared gem home; the gem is skipped with nothing written, never pinned over the user's constraint or appended as a new dependency, and the detail asks for `bundle lock` (or `bundle install`) and a re-run). Also v5.0: a registry override of the wrong kind (or none at all) warns the arm's missing-override code for nuget/gem/golang. Refusals stay fail-closed with a diagnosis that names the actual cause: a yarn-berry lock entry resolving through a non-`npm:` protocol keeps `redirect_yarn_berry_unsupported_protocol` with the entry's ACTUAL protocol in the detail — except socket-patch's OWN vendored wiring (a `file:` range into `.socket/vendor/`), which gets the distinct `redirect_yarn_berry_vendored_entry` code whose detail names the retirement path (`remove ` per package, or `vendor --revert` which unwinds every vendored package, then re-run `scan --mode hosted`). Both leave the entry byte-identical; neither changes exit code or status. **yarn berry line endings (v5.0)**: yarn writes a NEW `yarn.lock` with the OS line ending (`os.EOL` — CRLF on Windows) and keeps an existing lock's majority ending on every later write, and a `core.autocrlf` checkout turns an LF lock CRLF on any OS — so a uniformly CRLF lock is rewritten in its own ending: every untouched byte (a leading BOM included) round-trips (and `rollback`'s upstream restore keeps the lock's own ending). A lock that MIXES CRLF and LF (or holds a bare CR) has no single ending to keep — yarn's own `--immutable` check rejects it too (YN0028) — so it is refused untouched with `redirect_yarn_berry_mixed_line_endings` (the detail names `yarn install`, which normalizes it). The root `package.json`, which the rewrite re-renders to add `resolutions`, gets the same gate: a mixed one is refused untouched with the same code — the decision vendored mode takes with `vendor_yarn_berry_mixed_line_endings`, from the same shared berry gate set. This replaces v4's `redirect_yarn_berry_crlf_unsupported`, which refused every CRLF lock and is no longer emitted. A vendored→hosted takeover of a yarn-berry entry is checked against these project gates (mixed `yarn.lock` / `package.json` line endings, unsupported `cacheKey`, a non-zero `.yarnrc.yml` `compressionLevel`) on the project as it is before any revert, because the revert re-renders `package.json` in its majority ending — wet and `--dry-run` alike. A refused purl keeps its vendored wiring, ledger entry and artifact byte-identical, is skipped with the gate's code (reported in `warnings[]`, followed by `redirect_takeover_kept_vendored`) and is never announced as `redirect_takeover_reverted_vendored`. +`scan --mode hosted` swaps the in-place apply for the registry-redirect pipeline: discover → resolve hosted-patch references (grant token + integrity + per-dep registry override) → rewrite ONLY the patched dependencies' lockfile / registry-config entries to point at the hosted packages. A dep counts as **redirected** only when its hosted-artifact URL (or per-dep registry index URL) actually landed in a project file — a granted reference whose rewriter found nothing to edit is neither counted nor attested. **No ledger (v5.0)**: hosted mode writes ONLY the lockfile / registry-config edits — `.socket/vendor/redirect-state.json` is never written (on success or failure), and a pre-v5 one on disk is ignored (never read for planning, never quarantined, left byte-identical). The lockfiles are the only record of a hosted patch: `list`, `vex`, `rollback`, `remove`, `vendor` and `repair` all discover the hosted pins from them (a hosted URL counts only on `https://patch.socket.dev` or the `--patch-server-url` / `SOCKET_PATCH_SERVER_URL` origin), and commit-ready output is just the lockfile / config changes. Cargo and golang are confirmed only by their rewriter's own report (`confirmed_cargo_uuids` / `confirmed_golang_uuids`): a golang dep counts only when its go.mod `replace M V => patch.socket.dev/gopatch/ ` and both go.sum lines are in place, never because the patch-server origin or leftover go.sum lines appear somewhere. Gradle is confirmed the same way (`confirmed_gradle_uuids`): only when the final files hold the owned script, the index row, the live apply line in every build's settings file and the suffixed version in every lock entry of the GA (see [Gradle builds](#gradle-builds-v50)). A golang module that go.mod does not require and go.sum has no zip (`h1:` of the module tree) line for at the patched version is outside the build graph and is refused with `redirect_golang_not_in_module_graph` (nothing written); a `/go.mod`-only line doesn't count, since go writes one for every version MVS reads (#509). In a go < 1.17 graph, where `require` holds only lower bounds, the build selects the highest of the required version and go.sum's zip-line versions, and a patch for a lower version warns `redirect_golang_version_mismatch` (nothing written). Only the exact module `patch.socket.dev/gopatch/` is socket-owned; any other module path is refused with `redirect_golang_untrusted_module_path`. A vendored golang module is taken over like cargo and the npm family: its vendor wiring, committed copy and ledger entry are reverted first (`redirect_takeover_reverted_vendored`). A vendored PyPI package (requirements.txt, Poetry, Pipenv, uv, Hatch, PDM, pylock) is taken over the same way: its vendored wiring is restored to the recorded registry entry, its ledger entry and wheel are removed, and it is redirected in the same commit. The Python rewriters treat any non-registry source as user-authored, so without the revert they refused socket-patch's own vendored source and left the project vendored. A takeover revert that leaves vendored wiring in place is refused with `redirect_vendored_revert_failed`. That covers a drift-skipped record (`vendor_lock_entry_drifted`) and a reverted file that still references the artifact (`vendor_revert_residual_reference`). The ledger entry and artifact are kept, and the package stays vendored and skipped. `--dry-run` predicts the same refusal from the same signals instead of previewing `redirect_would_revert_vendored`. The hosted requirements.txt rewriter only rewrites an existing pin in the root `requirements.txt`, so a vendored requirements.txt package whose wiring is a pin in a `-r` include or a `(transitive)` line vendored mode appended is kept vendored, wet and `--dry-run` alike, with `redirect_requirements_takeover_unreachable` (a top-level `warnings[]` entry, and the `errorCode` of the package's `skipped` event). Its wiring, ledger entry and wheel are kept, so it stays vendored and patched (exit 0). The uv and Poetry rewriters are covered the same way: a vendored uv package whose recorded pre-vendor `uv.lock` entry is at another version than the patch (vendored uv pins the entry down to the patch's version; the revert brings the lock's own version back, and hosted mode only pins the version the lock resolves) is kept vendored with `redirect_uv_takeover_version_unreachable`, and a vendored Poetry package on a Poetry 0.x lock (which hosted mode refuses outright) is kept vendored with `redirect_poetry_lock_unsupported`. **Staged takeover (v5.0)**: the takeover is atomic. Each purl's vendored revert is staged in memory (the run's group commit, the same journaled commit vendored mode uses), the hosted rewrite plans against the reverted project, and a staged purl the rewrite does not pin is retracted: every staged revert is undone, that purl keeps its vendored wiring, ledger entry and artifact byte-identical, and the rest are staged and rewritten again. A retracted purl is a `skipped` event whose `errorCode` is the cause: its existing skip reason (unavailable wheel metadata, …), the rewriter warning that names the package (`redirect_yarn_berry_missing_checksum`, `redirect_pypi_platform_wheel`, …), `redirect_requirements_takeover_unreachable`, the rewrite's lock-level refusal (`redirect_yarn_berry_mixed_line_endings`, `redirect_bun_lock_unsupported`, a Gradle planner refusal, …) or `redirect_takeover_not_pinned`; that warning is reported in `warnings[]`, followed by `redirect_takeover_kept_vendored` naming the package. Exit 0: the package stays vendored and patched, never unpatched in both modes. The reverts, the hosted pins and the vendored ledger then reach the disk in one commit, and the reverted artifacts are deleted only after it: a refusal or write failure before the commit writes nothing, a failed commit puts back the files it replaced, and a commit interrupted after its journal was written is finished by the next command that takes the apply lock. `--dry-run` runs the same steps and drops the staged state instead of committing, so its `verified` events and warnings are the wet run's `applied` events and warnings. A hosted run with no takeover stages its writes the same way but commits them without the journal (it writes nothing under `.socket/`): a file that fails to be replaced puts back the ones already replaced, so a failed run leaves no lock half-redirected. Re-runs over already-rewritten output plan from the current lock text and are idempotent (exit 0, lock unchanged). **Lock (v5.0)**: the hosted engine acquires `<.socket>/apply.lock` before its first wet write (the staged takeover reverts) — not on `--dry-run`, and not when the run would write nothing (zero redirects, all skipped) — so previews and no-op runs never create `.socket/`; contention is `lock_held` and a lock-file I/O fault (a read-only project root, a file squatting on `.socket/`) is `lock_io` — both exit 1, refused BEFORE any project file is written, and rendered like every other lock holder: human `Error (): ` on stderr (+ the `--lock-timeout` hint for a live holder); JSON prints the envelope with `status: "error"` and `error: {code: "lock_held" | "lock_io", message}` (no `redirect` payload: nothing was rewritten). **Takeover symlink pre-check (v5.0)**: a vendored→hosted takeover whose recorded wiring file is a symlink is refused up front with `redirect_symlinked_file_unsupported` — wet and `--dry-run` alike, before any revert — so "nothing was written" holds. **Human mode (v5.0)**: hosted `scan` prints the results table and update detection like the other modes, then rewrites without a prompt (scan never prompts); `--dry-run` previews through the engine, and a detail fetch that leaves nothing to redirect enters the engine as a no-op (`Redirected 0 packages; rewrote 0 files.`, no lock, no `.socket/`). The detail fetch prints the same progress counter and per-package `Warning: could not fetch details for …` lines as the agent arm. An EMPTY hosted discovery prints `No patches available for installed packages.` and exits 0 without entering the engine; a discovery whose every offer is paid-tier for an org without paid access prints the table's paid nudge, then `No downloadable patches (paid subscription required).`, and exits 0 without entering the engine (parity with the agent/vendored arms). Under `--json` (v5.0, MAJOR) the outcome is recorded into the scan envelope: one event per selected patch, sorted by purl, each with `details.mode: "hosted"` — `applied` for a confirmed pin (`verified` under `--dry-run`), `skipped` with the skip reason as `errorCode` (and its detail as `reason`), or `skipped` / `redirect_unconfirmed` for a patch that was granted but no lockfile entry pinning it could be rewritten (the human output's `Not hosted : …` line) — plus a `redirect: {mode: "hosted", rewrittenFiles}` payload (the files rewritten, or that would be). A `skipped` event does not change `status` or the exit code (the hosted exit policy is an open decision, #704). Rewriter warnings carry stable `redirect_*` codes (e.g. `redirect_npm_no_lockfile`, `redirect_gradle_manual_snippet`, `redirect_golang_unsupported`); new codes are additive (MINOR). v5.0 additive codes: `redirect_composer_no_lockfile` / `redirect_gem_no_gemfile` (composer / gem: neither manifest nor lock present — once per run, after the intake gates), `redirect_gem_bundle_gemfile_unsupported` (gem: `BUNDLE_GEMFILE` — `BUNDLE_GEMFILE:` in the bundler app config, which outranks the environment variable as in `Bundler::Settings`, else the environment variable — names a manifest other than the project's `Gemfile` / `gems.rb`, so no gem is redirected or attested; a value naming one of those two selects that pair even when the other spelling is present), `redirect_gem_bundle_lockfile_unsupported` (gem: bundler 4's custom lockfile — the `BUNDLE_LOCKFILE` environment variable, else `BUNDLE_LOCKFILE:` in the bundler app config, else in the global config — names a lock other than the loaded pair's own `Gemfile.lock` / `gems.locked`, so no gem is redirected or attested rather than pinning a lock bundler ignores), `redirect_gem_twin_manifest_ambiguous` (gem: a `Gemfile` + `gems.rb` twin under default discovery; bundler 1.x loads the `Gemfile` and bundler ≥ 2 loads `gems.rb`, and a lock's `BUNDLED WITH` records which bundler wrote it, not which one installs it, so neither pair is wired or attested — remove the unused spelling or set `BUNDLE_GEMFILE` to the one in use), `redirect_gem_mirror_overrides_source` (gem: Bundler's all-source, exact patch-source or patch-hostname mirror can route the per-dep `source` block to an unpatched upstream gem. Intake reads the app config (`BUNDLE_APP_CONFIG`, where a set-but-empty value selects `/config`, honoring `BUNDLE_IGNORE_CONFIG`) and all `BUNDLE_MIRROR__...` variables visible to the scan; app config overrides the environment per encoded key, then `mirror.all` takes precedence over exact source, which takes precedence over hostname. URI matching follows Bundler's whole-URI case folding, default-port/trailing-slash normalization and single slash key alias, not URL prefixes. An exact-source fallback-timeout key without a mirror URL shadows the hostname mirror and fetches that source directly; a configured URL is conservatively refused even if a timeout could bypass an unreachable mirror at install time. Like `redirect_gem_bundle_gemfile_unsupported`, the gate leaves the Gemfile pair byte-identical and confirms no gem redirect. On an embedded `scan --vex`, rediscovered older hosted gem pins may attest only from verified installed bytes: a missing tree is not excused by the lockfile, and `--vex-no-verify` omits those hosted gems with `mirror_overrides_source` rather than trusting their intercepted source. Agent/vendored evidence, unrelated ecosystems and standalone VEX behavior are unchanged. Details identify the setting form and its app/environment origin without printing mirror values or source URLs, which may contain credentials. Remove the applicable all/source/hostname setting (including any slash alias) from that origin and reuse its existing mirror URL under `mirror.https://rubygems.org` to clear the refusal; an environment setting must be unset in the scan/install environment. User-global Bundler config and mirrors set only in a later install environment are not inspected; keep those mirrors scoped to the upstream source too), `redirect_maven_no_pom` (no `pom.xml` and no Gradle build), `redirect_nuget_lock_unparseable` (a present-but-corrupt `packages.lock.json` — warned once, nothing mutated; an absent lock still proceeds; a leading UTF-8 BOM, which dotnet reads past, is not corrupt and is kept on rewrite), `redirect_nuget_lock_other_version` (the lock also resolves the patched id at another version in some target framework: the exact-id `packageSourceMapping` would route that framework to a feed that serves only the patched version, so the dep is skipped with nothing written; only lock entries at the patched version are ever re-pinned, and `resolved` is never rewritten), `redirect_nuget_mapping_set_aside` (v5.0 #462: another source's `packageSourceMapping` also named the patched id exactly — Visual Studio's mapping UI writes such lists — which ties with the Socket source so NuGet would take the package from whichever feed answers first; that pattern, or its whole `` when it was the only one, is commented out in a `` comment while the patch is wired, and `remove` / `rollback` put it back byte-exact), `redirect_nuget_mapping_conflict` (such a pattern sits in markup that cannot go in a comment; the dep is skipped, nothing written). v5.0 #354: when the rewriter creates the `packageSourceMapping`, its `*` catch-all also names the sources NuGet merges in from the user config and every parent directory's config (honoring ``), since NuGet drops every source no pattern names; a fresh config only seeds `nuget.org` when those inherited configs have it (a parent that cleared it for a mirror keeps that choice); when an inherited config already has a `packageSourceMapping` (NuGet merges those too), no catch-all is written at all — only the Socket pattern — so a source the parent restricts is never widened; the exclusivity set-aside exempts only the source the run wires, so a `socket-patch-*` lookalike key or a stale uuid naming the id is set aside too, `redirect_cargo_lock_pkg_ambiguous` (several same-name+version `[[package]]` blocks and none carries the index `source` — transactional skip), `redirect_cargo_dep_overridden` (the crate does not resolve to crates.io because the user overrides it — its one `Cargo.lock` block carries a git / other-registry `source`, as a `[patch]` git or registry override in the root manifest or any cargo config leaves it, or has no `source` while the root `Cargo.toml` / project `.cargo/config*` holds a `[patch]` path entry for it; with no `Cargo.lock`, the same `[patch.crates-io]` / `[patch."https://github.com/rust-lang/crates.io-index"]` entry for the crate, by key or `package = "…"`, in the root `Cargo.toml` or the project `.cargo/config*` — transactional skip, nothing rewritten: a Socket pin would silently replace the user's fork and the then-unused `[patch]` entry breaks `cargo --locked`). Also additive: `redirect_gem_version_not_locked` (gem: no `GEM` section of the lock lists the crawled `name (version)`, for example a version another project installed into the shared gem home; the gem is skipped with nothing written, so the user's declared constraint and the locked version are never overwritten), `redirect_gem_no_lockfile` (gem: the project has a `Gemfile` / `gems.rb` but no `Gemfile.lock` / `gems.locked`, so nothing records which version it resolves and the crawled version may be another project's copy in the shared gem home; the gem is skipped with nothing written, never pinned over the user's constraint or appended as a new dependency, and the detail asks for `bundle lock` (or `bundle install`) and a re-run). Also v5.0: a registry override of the wrong kind (or none at all) warns the arm's missing-override code for nuget/gem/golang. Refusals stay fail-closed with a diagnosis that names the actual cause: a yarn-berry lock entry resolving through a non-`npm:` protocol keeps `redirect_yarn_berry_unsupported_protocol` with the entry's ACTUAL protocol in the detail — except socket-patch's OWN vendored wiring (a `file:` range into `.socket/vendor/`), which gets the distinct `redirect_yarn_berry_vendored_entry` code whose detail names the retirement path (`remove ` per package, or `vendor --revert` which unwinds every vendored package, then re-run `scan --mode hosted`). Both leave the entry byte-identical; neither changes exit code or status. **yarn berry line endings (v5.0)**: yarn writes a NEW `yarn.lock` with the OS line ending (`os.EOL` — CRLF on Windows) and keeps an existing lock's majority ending on every later write, and a `core.autocrlf` checkout turns an LF lock CRLF on any OS — so a uniformly CRLF lock is rewritten in its own ending: every untouched byte (a leading BOM included) round-trips (and `rollback`'s upstream restore keeps the lock's own ending). A lock that MIXES CRLF and LF (or holds a bare CR) has no single ending to keep — yarn's own `--immutable` check rejects it too (YN0028) — so it is refused untouched with `redirect_yarn_berry_mixed_line_endings` (the detail names `yarn install`, which normalizes it). The root `package.json`, which the rewrite re-renders to add `resolutions`, gets the same gate: a mixed one is refused untouched with the same code — the decision vendored mode takes with `vendor_yarn_berry_mixed_line_endings`, from the same shared berry gate set. This replaces v4's `redirect_yarn_berry_crlf_unsupported`, which refused every CRLF lock and is no longer emitted. A vendored→hosted takeover of a yarn-berry entry is checked against these project gates (mixed `yarn.lock` / `package.json` line endings, unsupported `cacheKey`, a non-zero `.yarnrc.yml` `compressionLevel`) on the project as it is before any revert, because the revert re-renders `package.json` in its majority ending — wet and `--dry-run` alike. A refused purl keeps its vendored wiring, ledger entry and artifact byte-identical, is skipped with the gate's code (reported in `warnings[]`, followed by `redirect_takeover_kept_vendored`) and is never announced as `redirect_takeover_reverted_vendored`. **Attribution gate (v5.0).** A hosted run never leaves wiring that lockfile discovery calls contested: before the rewrite writes any lockfile, the same discovery `vex`, `list`, `rollback`, `remove` and `vendor` read runs over the project as the rewrite would leave it. A candidate whose pin discovery reads but cannot attribute to one package version (a requirements `-r` include resolving the same version from the registry beside a rewired `Pipfile.lock`, #567; a Maven pin in a ``, #260) is left out of the rewrite and reported as a `skipped` event with `errorCode` `redirect_unattributable` (nothing written for it; exit code unchanged). Unchanged: a pin in a file discovery does not read (a pre-2.6 bundler `Gemfile`, locked by the next `bundle install`) keeps the rewriter's verdict, and so does a deliberate partial redirect the run already reports (a bundled or `bun patch`-ed copy left on the registry, a yarn `npm:` alias entry left on the registry with `redirect_yarn_classic_alias_skipped` / `redirect_yarn_berry_alias_skipped` while the package's direct entry is pinned, a dep withheld from the vlt rewrite while a sibling lock takes it), and so does a vendored→hosted takeover: its vendored wiring is reverted in the run's staged overlay before the rewrite plans, and it is redirected when the rewriters pin it (otherwise it is retracted and stays vendored), in a `--dry-run` preview the same. A lockless NuGet / Cargo pin (an exclusive Socket source mapping without `packages.lock.json`, a Cargo registry pin without `Cargo.lock`) is still written, with a `redirect_pin_lockless` warning: no lockfile records its version, so `vex` cannot attest it and `rollback` / `remove` / `vendor` refuse it until the lockfile exists (whether such pins should be written at all is an open decision). The rollout's recorded view uses the same discovery: a uuid a file merely mentions (a stale `package.json` field, an inactive `pdm.lock`, a comment) is not a pin and does not count as already patched. @@ -549,6 +549,12 @@ are patched by `socket-patch apply` like every other ecosystem: replaces the same `module@version` elsewhere); one already written fails `apply --check` / `vendor --check` and warns `go_workspace_replace_ineffective`, and `vex` omits the patch with `go_workspace_override` (#393, #458, #531). + `apply` also refuses a `replace` for a version the build list doesn't select (go.mod's `require` + for a go ≥ 1.17 graph; for an older one, the highest of `require` and go.sum's zip-line versions), + and one for a module a go ≥ 1.17 project doesn't use at all: Go keys `replace` by version, so it + would be ignored. One already written (a later `go get` bump) fails `apply --check` with a + resolved-version mismatch, and `vex` omits the patch, agent, vendored or hosted, with + `go_version_not_selected` (#391, #392, #509). ### sbt / Mill / scala-cli caches in agent mode diff --git a/crates/socket-patch-cli/src/commands/vex.rs b/crates/socket-patch-cli/src/commands/vex.rs index 209fd9a66..d0cc8d93d 100644 --- a/crates/socket-patch-cli/src/commands/vex.rs +++ b/crates/socket-patch-cli/src/commands/vex.rs @@ -845,6 +845,47 @@ async fn generate_vex( } } + // Go keys a replace by module AND version: a Socket replace (agent, + // vendored or hosted) pinned to a version the build list does not select + // is inert, and the build links the unpatched module however intact the + // copy or pin (#391, #392, #509). go.mod + go.sum say which version the + // build selects; never attest a patch for another one. + if !common.is_global() { + use socket_patch_core::utils::purl::build_golang_purl; + use socket_patch_core::utils::purl_key::PurlKey; + use socket_patch_core::vendor::go_build_list::{project_selection, Selection}; + for entry in socket_patch_core::vendor::go_mod_edit::read_replace_entries(&common.cwd).await + { + let Some(version) = entry.version.as_deref().filter(|_| entry.socket_owned()) else { + continue; + }; + let purl = build_golang_purl(&entry.module, version); + if !outcome.applied.iter().any(|p| PurlKey::same(p, &purl)) { + continue; + } + let Some(Selection::Other(selected)) = + project_selection(&common.cwd, &entry.module, version).await + else { + continue; + }; + outcome.applied.retain(|p| !PurlKey::same(p, &purl)); + note_warning( + warnings, + common, + GO_VERSION_NOT_SELECTED, + format!( + "{purl}: the build selects {} {selected}, so the replace pinned to \ + {version} is ignored and the build links the unpatched module", + entry.module + ), + ); + outcome.failed.push(FailedPatch { + purl, + reason: GO_VERSION_NOT_SELECTED.to_string(), + }); + } + } + // The plan's gate omissions (record_unavailable / record_mismatch / // vendor_unwired / redirect_unwired / wiring_conflict) join the omission // channel so they surface as per-purl `skipped` events like any @@ -1599,6 +1640,10 @@ fn join_and(items: &[&str]) -> String { /// overrides, ignores or rejects (#393, #458, #531). const GO_WORKSPACE_OVERRIDE: &str = "go_workspace_override"; +/// Omission tag for a Go patch whose version-pinned replace names a version +/// the build list does not select (#391, #392, #509). +const GO_VERSION_NOT_SELECTED: &str = "go_version_not_selected"; + /// Synthesize go-patches redirect targets for `vex`: for every socket-owned (`.socket/go-patches/`) `replace` in /// `go.mod` whose module+version maps to a golang PURL of the record view /// (`manifest` — for `vex` the merged manifest + ledger + lockfile view, so @@ -1733,6 +1778,10 @@ fn omission_phrase(reason: &str) -> &'static str { "the go.work in effect overrides, ignores or conflicts with its go.mod replace, \ so the build does not link the patched module" } + GO_VERSION_NOT_SELECTED => { + "go.mod and go.sum show the build selects another version of the module, so its \ + version-pinned replace is ignored and the build links the unpatched module" + } WIRING_CONFLICT => { "the lockfiles wire this package to different patches, so which one the build \ installs cannot be determined" @@ -1916,6 +1965,7 @@ mod tests { REDIRECT_UNWIRED, WIRING_CONFLICT, GO_WORKSPACE_OVERRIDE, + GO_VERSION_NOT_SELECTED, ] { assert_ne!( omission_phrase(tag), diff --git a/crates/socket-patch-cli/tests/covgap_commands_vex.rs b/crates/socket-patch-cli/tests/covgap_commands_vex.rs index 76d7f6496..5a98474be 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_vex.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_vex.rs @@ -993,6 +993,86 @@ fn go_patches_synthesis_skips_tampered_and_stale_replaces() { ); } +/// #391 / #392: a go-patches replace pinned to a version the build list no +/// longer selects is inert — after `go get M@v1.4.3` (go >= 1.17 require), +/// or in a go 1.16 graph where go.sum shows the build selects another +/// version — so vex must omit the patch however intact its copy is. +#[test] +fn go_patches_replace_for_an_unselected_version_is_not_attested() { + let module = "github.com/foo/bar"; + let version = "v1.4.2"; + let purl = format!("pkg:golang/{module}@{version}"); + let replace = + format!("replace {module} {version} => ./.socket/go-patches/{module}@{version}\n"); + let selected_v117 = + format!("module example.com/app\n\ngo 1.21\n\nrequire {module} {version}\n\n{replace}"); + let bumped_v117 = + format!("module example.com/app\n\ngo 1.21\n\nrequire {module} v1.4.3\n\n{replace}"); + let go116 = + format!("module example.com/app\n\ngo 1.16\n\nrequire example.com/mid v1.0.0\n\n{replace}"); + let go116_sum = format!( + "{module} {version}/go.mod h1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n\ + {module} v1.5.0 h1:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\n" + ); + for (go_mod, go_sum, attests) in [ + (&selected_v117, None, true), + (&bumped_v117, None, false), + (&go116, Some(go116_sum.as_str()), false), + (&go116, None, true), + ] { + let tmp = tempfile::tempdir().unwrap(); + let cwd = tmp.path(); + std::fs::write(cwd.join("go.mod"), go_mod).unwrap(); + if let Some(sum) = go_sum { + std::fs::write(cwd.join("go.sum"), sum).unwrap(); + } + let patched = b"package bar // patched\n"; + let copy = cwd.join(format!(".socket/go-patches/{module}@{version}")); + std::fs::create_dir_all(©).unwrap(); + std::fs::write(copy.join("bar.go"), patched).unwrap(); + let mut manifest = PatchManifest::new(); + manifest.patches.insert( + purl.clone(), + make_record( + GOOD_UUID, + "bar.go", + "a".repeat(64).as_str(), + &compute_git_sha256_from_bytes(patched), + "GHSA-go-unselected", + &["CVE-2026-391"], + ), + ); + write_manifest(cwd, &manifest); + let empty_cache = cwd.join("empty-gomodcache"); + std::fs::create_dir_all(&empty_cache).unwrap(); + + let out = cli() + .env("GOMODCACHE", &empty_cache) + .args([ + "vex", + "--cwd", + cwd.to_str().unwrap(), + "--product", + "pkg:golang/example.com/app@v0.0.1", + ]) + .output() + .expect("invoke vex"); + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + if attests { + assert!(out.status.success(), "{go_mod}\nstderr:\n{stderr}"); + assert!(stdout.contains("GHSA-go-unselected"), "{go_mod}\n{stdout}"); + } else { + assert_eq!(out.status.code(), Some(1), "{go_mod}\nstderr:\n{stderr}"); + assert!(!stdout.contains("not_affected"), "{go_mod}\n{stdout}"); + assert!( + stderr.contains("(go_version_not_selected)"), + "{go_mod}\nstderr:\n{stderr}" + ); + } + } +} + // ────────────────────────────────────────────────────────────────────── // Corrupt vendor ledger on a MANIFEST-FREE project (the D2 vendored // posture): the ledger was the only possible source of records, and it diff --git a/crates/socket-patch-core/src/patch/redirect/golang_local.rs b/crates/socket-patch-core/src/patch/redirect/golang_local.rs index 981249b6b..204965fe0 100644 --- a/crates/socket-patch-core/src/patch/redirect/golang_local.rs +++ b/crates/socket-patch-core/src/patch/redirect/golang_local.rs @@ -38,9 +38,9 @@ use crate::vendor::common::{ use crate::constants::SOCKET_DIR; use crate::patch::path_safety; use crate::utils::socket_dir::remove_tree_and_prune; +use crate::vendor::go_build_list::{self, Selection}; use crate::vendor::go_mod_edit::{ - self, read_replace_entries, read_required_versions, replace_target_path, ReplaceOwner, - GO_PATCHES_DIR, + self, read_replace_entries, replace_target_path, ReplaceOwner, GO_PATCHES_DIR, }; use crate::vendor::PackageSource; @@ -72,9 +72,10 @@ pub enum Drift { }, /// A socket-owned `replace` directive exists with no desired PURL. OrphanReplace { module: String }, - /// `go.mod`'s `require` set resolves this module to a version that does NOT - /// match the patched version, so the version-pinned `replace` is unused and - /// the build silently links the UNPATCHED module. + /// The build list selects a version of this module that does NOT match + /// the patched version (`go.mod`'s `require`, or for a pre-1.17 graph the + /// version go.sum records as built), so the version-pinned `replace` is + /// unused and the build silently links the UNPATCHED module. ResolvedVersionMismatch { purl: String, patched_version: String, @@ -117,8 +118,8 @@ impl std::fmt::Display for Drift { required_version, } => write!( f, - "{purl}: patched version {patched_version} is not the required version \ - (go.mod requires {required_version}) — go would link the UNPATCHED module" + "{purl}: patched version {patched_version} is not the version the build \ + selects ({required_version}) — go would link the UNPATCHED module" ), } } @@ -248,6 +249,14 @@ pub async fn apply_go_redirect<'a>( return synthesized_result(purl, ©_dir, Vec::new(), false, Some(refusal)); } + // A replace for a version the build list doesn't select is inert: the + // build links the unpatched module while apply would report it patched + // (#392). Discovery offers every cached version, so refuse before + // copying anything, the same gate the hosted rewriter applies. + if let Some(refusal) = selection_refusal(project_root, module, version).await { + return synthesized_result(purl, ©_dir, Vec::new(), false, Some(refusal)); + } + if dry_run { // Verify (read-only) against the pristine source for an accurate // "would patch" report, without creating the copy or editing go.mod. @@ -454,13 +463,12 @@ pub async fn reconcile_go_redirects( /// committed copies, and `go.mod` — never the module cache, no network — so it /// works on a fresh clone / airgapped CI. /// -/// Version cross-check limitation: the resolved-version comparison uses -/// `go.mod`'s `require` directives. After `go mod tidy` these list the selected -/// version of every module that provides an imported package (direct *and* -/// `// indirect`), so the common cases are covered. A patched module that is -/// **transitive-only and absent from `require`** cannot be version-checked here -/// (full MVS needs the toolchain); such a patch falling stale relies on the -/// build-time guard (which runs where `go` is present) for eventual detection. +/// Version cross-check: [`go_build_list::selection`] reads the selected +/// version from `go.mod` and `go.sum`. For a go ≥ 1.17 graph `require` lists +/// it for every module that provides a package; for an older graph go.sum's +/// zip lines show which version is built (#392). When neither file says, +/// the patch is not flagged; the build-time guard (which runs where `go` is +/// present) catches it. pub async fn verify_go_redirect_state( project_root: &Path, manifest: &PatchManifest, @@ -468,9 +476,6 @@ pub async fn verify_go_redirect_state( ) -> Result<(), Vec> { let mut drifts = Vec::new(); let entries = read_replace_entries(project_root).await; - // Required versions from go.mod (None ⇒ no go.mod ⇒ skip the version - // cross-check). Read once, project-local, offline. - let required = read_required_versions(project_root).await; let desired_modules: HashSet = desired .iter() .filter_map(|p| parse_golang_purl(p).map(|(m, _)| m.into_owned())) @@ -507,20 +512,20 @@ pub async fn verify_go_redirect_state( continue; } - // go.mod `require` cross-check: if the graph resolves this module to a - // version that is NOT the patched one, the version-pinned `replace` is - // unused and the build links the unpatched module — a silent-stale hole - // the copy/directive checks below can't see. (A module absent from - // `require` is harmless — it isn't built — so only flag a - // present-but-different resolution.) - if let Some(req) = required.as_ref().and_then(|r| r.get(module)) { - if req != version { - drifts.push(Drift::ResolvedVersionMismatch { - purl: purl.clone(), - patched_version: version.to_string(), - required_version: req.clone(), - }); - } + // Selected-version cross-check: if the build list selects a version + // that is NOT the patched one, the version-pinned `replace` is unused + // and the build links the unpatched module — a silent-stale hole the + // copy/directive checks below can't see. Only a proven other version + // is flagged: a module the build doesn't use at all isn't linked. + // Project-local and offline: go.mod + go.sum only. + if let Some(Selection::Other(selected)) = + go_build_list::project_selection(project_root, module, version).await + { + drifts.push(Drift::ResolvedVersionMismatch { + purl: purl.clone(), + patched_version: version.to_string(), + required_version: selected, + }); } let copy_dir = copy_dir_for(project_root, GO_PATCHES_DIR, module, version); @@ -593,6 +598,24 @@ pub async fn verify_go_redirect_state( // ── helpers ────────────────────────────────────────────────────────────────── +/// Why apply must not write a `replace` for `module@version`, when go.mod +/// and go.sum prove the build list does not select that version. +async fn selection_refusal(project_root: &Path, module: &str, version: &str) -> Option { + match go_build_list::project_selection(project_root, module, version).await? { + Selection::Other(selected) => Some(format!( + "the build selects {module} {selected}, not the patched {version}: a replace \ + pinned to {version} would be ignored and the build would link the unpatched \ + module; nothing written (get a patch for {selected}, or require {version})" + )), + Selection::NotInGraph => Some(format!( + "{module} {version} is not in this project's build graph (go.mod does not \ + require it and go.sum has no zip line for it): its replace would be ignored; \ + nothing written" + )), + Selection::Selected | Selection::Unknown => None, + } +} + /// Failure cleanup for the rebuild legs of [`apply_go_redirect`]. By the time /// a rebuild leg fails, any pre-existing copy is already gone (`fresh_copy` /// removes the destination first), so a pre-existing socket-owned `replace` @@ -1711,6 +1734,126 @@ mod tests { .any(|d| matches!(d, Drift::ResolvedVersionMismatch { .. }))); } + /// The #392 graph in a go 1.16 go.mod: `mida` requires bar v1.4.2, + /// `midb` requires v1.5.0, MVS selects v1.5.0. go.mod lists neither + /// version; go.sum keeps only a `/go.mod` line for the losing v1.4.2. + const GO116_MOD: &str = "module example.com/app\n\ngo 1.16\n\nrequire (\n\texample.com/mida v1.0.0\n\texample.com/midb v1.0.0\n)\n"; + const GO116_SUM: &str = "github.com/foo/bar v1.4.2/go.mod h1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n\ + github.com/foo/bar v1.5.0 h1:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\n\ + github.com/foo/bar v1.5.0/go.mod h1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n"; + + async fn apply_fixture( + root: &Path, + blobs: &Path, + pristine: &Path, + files: &HashMap, + ) -> ApplyResult { + let sources = PatchSources::blobs_only(blobs); + apply_go_redirect( + PURL, + MODULE, + VERSION, + pristine, + root, + GO_PATCHES_DIR, + files, + &sources, + false, + MismatchPolicy::Warn, + ) + .await + } + + /// #392: apply must not write a replace for a version the build list + /// doesn't select — not in a go 1.16 graph where go.sum shows another + /// version is built, not when a go >= 1.17 go.mod requires another + /// version, and not when the project doesn't use the module at all. + #[tokio::test] + async fn test_apply_refuses_a_version_the_build_does_not_select() { + for (go_mod, go_sum, needle) in [ + (GO116_MOD, Some(GO116_SUM), "the build selects github.com/foo/bar v1.5.0"), + ( + "module example.com/app\n\ngo 1.21\n\nrequire github.com/foo/bar v1.5.0 // indirect\n", + None, + "the build selects github.com/foo/bar v1.5.0", + ), + ("module example.com/app\n\ngo 1.21\n", None, "not in this project's build graph"), + ] { + let (dir, blobs, pristine, files, _after) = fixture().await; + let root = dir.path(); + tokio::fs::write(root.join("go.mod"), go_mod).await.unwrap(); + if let Some(sum) = go_sum { + tokio::fs::write(root.join("go.sum"), sum).await.unwrap(); + } + let result = apply_fixture(root, &blobs, &pristine, &files).await; + assert!(!result.success, "{go_mod}"); + let error = result.error.unwrap_or_default(); + assert!(error.contains(needle), "{error}"); + assert_eq!( + tokio::fs::read_to_string(root.join("go.mod")).await.unwrap(), + go_mod, + "nothing written" + ); + assert!(!root.join(GO_PATCHES_DIR).exists(), "no copy"); + } + } + + /// The selected version still applies in a go 1.16 graph, including a + /// transitive module go.mod doesn't list, and the re-run after + /// `go mod tidy` (which drops the replaced original's go.sum lines) + /// stays in sync. + #[tokio::test] + async fn test_apply_go116_selected_version_still_applies() { + let (dir, blobs, pristine, files, _after) = fixture().await; + let root = dir.path(); + tokio::fs::write(root.join("go.mod"), GO116_MOD) + .await + .unwrap(); + let sum = "github.com/foo/bar v1.4.2 h1:CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=\n\ + github.com/foo/bar v1.4.2/go.mod h1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n"; + tokio::fs::write(root.join("go.sum"), sum).await.unwrap(); + assert!(apply_fixture(root, &blobs, &pristine, &files).await.success); + // `go mod tidy` drops the replaced original's lines. + tokio::fs::write(root.join("go.sum"), "").await.unwrap(); + assert!(apply_fixture(root, &blobs, &pristine, &files).await.success); + let manifest = manifest_with(&files); + let desired: HashSet = [PURL.to_string()].into_iter().collect(); + assert_eq!( + verify_go_redirect_state(root, &manifest, &desired).await, + Ok(()) + ); + } + + /// #392: in a go 1.16 graph `apply --check` must flag a redirect whose + /// module go.mod doesn't list when go.sum shows the build selects + /// another version — "absent from require" isn't "not built" there. + #[tokio::test] + async fn test_verify_flags_go116_transitive_version_mismatch() { + let (dir, blobs, pristine, files, _after) = fixture().await; + let root = dir.path(); + assert!(apply_fixture(root, &blobs, &pristine, &files).await.success); + let replace = "\nreplace github.com/foo/bar v1.4.2 => ./.socket/go-patches/github.com/foo/bar@v1.4.2\n"; + tokio::fs::write(root.join("go.mod"), format!("{GO116_MOD}{replace}")) + .await + .unwrap(); + tokio::fs::write(root.join("go.sum"), GO116_SUM) + .await + .unwrap(); + let manifest = manifest_with(&files); + let desired: HashSet = [PURL.to_string()].into_iter().collect(); + let drifts = verify_go_redirect_state(root, &manifest, &desired) + .await + .unwrap_err(); + assert_eq!( + drifts, + vec![Drift::ResolvedVersionMismatch { + purl: PURL.into(), + patched_version: VERSION.into(), + required_version: "v1.5.0".into(), + }] + ); + } + #[tokio::test] async fn test_verify_orphan_replace() { let (dir, blobs, pristine, files, _after) = fixture().await; diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index dcdcbe249..e4c2765a5 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -8337,6 +8337,7 @@ fn rewrite_golang( overrides: &[DepOverride], result: &mut RewriteResult, ) { + use crate::vendor::go_build_list; use crate::vendor::go_mod_edit::{self, HOSTED_GO_MODULE_PREFIX}; use crate::vendor::go_sum_edit::{self, GoSumEditor}; @@ -8473,64 +8474,80 @@ fn rewrite_golang( // left in place, its module path keeps confirming the dep as // redirected (ledger + VEX attestation) while go links the unpatched // version. - if let Some(required) = scan.required.as_ref() { - if required != &dep.version { - result.warnings.push(RewriteWarning { - code: "redirect_golang_version_mismatch".into(), - detail: format!( - "{fname}: go.mod requires {required} but the patch targets {} — \ - a version-pinned replace would be silently ignored", - dep.version - ), - }); - let stale_hosted = - prior.filter(|e| e.owner == Some(go_mod_edit::ReplaceOwner::Hosted)); - if let Some(stale) = stale_hosted { - if let Ok(Some(new)) = go_mod_edit::remove_replace_entry( - &go_mod, - &fname, - go_mod_edit::ReplaceOwner::Hosted, - ) { - go_mod = new; - mod_changed = true; + // + // Which version the build selects comes from go.mod and go.sum + // (`go_build_list`): `require` for a go >= 1.17 graph; for an older + // one, the highest of `require` and go.sum's zip lines, since a + // `/go.mod`-only line is written for every version MVS reads, not + // only the one it selects (#509). + let zips = go_sum.zip_versions(&fname); + let zips: Vec<&str> = zips.iter().map(String::as_str).collect(); + let selection = go_build_list::select( + go_mod_edit::prunes_module_graph(&go_mod), + scan.required.as_deref(), + &zips, + &dep.version, + ); + if let Some(selected) = selection.other_version() { + let basis = if scan.required.as_deref() == Some(selected) { + "go.mod requires" + } else { + "go.sum shows the build selects" + }; + result.warnings.push(RewriteWarning { + code: "redirect_golang_version_mismatch".into(), + detail: format!( + "{fname}: {basis} {selected} but the patch targets {} — \ + a version-pinned replace would be silently ignored", + dep.version + ), + }); + let stale_hosted = prior.filter(|e| e.owner == Some(go_mod_edit::ReplaceOwner::Hosted)); + if let Some(stale) = stale_hosted { + if let Ok(Some(new)) = go_mod_edit::remove_replace_entry( + &go_mod, + &fname, + go_mod_edit::ReplaceOwner::Hosted, + ) { + go_mod = new; + mod_changed = true; + result.edits.push(FileEdit { + path: "go.mod".into(), + kind: "redirect_golang_stale_replace_removed".into(), + action: "removed".into(), + key: Some(fname.clone()), + original: prior_text.clone().map(Value::String), + new: None, + }); + } + if let Some(stale_rhs) = stale.rhs_module.as_deref() { + if go_sum.remove_module_prefix_lines(stale_rhs) { + sum_changed = true; result.edits.push(FileEdit { - path: "go.mod".into(), - kind: "redirect_golang_stale_replace_removed".into(), + path: "go.sum".into(), + kind: "redirect_golang_stale_gosum_removed".into(), action: "removed".into(), - key: Some(fname.clone()), - original: prior_text.clone().map(Value::String), + key: Some(stale_rhs.to_string()), + original: None, new: None, }); } - if let Some(stale_rhs) = stale.rhs_module.as_deref() { - if go_sum.remove_module_prefix_lines(stale_rhs) { - sum_changed = true; - result.edits.push(FileEdit { - path: "go.sum".into(), - kind: "redirect_golang_stale_gosum_removed".into(), - action: "removed".into(), - key: Some(stale_rhs.to_string()), - original: None, - new: None, - }); - } - } } - continue; } - } else if !go_sum.has_module_version(&fname, &dep.version) + continue; + } else if selection != go_build_list::Selection::Selected && prior.is_none_or(|e| e.version.as_deref() != Some(dep.version.as_str())) { - // Not required, not in go.sum at this version, and not already - // redirected by us: the module is outside this project's graph - // (local discovery crawls the whole module cache). Its replace - // would be inert, and confirming it would attest a patch no - // build links. + // Not selected by go.mod's require, no go.sum zip line at this + // version, and not already redirected by us: the module is + // outside this project's graph (local discovery crawls the whole + // module cache). Its replace would be inert, and confirming it + // would attest a patch no build links. result.warnings.push(RewriteWarning { code: "redirect_golang_not_in_module_graph".into(), detail: format!( - "{fname}@{}: not required by go.mod and absent from go.sum — the \ - module is not in this project's build graph; nothing redirected", + "{fname}@{}: not required by go.mod and go.sum has no zip line for it — \ + the module is not in this project's build graph; nothing redirected", dep.version ), }); @@ -21480,6 +21497,48 @@ packages: ); } + /// #509: in a go 1.16 graph go writes a `/go.mod` line for every version + /// MVS reads, so the losing v1.4.2 keeps one while the build selects + /// v1.5.0 (the only zip line). That line is not "in the graph": the + /// redirect would be inert, so nothing is written. + #[test] + fn golang_go116_go_mod_only_line_is_not_the_selected_version() { + let go_mod = "module example.com/app\n\ngo 1.16\n\nrequire (\n\texample.com/mida v1.0.0\n\texample.com/midb v1.0.0\n)\n"; + let go_sum = "github.com/foo/bar v1.4.2/go.mod h1:UPSTREAMM=\n\ + github.com/foo/bar v1.5.0 h1:NEWER=\n\ + github.com/foo/bar v1.5.0/go.mod h1:NEWERM=\n"; + let mut files = golang_files(); + files.insert("go.mod".to_string(), go_mod.to_string()); + files.insert("go.sum".to_string(), go_sum.to_string()); + let ovr = golang_override(); + let out = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr)); + assert!(out.files.is_empty(), "nothing written: {:?}", out.files); + assert_eq!( + out.warnings + .iter() + .map(|w| w.code.as_str()) + .collect::>(), + ["redirect_golang_version_mismatch"] + ); + assert!( + out.warnings[0] + .detail + .contains("go.sum shows the build selects v1.5.0"), + "{}", + out.warnings[0].detail + ); + + // The selected version still redirects in the same go 1.16 graph. + let selected = go_sum.replace("v1.5.0", "v1.4.1"); + files.insert( + "go.sum".to_string(), + format!("{selected}github.com/foo/bar v1.4.2 h1:UPSTREAM=\n"), + ); + let out = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr)); + assert!(out.warnings.is_empty(), "warnings: {:?}", out.warnings); + assert!(out.files["go.mod"].contains("replace github.com/foo/bar v1.4.2 =>")); + } + /// Hosted confirmation keys off `confirmed_golang_uuids`: set when the /// redirect lands or is already in place, never for a refused dep. #[test] diff --git a/crates/socket-patch-core/src/vendor/go_build_list.rs b/crates/socket-patch-core/src/vendor/go_build_list.rs new file mode 100644 index 000000000..0b25220b8 --- /dev/null +++ b/crates/socket-patch-core/src/vendor/go_build_list.rs @@ -0,0 +1,228 @@ +//! Offline answer to "does the build list select `module@version`?". +//! +//! Go keys a `replace` by module path **and version**, so a replace pinned +//! to a version the build list does not select is silently inert: the build +//! links the unpatched module and nothing warns. Every Go path that writes, +//! audits or attests such a replace (agent `apply`, `apply --check`, `vex`, +//! the hosted rewriter, vex lockfile discovery) asks this one question here +//! instead of keeping its own go.mod/go.sum heuristic (#391, #392, #509). +//! +//! The answer comes from the project's `go.mod` and `go.sum` only (no +//! toolchain, no network), so it works on a fresh clone in CI: +//! +//! * **go ≥ 1.17** (pruned graph): `require` lists the selected version of +//! every module that provides a package to the build, so it is +//! authoritative. A module absent from `require` is built only if go.sum +//! carries its zip line at that version. +//! * **go < 1.17** (or no `go` directive): `require` holds lower bounds and +//! omits transitive modules. MVS selects the highest version the graph +//! asks for, and go.sum records a zip (`h1:`) line for each version whose +//! packages are built. A `/go.mod`-only line means MVS *read* that +//! version's requirements, not that it selected it (#509). So the highest +//! of the required version and the zip-line versions is the selected one. +//! With no evidence at or above the patched version, the answer is +//! [`Selection::Unknown`]: a directory `replace` makes `go mod tidy` drop +//! the original's go.sum lines, so absence proves nothing. + +use std::cmp::Ordering; +use std::path::Path; + +use crate::utils::fs::read_regular_to_string; + +use super::go_mod_edit::{parse_required_versions, prunes_module_graph}; +use super::go_sum_edit::{go_semver_cmp, go_sum_lines}; + +/// Whether the build list selects the patched version of a module. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Selection { + /// The build selects exactly the patched version. + Selected, + /// The build selects this other version, so a replace pinned to the + /// patched version is inert. + Other(String), + /// The module is not in the build at all (pruned graph only). + NotInGraph, + /// go.mod and go.sum hold no evidence either way. + Unknown, +} + +impl Selection { + /// The version the build selects instead of the patched one, when the + /// files prove a replace pinned to the patched version is inert. + pub fn other_version(&self) -> Option<&str> { + match self { + Selection::Other(v) => Some(v), + _ => None, + } + } +} + +/// [`Selection`] for `module@version` from the text of `go.mod` and (when +/// present) `go.sum`. +pub fn selection(go_mod: &str, go_sum: Option<&str>, module: &str, version: &str) -> Selection { + let required = parse_required_versions(go_mod).remove(module); + let zips: Vec<&str> = go_sum + .map(|text| { + go_sum_lines(text) + .filter(|l| !l.go_mod && l.module == module) + .map(|l| l.version) + .collect() + }) + .unwrap_or_default(); + select( + prunes_module_graph(go_mod), + required.as_deref(), + &zips, + version, + ) +} + +/// [`selection`] from already-parsed parts: whether the graph is pruned, +/// the module's `require` version, and the versions go.sum holds a zip line +/// for. +pub fn select(pruned: bool, required: Option<&str>, zips: &[&str], version: &str) -> Selection { + if pruned { + return match required { + Some(r) if r == version => Selection::Selected, + Some(r) => Selection::Other(r.to_string()), + None if zips.contains(&version) => Selection::Selected, + None => Selection::NotInGraph, + }; + } + let highest = required + .into_iter() + .chain(zips.iter().copied()) + .max_by(|a, b| go_semver_cmp(a, b)); + match highest { + Some(h) if h == version => Selection::Selected, + Some(h) if go_semver_cmp(h, version) == Ordering::Greater => { + Selection::Other(h.to_string()) + } + _ => Selection::Unknown, + } +} + +/// [`selection`] for a project directory: reads `/go.mod` and +/// `go.sum` (`None` when there is no readable go.mod). In workspace mode the +/// build list also takes other members' requirements, so a module absent +/// from this go.mod may still be built: [`Selection::NotInGraph`] becomes +/// [`Selection::Unknown`] there. +pub async fn project_selection( + project_root: &Path, + module: &str, + version: &str, +) -> Option { + let go_mod = read_regular_to_string(&project_root.join("go.mod")) + .await + .ok()?; + let go_sum = read_regular_to_string(&project_root.join("go.sum")) + .await + .ok(); + let sel = selection(&go_mod, go_sum.as_deref(), module, version); + if sel == Selection::NotInGraph && super::go_consumer_sync::workspace_in_effect(project_root) { + return Some(Selection::Unknown); + } + Some(sel) +} + +#[cfg(test)] +mod tests { + use super::*; + + const UP: &str = "example.com/upstream"; + + fn sum(lines: &[&str]) -> String { + lines.iter().map(|l| format!("{l} h1:x=\n")).collect() + } + + #[test] + fn pruned_graph_trusts_require() { + let m = format!("module c\n\ngo 1.21\n\nrequire {UP} v1.0.1 // indirect\n"); + assert_eq!( + selection(&m, None, UP, "v1.0.0"), + Selection::Other("v1.0.1".into()) + ); + assert_eq!(selection(&m, None, UP, "v1.0.1"), Selection::Selected); + } + + #[test] + fn pruned_graph_without_require_is_not_in_graph() { + let m = "module c\n\ngo 1.21\n"; + assert_eq!(selection(m, None, UP, "v1.0.0"), Selection::NotInGraph); + let s = sum(&[&format!("{UP} v1.0.0/go.mod")]); + assert_eq!(selection(m, Some(&s), UP, "v1.0.0"), Selection::NotInGraph); + let s = sum(&[&format!("{UP} v1.0.0"), &format!("{UP} v1.0.0/go.mod")]); + assert_eq!(selection(m, Some(&s), UP, "v1.0.0"), Selection::Selected); + } + + /// #509 / #392: in a go 1.16 graph a losing version keeps only its + /// `/go.mod` line, and the selected version has the zip line. + #[test] + fn unpruned_graph_reads_the_selected_version_from_zip_lines() { + let m = "module c\n\ngo 1.16\n\nrequire (\n\texample.com/mida v1.0.0\n\texample.com/midb v1.0.0\n)\n"; + let s = sum(&[ + &format!("{UP} v1.0.0/go.mod"), + &format!("{UP} v1.0.1"), + &format!("{UP} v1.0.1/go.mod"), + ]); + assert_eq!( + selection(m, Some(&s), UP, "v1.0.0"), + Selection::Other("v1.0.1".into()) + ); + assert_eq!(selection(m, Some(&s), UP, "v1.0.1"), Selection::Selected); + } + + #[test] + fn unpruned_require_is_a_lower_bound() { + let m = format!("module c\n\ngo 1.16\n\nrequire {UP} v1.0.0\n"); + let s = sum(&[&format!("{UP} v1.0.1")]); + assert_eq!( + selection(&m, Some(&s), UP, "v1.0.0"), + Selection::Other("v1.0.1".into()) + ); + // A tidied directory replace drops the original's go.sum lines. + assert_eq!(selection(&m, Some(""), UP, "v1.0.0"), Selection::Selected); + // Only a lower bound below the patched version: can't tell. + assert_eq!(selection(&m, None, UP, "v1.2.0"), Selection::Unknown); + } + + #[test] + fn missing_go_directive_is_unpruned() { + let m = "module c\n"; + assert_eq!(selection(m, None, UP, "v1.0.0"), Selection::Unknown); + let s = sum(&[&format!("{UP} v1.0.1")]); + assert_eq!( + selection(m, Some(&s), UP, "v1.0.0"), + Selection::Other("v1.0.1".into()) + ); + } + + #[test] + fn go_directive_versions() { + for (d, pruned) in [ + ("go 1.16", false), + ("go 1.17", true), + ("go 1.21.0", true), + ("go 1.9", false), + ("go 1.22rc1", true), + ("go 2.0", true), + ("go \t1.24 // pinned", true), + ] { + assert_eq!( + prunes_module_graph(&format!("module c\n\n{d}\n")), + pruned, + "{d}" + ); + } + assert!(!prunes_module_graph("module c\ngodebug default=go1.21\n")); + } + + #[test] + fn incompatible_versions_compare_exactly() { + let m = format!("module c\n\ngo 1.21\n\nrequire {UP} v2.0.0+incompatible\n"); + assert_eq!( + selection(&m, None, UP, "v2.0.0+incompatible"), + Selection::Selected + ); + } +} diff --git a/crates/socket-patch-core/src/vendor/go_consumer_sync.rs b/crates/socket-patch-core/src/vendor/go_consumer_sync.rs index ce7649ab9..c4ec131b9 100644 --- a/crates/socket-patch-core/src/vendor/go_consumer_sync.rs +++ b/crates/socket-patch-core/src/vendor/go_consumer_sync.rs @@ -280,6 +280,13 @@ fn modules_txt_location(project_root: &Path) -> (PathBuf, &'static str) { ) } +/// Whether the go command builds `project_root` in workspace mode (a +/// `go.work` from `GOWORK` or at or above the project), so its build list +/// spans every member's requirements, not only the project go.mod's. +pub(crate) fn workspace_in_effect(project_root: &Path) -> bool { + workspace_file(project_root).is_some() +} + /// The `go.work` go would use for `project_root`: `GOWORK` (resolved as go /// resolves it: the environment, else the `go env -w` file) when it names a /// file (`off` disables workspaces), else the nearest `go.work` at or above diff --git a/crates/socket-patch-core/src/vendor/go_mod_edit.rs b/crates/socket-patch-core/src/vendor/go_mod_edit.rs index 4a9b49de8..d0de79958 100644 --- a/crates/socket-patch-core/src/vendor/go_mod_edit.rs +++ b/crates/socket-patch-core/src/vendor/go_mod_edit.rs @@ -405,6 +405,26 @@ pub fn parse_required_versions(content: &str) -> HashMap { out } +/// True when the `go` directive is 1.17 or later, so the module graph is +/// pruned and `require` lists the selected version of every module that +/// provides a package to the build. Without the directive go assumes 1.16, +/// whose `require` set is only a set of lower bounds. +pub fn prunes_module_graph(content: &str) -> bool { + let mut pruned = false; + let _ = for_each_directive_body(content, "go", |_, body| { + let mut parts = body.split_whitespace().next().unwrap_or("").split('.'); + let major = parts.next().and_then(|p| p.parse::().ok()); + let minor = parts.next().and_then(|p| { + let digits: String = p.chars().take_while(char::is_ascii_digit).collect(); + digits.parse::().ok() + }); + pruned = matches!((major, minor), (Some(m), _) if m > 1) + || matches!((major, minor), (Some(1), Some(n)) if n >= 17); + Ok(()) + }); + pruned +} + /// The module path the go.mod `module` directive declares, in its /// single-line (`module example.com/m`) or block (`module ( … )`) form, /// quoted or bare, with a trailing `//` comment and a leading BOM allowed. diff --git a/crates/socket-patch-core/src/vendor/go_sum_edit.rs b/crates/socket-patch-core/src/vendor/go_sum_edit.rs index 0fd80d3fe..4cbb022c6 100644 --- a/crates/socket-patch-core/src/vendor/go_sum_edit.rs +++ b/crates/socket-patch-core/src/vendor/go_sum_edit.rs @@ -289,6 +289,18 @@ impl GoSumEditor { .any(|l| is_version_line(l, module, version)) } + /// The versions of `module` the content carries a zip (`h1:` of the + /// module tree) line for, in file order — the versions whose packages + /// the build loads, unlike the `/go.mod` lines go writes for every + /// version MVS reads. + pub(crate) fn zip_versions(&self, module: &str) -> Vec { + self.current_lines() + .flat_map(go_sum_lines) + .filter(|l| !l.go_mod && l.module == module) + .map(|l| l.version.to_string()) + .collect() + } + /// Remove the lines for exactly `module@version` (both the zip and /// `/go.mod` forms). Used to prune the REPLACED original's lines: once a /// version-pinned `replace` covers the resolved version, go never fetches diff --git a/crates/socket-patch-core/src/vendor/mod.rs b/crates/socket-patch-core/src/vendor/mod.rs index d1c3b1bcd..ca0b10bab 100644 --- a/crates/socket-patch-core/src/vendor/mod.rs +++ b/crates/socket-patch-core/src/vendor/mod.rs @@ -63,6 +63,7 @@ pub mod cargo_tag; pub(crate) mod common; pub mod composer_lock; pub mod gem; +pub mod go_build_list; pub mod go_consumer_sync; pub mod go_mod_edit; pub mod go_sum_edit; diff --git a/docs/ecosystems.md b/docs/ecosystems.md index a87979f90..5ab363d3c 100644 --- a/docs/ecosystems.md +++ b/docs/ecosystems.md @@ -1326,6 +1326,16 @@ A replace written before the `go.work` changed is reported as leaves the patch out (`go_workspace_override`). To patch the whole workspace, apply in one member it uses, or move the Socket `replace` into `go.work`. +A `replace` names an exact module version, so it only works while the build +selects that version. socket-patch reads which version that is from `go.mod` +and `go.sum`: the `require` line in a go 1.17+ module, or, in an older module +(whose `require` lists only lower bounds and leaves out transitive modules), +the highest of the required version and the versions `go.sum` has a module +zip line for. `apply` and `scan --mode hosted` refuse to write a replace for +any other version. After an upgrade such as `go get M@v1.0.1`, `apply --check` +fails and `vex` leaves the patch out (`go_version_not_selected`) until you +get a patch for the new version. + Paid hosted Go references are unsupported: embedding credentials in module paths would expose them to module proxies and change module identity. Use vendored mode for those patches; the CLI reports `redirect_golang_unsupported` when the required From fcca611870876090765b8e076cb647592978a48b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 22:42:37 +0000 Subject: [PATCH 3/6] Re-bless Go rewrite golden for new gate message The not-in-graph warning now says go.sum has no zip line for the version, since a /go.mod-only line no longer counts. Checked that the old golden still matches with the old wording, so only the message changed. The +incompatible reconcile test now requires the version it applies. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/patch/redirect/golang_local.rs | 7 + .../tests/equivalence/golang_rewrite.golden | 594 +++++++++--------- 2 files changed, 304 insertions(+), 297 deletions(-) diff --git a/crates/socket-patch-core/src/patch/redirect/golang_local.rs b/crates/socket-patch-core/src/patch/redirect/golang_local.rs index 204965fe0..59e86583c 100644 --- a/crates/socket-patch-core/src/patch/redirect/golang_local.rs +++ b/crates/socket-patch-core/src/patch/redirect/golang_local.rs @@ -2083,6 +2083,13 @@ mod tests { let (module, version) = parse_golang_purl(encoded_purl).unwrap(); // After parsing, version is `v2.0.0+incompatible` (decoded). assert_eq!(version, "v2.0.0+incompatible"); + // The build must select the patched version for apply to wire it. + tokio::fs::write( + root.join("go.mod"), + "module example.com/app\n\ngo 1.21\n\nrequire github.com/foo/bar v2.0.0+incompatible\n", + ) + .await + .unwrap(); let result = apply_go_redirect( encoded_purl, diff --git a/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden b/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden index 538d71421..13672fd32 100644 --- a/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden +++ b/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden @@ -1,302 +1,302 @@ # One seeded go.mod / go.sum pair + overrides, or the re-run over its output. # -0-19 8affc108e0fea83c d217609ae2b6d375 -20-39 2e8996839f016121 a6f67b7a540e2d5a -40-59 a86f60d4d868c79c d11b7a64905bc1ea -60-79 802e356468c4982c 5c7c80663f4e06d4 -80-99 35a6b1febe605c4f a65ffa255457f57f -100-119 bd3577de6dc6538e 9ead57b9341fa876 -120-139 4f16b94967700492 6de9209ec21c5e87 -140-159 e54c48f83f4f8b9f 5c580e4063e600ee -160-179 515b4fd117413b07 3bd58357b361e948 -180-199 5109488d3766b78d 76221cf3974652ec -200-219 28f6319a64dc3fbb 96d40e96c9a0edf9 -220-239 39618d849d8fda8c 26dffcbea2452394 -240-259 bdae75a5c4e825a6 2a5f51ef3dabc45b -260-279 c745748faa22afc7 687a509c034547a4 -280-299 1011d2e2a4d5b0ec ba572c70a61b7af4 -300-319 4b16f76f77f2864d 9cd531d6442ce036 -320-339 39bd1e211fc7dc2d 9366ce11dd037c95 -340-359 3301fbd0c3711331 7e0577f3964bd709 -360-379 df998500fae69f60 784dedd3b5051ff1 -380-399 723fe126896b13a6 3be8d3ccfeea3492 -400-419 8af5995cf2f76dea cb4c682266cf703c -420-439 0fe2bc1416e755d6 2e7972d1e3bba646 -440-459 3df2c0d14ad9d565 830bb30e194e2080 -460-479 560a1006d9373f2b 7058a857b37bf6e0 -480-499 26d14b61cf702dab 67592452ce9e29db -500-519 3961ba4ed9781da7 6d91b40fe0077b73 -520-539 1b38432bef93a466 077967265c1cd4fd -540-559 267215b64ecec280 196475a37c88b633 -560-579 21b9a5619b7413e4 4c37e05a7224de06 -580-599 b5545d490aac09a9 c6559305a9fc03f9 -600-619 e8dd88bc440f73f8 1a23fea251df37ae -620-639 af3b19b435cf6fac 3e8f4503cade03d0 -640-659 bae7e86659b156c3 6d894f821baaa81f -660-679 d3f04db367baa7ca 23c4fdf0614be428 -680-699 963d9788fb95ba48 1b0a8013aab9b9ff -700-719 d9f7740b1b0aaa1d 0e9d8a51c9dc660b -720-739 61b704425a73b56d 2cdefa768429a2e9 -740-759 a1539928074f96af 2466fa22e4dca6ee -760-779 a71deef5b9b2c42e b5a2d9405cf320cb -780-799 6dc37e510ac31cbf 28b8c3bc01b4f66a -800-819 0116b8f40912ff10 af22460213179a03 -820-839 a894c27412910ab3 bd9061bc9b125f6a -840-859 9e9eaed3d98358ee 3fe03ce528cc985c -860-879 92d169f487a02ea0 f8813ff1d0441c6e -880-899 6cda3400ac7fe005 90d60665a8fc5a8b -900-919 1deaf15626d0a873 6cc5208e0f281496 -920-939 1de022c7c9306a5e 4251e877baf0c488 -940-959 b1abc86bef6b3063 95036d70702b674d -960-979 91a059a20d7961f4 44f9de049d21e73d -980-999 6642fbe828ba9cd2 d43c047c8e275f33 -1000-1019 0ff1157561f6e666 8471546cd9252f52 -1020-1039 6170e2fe8d9c912f 88aee1438b50c94e -1040-1059 2c7a678f7bef19b5 626885bf2eb77ec2 -1060-1079 d7df42b4f6319ea4 10a5fad62f7dc00a -1080-1099 38a227b69e3f6ae7 ade9923b97f6afb6 -1100-1119 cec7880ed7eec8b2 5cc6711e6965488a -1120-1139 fde72a21c0a18004 d27054192986e456 -1140-1159 83e9cba84dde6ad3 0b15482eb870fcc3 -1160-1179 7f90cc68e619694e d68df1393fbdd500 -1180-1199 f06d71f7c517a0a2 d36743aa98721a08 -1200-1219 1da3d2d5deb1cf4d 94f1186fbf9534e8 -1220-1239 0722c3f2eb664b0c ffcf13aedf1e706f -1240-1259 1c0655e6360b5de5 2453a51f68a72380 -1260-1279 a1579b39d5ac7bbe f9c7eb8621d19ce8 -1280-1299 cd1ee243a50b3619 7a519d7744966974 -1300-1319 206339cf9ebd5ae0 8f3e984ed962380f -1320-1339 86094c70406465d1 1a46b7ca1c33bec4 -1340-1359 173a4ba8543a2e36 52c34931464f099e -1360-1379 76e4d6fbd8d437ef 430ac3b111334b2d -1380-1399 559ca65099b73a05 122e3b6d92b9b62b -1400-1419 39dd13b5fbbbf0e6 b83f304811e7d9b1 -1420-1439 d00c1c4afb54a563 67e1757624c55eba -1440-1459 4785cfd44210b032 3b769c0716a58286 -1460-1479 db68a6b46a1304ea ff04ed7d28aed2f8 -1480-1499 32c66b76e3c3b526 18796517b568bb60 -1500-1519 943c1df4bb8cb02a 66f879f9862b445b -1520-1539 a289fd547fd60656 fc7b7dc6491ad20e -1540-1559 4874a2ac9f5077ce e535b07601b6fdc8 -1560-1579 f0f5333624b0a0bc e607a6fd0cb22d10 -1580-1599 ceb59f6639170e68 28e462491ec7c339 -1600-1619 cb6f199400f49f4d 343a1135b50c03c8 -1620-1639 071ea36a03e8574e 52422c98a51b0d5c -1640-1659 84bdbdd5d83a725a 01fe2de48922008e -1660-1679 f51bf7929ee1247d f6b9ba1f8e9b1f21 -1680-1699 4bb10b37555656a4 3880415ae0e234cb -1700-1719 f3adc36c8c101d61 a7ea83bf0b9f1f0a -1720-1739 1b3bc33b47ee5644 70f6c20e3412377f -1740-1759 dc1f5483bef22de7 64c3abc0414937e3 -1760-1779 51bcf5fe24b39c29 b1a990873e9914dc -1780-1799 02fe708f48a2ddc2 604127d239db44a9 -1800-1819 a384e804075283ca ce017362903b025e -1820-1839 c0673d4164f0b499 c737ee00ca614249 -1840-1859 19e9bb194bab6da5 51eceaccdcc30f56 -1860-1879 957e33f771f9e3ff a05ac0862763538e -1880-1899 d87b280fd9a2982c 89cd98f44afc5e03 -1900-1919 318c8bfcb6dea857 830725405cb40902 -1920-1939 25f470ea3ae09040 3ad3700a98bcd972 -1940-1959 87bd939828a4bb17 a8e63b264294f723 -1960-1979 d8986cdd634f0838 c4def3ef9119911f -1980-1999 eb596ffc2b2cc984 0d7333bed730e179 -2000-2019 325f5116c5754a2d b9656e64977d389e -2020-2039 349e0ed883787441 aa7c1d12842a4c29 -2040-2059 dadd4a7567aa2a18 be860bb89c763bf0 -2060-2079 c0e53141ba27f125 4b723ca09cc6bcbf -2080-2099 63078052a0b9d316 d1ad50fdedefc211 -2100-2119 aac3f4e372a600d3 0d7de181c4db3af7 -2120-2139 4ddd3fe935626962 316e3f753defafd2 -2140-2159 cbb1adf75e0f8653 52a9230c43af1283 -2160-2179 a6ad34863cec0838 4d3de2f2cbcc8578 -2180-2199 d922850193265596 775ad6dbd09a4fcf -2200-2219 ab4d606834bf2fbf 5ccab1b1f986f286 -2220-2239 f01c3cbe182ea5e2 302f09ba630f61db -2240-2259 8c36183efdcf7430 e44f6c0a7a57b6b0 -2260-2279 8b2db3ee961a62bf 86ff90b276dfc3ee -2280-2299 0d81bf57b17a39eb b25daf2afb37300b -2300-2319 e5e930944795cc37 ba2ca3ade42b7338 -2320-2339 da479c323a75e58b 64dd928266b63e3c -2340-2359 a3696ebf2cfceeff c2471d5f8ab1a6dc -2360-2379 fd76d24f7a5bd6d4 96a1a26b35f7a54f -2380-2399 3649464b5f3252bc 3237b1b634a17cb3 -2400-2419 668ec474d9ac2f10 577be250a585ce0a -2420-2439 a7a961307a292779 c87ff2537feffdcb -2440-2459 6f8712b563d282ea be0cff28f497b70f -2460-2479 7b3164c923dbdb29 b7b2c522c09b12d2 -2480-2499 61b9bae3cbbd6142 163e5f1156cb1532 -2500-2519 f0d126572f954122 b014bbdc750615d2 -2520-2539 fd9e5e7b86fed4e5 763af30bb3d6efef -2540-2559 ee6195b09b6a225b c9ad3de0278e89ee -2560-2579 080aa26be640e3cb 3396a7a42ca98d09 -2580-2599 0c5a33a5fe1024be 0ce1df0b0ef96364 -2600-2619 6cc48c2f3d711fc6 0e77be6864e2b941 -2620-2639 c99ab863ceb2b70f 4b4eb04a9792fbb2 -2640-2659 259fbae892ffe01b d53466aeb2c4b0d4 -2660-2679 e9b861ecdaa9aaf5 0d459af4389f73d5 -2680-2699 85009c71a9067ae9 bf178f364d995c8f -2700-2719 77de60f9977e14d6 fed65df0975077a6 -2720-2739 2617ec02575b373a 0289f17f84bfc7df -2740-2759 bbe6acac5ba729f0 066f2ed415820d60 -2760-2779 4f89f3064ce83b6e d0fe4c54b6564dc9 -2780-2799 ce182a92235c7631 7aad80adb6b342fb -2800-2819 e509eb852f613541 bcc108625c0c58d9 -2820-2839 712799bcc42c6179 e6c631afd83c28a2 -2840-2859 c474f41591dd5d3e 3525396be5d8bd2e -2860-2879 b54bdba568e1e578 f5a71fb2931c35fa -2880-2899 93aad789093cd996 cd3c18c91adf2b6a -2900-2919 30ea69cb9a9d6f6a c01afc46727f70d5 -2920-2939 9a5db4b416448ad6 9b58845393bf1503 -2940-2959 a71f706ece681ae9 8e1816337c0cf4ff -2960-2979 ccfaef92b67c8b08 a29d68a6482420e1 +0-19 8affc108e0fea83c 092dfcac98e17988 +20-39 2e8996839f016121 8902142b9c0cb4ea +40-59 a86f60d4d868c79c 96b1243d2bc88be3 +60-79 802e356468c4982c 66a514779cd1889d +80-99 35a6b1febe605c4f 6786aa45f089843a +100-119 bd3577de6dc6538e 2356e1031ccde0d0 +120-139 4f16b94967700492 f740a4ed46b5ecc4 +140-159 e54c48f83f4f8b9f 5b3b62679e62fad7 +160-179 515b4fd117413b07 36a0264761ab0ca4 +180-199 5109488d3766b78d 9dffe0a52eef4233 +200-219 28f6319a64dc3fbb c4295f304acd9fd0 +220-239 39618d849d8fda8c 2b480f15ea6f3288 +240-259 bdae75a5c4e825a6 5e5e1bc7ec9ba66e +260-279 c745748faa22afc7 f41e772e7e00368d +280-299 1011d2e2a4d5b0ec 0dd08c4e9531625d +300-319 4b16f76f77f2864d 5da1e905d5398b67 +320-339 39bd1e211fc7dc2d 67c81fe1b0d44667 +340-359 3301fbd0c3711331 702c8b6cd982666d +360-379 df998500fae69f60 9de75bdf4286492a +380-399 723fe126896b13a6 ea83175776691a85 +400-419 8af5995cf2f76dea 38b2ce0983cb153b +420-439 0fe2bc1416e755d6 3b3805acbd2ee3a9 +440-459 3df2c0d14ad9d565 3bc9150926060145 +460-479 560a1006d9373f2b 0b6f59ea243c11bd +480-499 26d14b61cf702dab f9c76cdcc6b2a113 +500-519 3961ba4ed9781da7 1a81932ca027a6cf +520-539 1b38432bef93a466 2636ece160a37adb +540-559 267215b64ecec280 ede0eeddee390828 +560-579 21b9a5619b7413e4 dbda0d4acac4278a +580-599 b5545d490aac09a9 23e06e5a481388c4 +600-619 e8dd88bc440f73f8 eccc39ef3eaf6f42 +620-639 af3b19b435cf6fac b3ae41db7894e4ce +640-659 bae7e86659b156c3 469b01b8e29e7322 +660-679 d3f04db367baa7ca fa245cb001882c77 +680-699 963d9788fb95ba48 a2359f1ea009d636 +700-719 d9f7740b1b0aaa1d 44f223df91df58c5 +720-739 61b704425a73b56d 7e9b4810a1b738ea +740-759 a1539928074f96af 11fe0627578c5d20 +760-779 a71deef5b9b2c42e c7a6b857d0b0dbb6 +780-799 6dc37e510ac31cbf c526714d87c0a1ea +800-819 0116b8f40912ff10 75bddb967cc35886 +820-839 a894c27412910ab3 e7fcffc66ea52de8 +840-859 9e9eaed3d98358ee 008b6bdac3eb877c +860-879 92d169f487a02ea0 d3d61c35045ff9b4 +880-899 6cda3400ac7fe005 7c3cd88332a15e60 +900-919 1deaf15626d0a873 dea1750b2e7e8d57 +920-939 1de022c7c9306a5e f6352469d5d6fc35 +940-959 b1abc86bef6b3063 43bd42598b1f3369 +960-979 91a059a20d7961f4 f1b67bc624abb8ae +980-999 6642fbe828ba9cd2 d0a91c99b9bf0546 +1000-1019 0ff1157561f6e666 c16ce92021d598ef +1020-1039 6170e2fe8d9c912f 6ed85b928d59d3a3 +1040-1059 2c7a678f7bef19b5 92b2edf8b44c665c +1060-1079 d7df42b4f6319ea4 c9fb1931453ed76d +1080-1099 38a227b69e3f6ae7 67b812d28ed6ba62 +1100-1119 cec7880ed7eec8b2 5761ac6c3bd964a4 +1120-1139 fde72a21c0a18004 6cb374ca67e82a5e +1140-1159 83e9cba84dde6ad3 95ff4b583aca31da +1160-1179 7f90cc68e619694e 3e7763ecd7281026 +1180-1199 f06d71f7c517a0a2 28a5ecf8437175b0 +1200-1219 1da3d2d5deb1cf4d 1aa79ea633d7757e +1220-1239 0722c3f2eb664b0c 8c1ec4652337983a +1240-1259 1c0655e6360b5de5 6dff50c6e74e14c6 +1260-1279 a1579b39d5ac7bbe 1805cca3ea201640 +1280-1299 cd1ee243a50b3619 4f7dc54253034c35 +1300-1319 206339cf9ebd5ae0 14ceb2edcfa5b0eb +1320-1339 86094c70406465d1 618924cd290e49ff +1340-1359 173a4ba8543a2e36 6bdb166096b8cabf +1360-1379 76e4d6fbd8d437ef 971b984d3358adc2 +1380-1399 559ca65099b73a05 d50e0fd1cc0e88d9 +1400-1419 39dd13b5fbbbf0e6 f4be69578987a2ac +1420-1439 d00c1c4afb54a563 f3706e29249525ed +1440-1459 4785cfd44210b032 194563b6ca508709 +1460-1479 db68a6b46a1304ea ab3b75fce7ec72ca +1480-1499 32c66b76e3c3b526 01800bbe9380dbc6 +1500-1519 943c1df4bb8cb02a bb65c8ae5a10ea9d +1520-1539 a289fd547fd60656 deb66d54ca0199e2 +1540-1559 4874a2ac9f5077ce 11542d0e64ae794c +1560-1579 f0f5333624b0a0bc 74769ac2fff04c5b +1580-1599 ceb59f6639170e68 1becf822e32d70c4 +1600-1619 cb6f199400f49f4d c620a9a9fd767361 +1620-1639 071ea36a03e8574e 02022947783c8b11 +1640-1659 84bdbdd5d83a725a 3f6ee8ecab25b341 +1660-1679 f51bf7929ee1247d 7a8afeda5f113996 +1680-1699 4bb10b37555656a4 2bf1238eee77489d +1700-1719 f3adc36c8c101d61 1b653caf28ccbc15 +1720-1739 1b3bc33b47ee5644 0926193ac302e2fd +1740-1759 dc1f5483bef22de7 4102204b4d9428fc +1760-1779 51bcf5fe24b39c29 2656ce8616467139 +1780-1799 02fe708f48a2ddc2 a1149f37ee472b2a +1800-1819 a384e804075283ca 7ef0d048f91e4b7a +1820-1839 c0673d4164f0b499 eae596be6cdfeb33 +1840-1859 19e9bb194bab6da5 8141909471e0d585 +1860-1879 957e33f771f9e3ff f8b0ef99f5afa5ad +1880-1899 d87b280fd9a2982c 23b8267e013c080d +1900-1919 318c8bfcb6dea857 c9a9dffd5f39857b +1920-1939 25f470ea3ae09040 34112c942d74fba3 +1940-1959 87bd939828a4bb17 0a1b81ef05e50138 +1960-1979 d8986cdd634f0838 86f86ca37ddb9123 +1980-1999 eb596ffc2b2cc984 b6799f7ada3850a9 +2000-2019 325f5116c5754a2d d2d19905d406f554 +2020-2039 349e0ed883787441 e405d9fd2963a3e0 +2040-2059 dadd4a7567aa2a18 01d339231300c811 +2060-2079 c0e53141ba27f125 7f853322f64a8a30 +2080-2099 63078052a0b9d316 0c27fdc1961a61d8 +2100-2119 aac3f4e372a600d3 7c189230ebf89f96 +2120-2139 4ddd3fe935626962 6d46143f756b845c +2140-2159 cbb1adf75e0f8653 09bbad24c84e9218 +2160-2179 a6ad34863cec0838 048e5c47a8fd74b7 +2180-2199 d922850193265596 957b2ecc89969ea7 +2200-2219 ab4d606834bf2fbf 566852473a1c46ba +2220-2239 f01c3cbe182ea5e2 5352ea2d58d04892 +2240-2259 8c36183efdcf7430 9b3fc6ab99b8bbf6 +2260-2279 8b2db3ee961a62bf 47e39ef58564fed0 +2280-2299 0d81bf57b17a39eb c21a81a099a9ba9e +2300-2319 e5e930944795cc37 57830a8e3ecfcb22 +2320-2339 da479c323a75e58b f667462b0b5e979a +2340-2359 a3696ebf2cfceeff c1fc492cca84f605 +2360-2379 fd76d24f7a5bd6d4 f44dfe4a0ff994c1 +2380-2399 3649464b5f3252bc 0315dfbb6d522e22 +2400-2419 668ec474d9ac2f10 1704b1e0cbe726e9 +2420-2439 a7a961307a292779 30eb21f4162d46d2 +2440-2459 6f8712b563d282ea 29725f17fff0f524 +2460-2479 7b3164c923dbdb29 ca3b204ee4655985 +2480-2499 61b9bae3cbbd6142 ecd55ff71d834f73 +2500-2519 f0d126572f954122 326e6b723810c3bc +2520-2539 fd9e5e7b86fed4e5 a08ed1dd294d0d20 +2540-2559 ee6195b09b6a225b 7f7df9e8d25a9848 +2560-2579 080aa26be640e3cb b7edf906876333fc +2580-2599 0c5a33a5fe1024be e67f8635ae09806e +2600-2619 6cc48c2f3d711fc6 e1fc66eee8ea93a3 +2620-2639 c99ab863ceb2b70f 6c2d3cc054cbb6ec +2640-2659 259fbae892ffe01b 285bc03761d7f432 +2660-2679 e9b861ecdaa9aaf5 78ca278ca6239de2 +2680-2699 85009c71a9067ae9 d12fdb1bd70119a3 +2700-2719 77de60f9977e14d6 d6f8a8e01b0484ff +2720-2739 2617ec02575b373a e7236f35c7918294 +2740-2759 bbe6acac5ba729f0 34d1a4091a2470e6 +2760-2779 4f89f3064ce83b6e 92baf3ab29dd76e4 +2780-2799 ce182a92235c7631 fe4ff6b8e465203b +2800-2819 e509eb852f613541 98ac66ec6502ea9f +2820-2839 712799bcc42c6179 f95e02796c3b45e4 +2840-2859 c474f41591dd5d3e 941683e22d99e9dc +2860-2879 b54bdba568e1e578 6ef7364d473f1168 +2880-2899 93aad789093cd996 3d539c724f7ef5bb +2900-2919 30ea69cb9a9d6f6a 4c8bd7fed2f362c0 +2920-2939 9a5db4b416448ad6 3681e68e429d5c67 +2940-2959 a71f706ece681ae9 d995604d623ae2e1 +2960-2979 ccfaef92b67c8b08 5802ad380154401a 2980-2999 35102d0285ad9ac3 526ead31b21b42ec -3000-3019 9a0ab906304ae3e9 2aa21fc8c0d0ea26 -3020-3039 ac5801e4552dd8ce 774404578bd548ef -3040-3059 3fcebf290c7d6da0 fa0ee81d4be1f86a -3060-3079 6869220edc877255 3df8453d3348bf7f -3080-3099 47b7a9f3a80b3aaf 44bf883f47b64e58 -3100-3119 ab7a9ad89a568877 d4e0cd1bf4f395e5 -3120-3139 756f8e0584318bc5 fbb7d1db8205f710 -3140-3159 29080f093dfc4d87 3c9f063bc21b2447 -3160-3179 f0313ed29b76b2df 39f4de3896ff0436 -3180-3199 c05fc95c6136d794 18246c9ff1568c5e -3200-3219 982685991c110eb9 809e2d8b177f0931 -3220-3239 2bf501211a23d7d6 33049d1a2ef31b63 -3240-3259 b24df1f4abfe4f76 779a3db14c20de58 -3260-3279 914f9f9b3cebc5f2 3ff8137e0b865b9d -3280-3299 49f1db970edd8dd9 0fa13d7b4ef86b1e -3300-3319 81f3c893279db890 8cd83afe6c778aab -3320-3339 653c87469aaf67da c18c18ccfe2fffdb -3340-3359 e126c2aa29229924 312536f6ef2547f0 -3360-3379 caadf2de729c96fb e4970d2eafc0f697 -3380-3399 e0ac63aa0438a819 7641defec5b2cdc4 -3400-3419 148fa49915966b39 54362094e3e5dace -3420-3439 d21d7ab89c261e21 2284f112a98637f4 -3440-3459 8986785d84078afb aba11235b40f1a10 -3460-3479 a3871fff7ee3a317 00ca947b5e597565 -3480-3499 a0a6ff0d603577c7 c16e83413144597b -3500-3519 896f05102f69aca2 84e0dfe27c056050 -3520-3539 78e4432a544cdfc1 966bde68876e3cac -3540-3559 053bbabaad351348 021a0e9d814010a8 -3560-3579 f85bfacb7b79dd46 b5852fc26ca5f675 -3580-3599 bb0896a97cccc631 95284fec0e4a87dd -3600-3619 a934668f7f478d63 db7a39ebe741a172 -3620-3639 7f2d5173fd8aca03 cfc09ac69965a1bf -3640-3659 b2b08ad8d0acffbd 446d748f1a2b50fd -3660-3679 e496a59dc9e6cf78 f079d2ea733d7206 -3680-3699 906777b2aaf09979 a05f8ba5ef68d1bd -3700-3719 8021d268b1c46124 013cff2c7fa111cd -3720-3739 e1608a68de7441bf b1ab2a8ab9262785 -3740-3759 187e19f3133b00e3 4628abd02b6fc93b -3760-3779 a15955249e18baa9 a0b350f405857591 -3780-3799 bf6cd2e656df3e7d 32a6cf79d139a1d7 -3800-3819 9835bc7593be9cbf 1a6be5a47788ba10 -3820-3839 ca1fee48b41c2890 bf05d1c5efbc63c4 -3840-3859 c174b010a0404c5e f9798cbb99a7969b -3860-3879 119fa6b73a3338c1 0494084493c420eb -3880-3899 02be9399d5302237 88210e74292e8c5c -3900-3919 c98b3f853c873bc0 753eddbd7591851e -3920-3939 da476b6754ae3446 0baba0035bff015b +3000-3019 9a0ab906304ae3e9 7eb1a9ea97afeed1 +3020-3039 ac5801e4552dd8ce 6e749f11db334beb +3040-3059 3fcebf290c7d6da0 e3a018f0d165ef07 +3060-3079 6869220edc877255 62beb8906bb95c0f +3080-3099 47b7a9f3a80b3aaf 20c9252e25925e18 +3100-3119 ab7a9ad89a568877 0e033b9e2bbf6aba +3120-3139 756f8e0584318bc5 4bd210dc9c341319 +3140-3159 29080f093dfc4d87 17b19b9f43741d28 +3160-3179 f0313ed29b76b2df 4fbe4523f68225d2 +3180-3199 c05fc95c6136d794 5b5fe7aaecc25c80 +3200-3219 982685991c110eb9 365c1beef97fc444 +3220-3239 2bf501211a23d7d6 6e64d03c1bf15c2d +3240-3259 b24df1f4abfe4f76 cce913d31e160685 +3260-3279 914f9f9b3cebc5f2 bb1fa9fbb351f9b0 +3280-3299 49f1db970edd8dd9 e5c6ce9f229f91c3 +3300-3319 81f3c893279db890 b19a868b881409af +3320-3339 653c87469aaf67da f1438e86f39f54de +3340-3359 e126c2aa29229924 cfb2cf5fb324c23b +3360-3379 caadf2de729c96fb c2aaf4261beddc56 +3380-3399 e0ac63aa0438a819 46c9d4f1fb8e81a1 +3400-3419 148fa49915966b39 b3370d4f2e990338 +3420-3439 d21d7ab89c261e21 d0ecbd8dfeb91a5f +3440-3459 8986785d84078afb 810daa8e7edfed4a +3460-3479 a3871fff7ee3a317 0dd06d5e3dd959f1 +3480-3499 a0a6ff0d603577c7 bec8d6095f162fd3 +3500-3519 896f05102f69aca2 ec038d3e7d3d3e61 +3520-3539 78e4432a544cdfc1 0e8ac5564d2ac024 +3540-3559 053bbabaad351348 4237805f245ac6cb +3560-3579 f85bfacb7b79dd46 fa688032754d19a6 +3580-3599 bb0896a97cccc631 9d90e59fa2098dfa +3600-3619 a934668f7f478d63 5b1ebbe0584a59e9 +3620-3639 7f2d5173fd8aca03 ac98539765412e06 +3640-3659 b2b08ad8d0acffbd 2442d0ae0b446b81 +3660-3679 e496a59dc9e6cf78 f469e2e1bf13dbc0 +3680-3699 906777b2aaf09979 92f8ec6cf37910b8 +3700-3719 8021d268b1c46124 2591cfc4020d5e95 +3720-3739 e1608a68de7441bf 0f32bb533a85df67 +3740-3759 187e19f3133b00e3 16502bedd5efd26b +3760-3779 a15955249e18baa9 5d39caa71dfe30ca +3780-3799 bf6cd2e656df3e7d fb5e12bb34006984 +3800-3819 9835bc7593be9cbf 82ebf700c978bf37 +3820-3839 ca1fee48b41c2890 f1f335204296cdbf +3840-3859 c174b010a0404c5e 9a5ce03fc5d4760c +3860-3879 119fa6b73a3338c1 1bfbfb4d0a043114 +3880-3899 02be9399d5302237 d9cccf5db3fedfde +3900-3919 c98b3f853c873bc0 9b456e3889b25db0 +3920-3939 da476b6754ae3446 5d4c617fd558e09e 3940-3959 ab36db310aa8158a 14b3a77075691611 -3960-3979 027f1794db03104a 0a2904b00c8e71d7 -3980-3999 f759257256eff31f afc89cfd50056f55 -4000-4019 b0407445f0c0f422 4483fda0061979f1 -4020-4039 23d69df558a9bac7 e317451ab03d5445 -4040-4059 428232094dfea685 7ea5a009e0f80b14 -4060-4079 ee64aca81df7f525 96a4f5a0b89f52f2 -4080-4099 08e40210db249512 3b12f83e486fb3a6 -4100-4119 d5b69997ff78cc2f 20ec0d81524923bb -4120-4139 038b12e0eb745067 6284aa5fd4a513ab -4140-4159 6dc5e81b12b5d286 7c4d6f6b23e9b21a -4160-4179 d1107eab387957eb 22784fe4c02962bb -4180-4199 68b35dda89fa2148 d702c9161546b93a -4200-4219 93b4144d34e1e1bc 330f8334c8fbef75 -4220-4239 21e9315592104765 49a59fb15998115f -4240-4259 83a3c76cb0dadb60 1ad3546e533f786e -4260-4279 afc13a46a11f9c54 643ef6ee918603c0 -4280-4299 3325d6fe7efffd39 a64ab40fad5b2cda -4300-4319 2c6e7cdd6cb38dfe ef735982ac42fb18 -4320-4339 5edeea2de70f4957 6be04bc3474fb745 -4340-4359 ca6904e0bfa42a63 e9a74c67397620c3 -4360-4379 1735c316065edcca 89d50377253c244a -4380-4399 9fd6f6e6344df8c0 141a227e7d0fb70b -4400-4419 f277ade63fd98e74 d2e8495a5c283a94 -4420-4439 1a3c06c5b6dc9620 0bcf2139847b52dc -4440-4459 6f4f4501f5e08f53 ce747a5108d4db62 -4460-4479 bb2cc34d8b7a829f aa500f2fb0904dda -4480-4499 f915542aa3602eeb 329c453e98becea8 -4500-4519 bcf8dc911c6794ee 8b89bb6c07ffbeaf -4520-4539 37ea918f8a516f7a 0092f54987960586 -4540-4559 0f3192f4bc58acf9 228e0bb70e710e22 -4560-4579 a59c8c8cecd95029 1bdaa52fea5d55eb -4580-4599 32086aaa5312e148 fa1fcfb1824ec147 -4600-4619 958c9fb5ec4058c9 68b20c3d188197d9 -4620-4639 c185ecb19834f1c9 55ab2e8adb9d4c91 -4640-4659 6e6f7f2e8c0508cd b961530b8603b119 -4660-4679 ba49e7c4631a5da3 cc2168755927c8d9 -4680-4699 25e96ebd7af9ad7c 14779d7147afb9cb -4700-4719 48e53b8e1634bfdc 24f28e8fa462500d -4720-4739 2765c4157850ac0c cd9108cc1ecfd9e0 -4740-4759 960f3e6338bbcee7 ff971530ca995949 -4760-4779 899832b024b60495 c805ddf365908901 -4780-4799 2083775b22c08a78 4fdbfffb0bab0979 -4800-4819 047415e908159297 2d6a5fb4556c5160 -4820-4839 850982f4053e31c2 b7946ffbbb35cde6 -4840-4859 e4ba494b9dd5eab2 e8639539828ac47a -4860-4879 0cd98484940640c6 62bc2301ab8fd4e9 -4880-4899 907e91fa363dfff1 5e76475a237d1dd6 -4900-4919 e16d234fe23b3ec3 627e76ad230722a0 -4920-4939 00e999b1cd8b21af 8d53ef86faaa27c5 +3960-3979 027f1794db03104a 90ab011560f4f370 +3980-3999 f759257256eff31f 09cf343c03a81603 +4000-4019 b0407445f0c0f422 cbc89282db9e388b +4020-4039 23d69df558a9bac7 b2e5170b1fbafe31 +4040-4059 428232094dfea685 f657ce97eb3b4f12 +4060-4079 ee64aca81df7f525 a26db6b2f0d26260 +4080-4099 08e40210db249512 81aa31ef783ee6d7 +4100-4119 d5b69997ff78cc2f c23f891b80f5d49c +4120-4139 038b12e0eb745067 b90560991d98122e +4140-4159 6dc5e81b12b5d286 2d5bf22ebb9c692b +4160-4179 d1107eab387957eb 7444d5ba30a158b1 +4180-4199 68b35dda89fa2148 26fbce455f9e5d61 +4200-4219 93b4144d34e1e1bc fe80c90312aa7707 +4220-4239 21e9315592104765 739ec4ecd192eb41 +4240-4259 83a3c76cb0dadb60 2b3aea5cc751ab3e +4260-4279 afc13a46a11f9c54 93af00524ac551fb +4280-4299 3325d6fe7efffd39 ff09ed58cf39405a +4300-4319 2c6e7cdd6cb38dfe 30be0f61fadcdc69 +4320-4339 5edeea2de70f4957 c2ca7af16a2a1b08 +4340-4359 ca6904e0bfa42a63 b0f1a9d357f9a2b4 +4360-4379 1735c316065edcca f509c14da16c5c1a +4380-4399 9fd6f6e6344df8c0 ed191725a55b25d1 +4400-4419 f277ade63fd98e74 b7961817d63f0330 +4420-4439 1a3c06c5b6dc9620 25413fca8ac2da80 +4440-4459 6f4f4501f5e08f53 7d9ecf9ae893009b +4460-4479 bb2cc34d8b7a829f dffdd56525cca681 +4480-4499 f915542aa3602eeb a7511a77bfb9716b +4500-4519 bcf8dc911c6794ee c27a61aa38ef1fcf +4520-4539 37ea918f8a516f7a d74a1b2fb3efa16a +4540-4559 0f3192f4bc58acf9 210a216d18d735ca +4560-4579 a59c8c8cecd95029 5989d29b837a9c9a +4580-4599 32086aaa5312e148 3540de1bed30af90 +4600-4619 958c9fb5ec4058c9 ade05e6f1b4be0bb +4620-4639 c185ecb19834f1c9 2a5c2ab90a6842a2 +4640-4659 6e6f7f2e8c0508cd 55f218ce20d3de2e +4660-4679 ba49e7c4631a5da3 2e7fd91cdb1f9d39 +4680-4699 25e96ebd7af9ad7c 763a3d8a98c67b79 +4700-4719 48e53b8e1634bfdc b4314ee0caefeaea +4720-4739 2765c4157850ac0c 3f3094216e71d478 +4740-4759 960f3e6338bbcee7 21e65fc90837ec66 +4760-4779 899832b024b60495 a58a4bff7b927c7a +4780-4799 2083775b22c08a78 01721898507c4196 +4800-4819 047415e908159297 1f406d41dfa8a214 +4820-4839 850982f4053e31c2 e50f38f8a522f8e7 +4840-4859 e4ba494b9dd5eab2 df470d1febfa52cb +4860-4879 0cd98484940640c6 a332b7fcba3af947 +4880-4899 907e91fa363dfff1 e0f58c2814c4ffcb +4900-4919 e16d234fe23b3ec3 7768f4378ba8e59e +4920-4939 00e999b1cd8b21af aef8e7e80d563f2e 4940-4959 eee3ad4e714b56db 5e49ce5b9597ffde -4960-4979 cb7f3086ee803ad3 b9fcaa61095deeba -4980-4999 169b233699a55212 b186f6f23bea6352 -5000-5019 033535d87f667548 4b5e0ec932c883fc -5020-5039 493b1fd8900b5c00 34090b39bf7105b9 -5040-5059 4db1e6ec7510f8e0 59da836fad647d06 -5060-5079 3a700fa57d9051a0 284c2fcdbcd5565b -5080-5099 6d2ed04b5107d197 4004ef67267d0a70 -5100-5119 b6f38fb1a4a4b251 1a94535975b4461d -5120-5139 e16f3933ba5b17a7 16d7328c82a5d880 -5140-5159 9e889880394621ec c35ba8c5768421c8 -5160-5179 3304e6c6e83b77e4 cc886ccf687803a7 -5180-5199 ed29793cd6d2cfea 6f7ce175cc8cb8d4 -5200-5219 d9c93081624bf463 d48214b6ddff9501 -5220-5239 82e20114df8bb4da ba9d6980a3fc8028 -5240-5259 3ad400fbcfb0c73c 7d0d16b5b5f81bd7 -5260-5279 b0124204abda917d ef3f5a3e8ae67684 -5280-5299 56fe5b24df61f9ea faf339c85b1937a7 -5300-5319 7fddda4649deea19 1278ddca31068fe0 -5320-5339 d000c13e57c12c61 5b05198571d574f9 -5340-5359 4e213dfece0d3540 c85136fdb7f1035e -5360-5379 ce888aa8130cc5c8 e45e9ef7a53b847f -5380-5399 e900edcb9c11f809 8f76b63ca894e456 -5400-5419 e59ee86f05b7b954 ac63b11fbb40df80 -5420-5439 d42a808c053e5235 737af0c863a9b829 -5440-5459 7de5e8f7c42f423d 68c9246c16a043cd -5460-5479 7207311b1a516a80 2f2c29a0f060c17c -5480-5499 f4c4a38bce615fad 0197c65d7d5e9507 -5500-5519 fdef567a2a681265 1abc999bfb51f8d5 -5520-5539 2d71cd1f4b5e1a7e ea2c62f2c1d38c0e -5540-5559 dd85b7984bcdfa3c 5e34915e14dedba2 -5560-5579 02508d460551610b b31c0e14459a2a5b -5580-5599 748a06d020cc8af0 f588783925ca4d00 -5600-5619 3b28bdc009f4c967 96dd530592c20611 -5620-5639 916cd9558dc06074 ca6bd2a7c3e459d4 -5640-5659 24e14cdedd9bfd1f ff79a78f0c2c055e -5660-5679 2b50d91e0f2e895d 65872fcdf4cbbd47 -5680-5699 3fc3b812649bd56e f489ab55e8959013 -5700-5719 062ac098b8450ac3 044b6b33dddcc9cc -5720-5739 b1d9966911b13003 0c980114b18aa1c6 -5740-5759 f57706c708958d95 1bd8934657451ed5 -5760-5779 9f1d45f92b056a83 48f2d3f6a6c32d48 -5780-5799 395460c3d6bf2509 9146caa048dcb410 -5800-5819 07cf8ea8bb4e35b3 8ecf43ba7be610d3 -5820-5839 a1621eab8920e2f8 a103adbcee879fd0 -5840-5859 269e9700efe874a5 1318eead1a80a43e -5860-5879 85b9ed84639cb2fe 89e3dbbbf5576f69 -5880-5899 ea2abbe034b2a0a4 40937ad52fea4569 -5900-5919 27bd9015ce0266c0 bba95be7aebb7f90 -5920-5939 eea6098e14c30e8d 846645842badded4 -5940-5959 5343ec7763ca4275 4c416edc2525cbee -5960-5979 c5fe4f4fa5da17fb 45a46bcfbfad869d -5980-5999 8af6e2a947835e9b 8ca9f6ee7704cc5d +4960-4979 cb7f3086ee803ad3 b5333249f513f6ee +4980-4999 169b233699a55212 7c9e40b246910b87 +5000-5019 033535d87f667548 a1d00e3ce59d9612 +5020-5039 493b1fd8900b5c00 f5dcc96921a5fce7 +5040-5059 4db1e6ec7510f8e0 a1b94c07f36c4fd9 +5060-5079 3a700fa57d9051a0 cd5ea389290e9da7 +5080-5099 6d2ed04b5107d197 d86f5d76a0e6ec83 +5100-5119 b6f38fb1a4a4b251 7ce713d215664691 +5120-5139 e16f3933ba5b17a7 81e3cd8cd6935f29 +5140-5159 9e889880394621ec d3742b3f48f5be6a +5160-5179 3304e6c6e83b77e4 2959d47d253ec370 +5180-5199 ed29793cd6d2cfea 1fd8e9eb61fc2349 +5200-5219 d9c93081624bf463 2506f6ed18a43b2a +5220-5239 82e20114df8bb4da 4cefc9ee2ad83079 +5240-5259 3ad400fbcfb0c73c d8fbe28887e841fc +5260-5279 b0124204abda917d 5cd9ef113d49ca21 +5280-5299 56fe5b24df61f9ea f17a3576b845e498 +5300-5319 7fddda4649deea19 65e046922f0a186e +5320-5339 d000c13e57c12c61 149974cc9b39dc88 +5340-5359 4e213dfece0d3540 f64b655ea3207a9d +5360-5379 ce888aa8130cc5c8 6e97d17a5cfc0590 +5380-5399 e900edcb9c11f809 84bc7361866aa699 +5400-5419 e59ee86f05b7b954 d9cbb238b7ba25fc +5420-5439 d42a808c053e5235 ecd0c36dd65330f9 +5440-5459 7de5e8f7c42f423d 7181b849201cf33c +5460-5479 7207311b1a516a80 4f631ae2e3b6d70a +5480-5499 f4c4a38bce615fad 1e26786dae7bc4cc +5500-5519 fdef567a2a681265 47f52c4859300252 +5520-5539 2d71cd1f4b5e1a7e 64f5cb7430e62715 +5540-5559 dd85b7984bcdfa3c 961b364bfcdb0995 +5560-5579 02508d460551610b e09cb096fd4cd973 +5580-5599 748a06d020cc8af0 093d7dbbbcc57a05 +5600-5619 3b28bdc009f4c967 e2ee64e8aa9859ec +5620-5639 916cd9558dc06074 6a86c81062076892 +5640-5659 24e14cdedd9bfd1f c4e2aa96610a1ae6 +5660-5679 2b50d91e0f2e895d 2cf7ecf13c7ab6af +5680-5699 3fc3b812649bd56e 2d3a9fa39059910e +5700-5719 062ac098b8450ac3 4547597f6b2b3ae2 +5720-5739 b1d9966911b13003 75a33bfa45c7848d +5740-5759 f57706c708958d95 ec6b36a9d6a68015 +5760-5779 9f1d45f92b056a83 756ff1f4c0128fb8 +5780-5799 395460c3d6bf2509 7bb1ba842e474faf +5800-5819 07cf8ea8bb4e35b3 747c4bec40c60d4e +5820-5839 a1621eab8920e2f8 046e414cfc31fc74 +5840-5859 269e9700efe874a5 2d912543194018b9 +5860-5879 85b9ed84639cb2fe 9aeb3720f5cbe7f1 +5880-5899 ea2abbe034b2a0a4 fe6391cd37095230 +5900-5919 27bd9015ce0266c0 781908b645b290d9 +5920-5939 eea6098e14c30e8d 29eb70132bd2b7ed +5940-5959 5343ec7763ca4275 9e8afaf24f63bd30 +5960-5979 c5fe4f4fa5da17fb 75b0c5b301e8ce24 +5980-5999 8af6e2a947835e9b 4676b33d57ff51fc From 4a759f2477011c71d8b2a056f8ded335e8059e24 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 23:14:06 +0000 Subject: [PATCH 4/6] Address review: lower zip lines, vendor gate A go 1.16 go.sum zip line below the patched version now shows that lower version is built, so apply and apply --check catch it again. Vendored mode gets the same build-list gate before it fetches or wires a module. The hosted not-in-graph warning no longer claims go.mod lacks a require it may have, and the zip-line rule lives in one place. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/patch/redirect/golang_local.rs | 6 +- .../src/patch/redirect/mod.rs | 5 +- .../src/vendor/go_build_list.rs | 13 +- .../src/vendor/go_sum_edit.rs | 13 +- crates/socket-patch-core/src/vendor/golang.rs | 8 + .../tests/equivalence/golang_rewrite.golden | 594 +++++++++--------- 6 files changed, 335 insertions(+), 304 deletions(-) diff --git a/crates/socket-patch-core/src/patch/redirect/golang_local.rs b/crates/socket-patch-core/src/patch/redirect/golang_local.rs index 59e86583c..ec2f342ee 100644 --- a/crates/socket-patch-core/src/patch/redirect/golang_local.rs +++ b/crates/socket-patch-core/src/patch/redirect/golang_local.rs @@ -600,7 +600,11 @@ pub async fn verify_go_redirect_state( /// Why apply must not write a `replace` for `module@version`, when go.mod /// and go.sum prove the build list does not select that version. -async fn selection_refusal(project_root: &Path, module: &str, version: &str) -> Option { +pub(crate) async fn selection_refusal( + project_root: &Path, + module: &str, + version: &str, +) -> Option { match go_build_list::project_selection(project_root, module, version).await? { Selection::Other(selected) => Some(format!( "the build selects {module} {selected}, not the patched {version}: a replace \ diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index e4c2765a5..90b97ae6f 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -8546,8 +8546,9 @@ fn rewrite_golang( result.warnings.push(RewriteWarning { code: "redirect_golang_not_in_module_graph".into(), detail: format!( - "{fname}@{}: not required by go.mod and go.sum has no zip line for it — \ - the module is not in this project's build graph; nothing redirected", + "{fname}@{}: neither go.mod's require nor a go.sum zip line shows the \ + build selecting this version — it is not in this project's build graph; \ + nothing redirected", dep.version ), }); diff --git a/crates/socket-patch-core/src/vendor/go_build_list.rs b/crates/socket-patch-core/src/vendor/go_build_list.rs index 0b25220b8..45a8f881e 100644 --- a/crates/socket-patch-core/src/vendor/go_build_list.rs +++ b/crates/socket-patch-core/src/vendor/go_build_list.rs @@ -64,8 +64,7 @@ pub fn selection(go_mod: &str, go_sum: Option<&str>, module: &str, version: &str let zips: Vec<&str> = go_sum .map(|text| { go_sum_lines(text) - .filter(|l| !l.go_mod && l.module == module) - .map(|l| l.version) + .filter_map(|l| l.zip_version_of(module)) .collect() }) .unwrap_or_default(); @@ -98,6 +97,10 @@ pub fn select(pruned: bool, required: Option<&str>, zips: &[&str], version: &str Some(h) if go_semver_cmp(h, version) == Ordering::Greater => { Selection::Other(h.to_string()) } + // Everything is below the patched version. A zip line there shows + // that lower version is the one built (go records no zip line for + // the patched version only once a directory replace covers it). + Some(h) if zips.contains(&h) => Selection::Other(h.to_string()), _ => Selection::Unknown, } } @@ -184,6 +187,12 @@ mod tests { assert_eq!(selection(&m, Some(""), UP, "v1.0.0"), Selection::Selected); // Only a lower bound below the patched version: can't tell. assert_eq!(selection(&m, None, UP, "v1.2.0"), Selection::Unknown); + // A zip line below it shows the lower version is built. + let s = sum(&[&format!("{UP} v1.0.0")]); + assert_eq!( + selection(&m, Some(&s), UP, "v1.2.0"), + Selection::Other("v1.0.0".into()) + ); } #[test] diff --git a/crates/socket-patch-core/src/vendor/go_sum_edit.rs b/crates/socket-patch-core/src/vendor/go_sum_edit.rs index 4cbb022c6..6c5fcceb4 100644 --- a/crates/socket-patch-core/src/vendor/go_sum_edit.rs +++ b/crates/socket-patch-core/src/vendor/go_sum_edit.rs @@ -296,8 +296,7 @@ impl GoSumEditor { pub(crate) fn zip_versions(&self, module: &str) -> Vec { self.current_lines() .flat_map(go_sum_lines) - .filter(|l| !l.go_mod && l.module == module) - .map(|l| l.version.to_string()) + .filter_map(|l| l.zip_version_of(module).map(str::to_string)) .collect() } @@ -375,6 +374,16 @@ pub(crate) struct GoSumLine<'a> { pub(crate) extra_tokens: bool, } +impl<'a> GoSumLine<'a> { + /// The version, when this is a zip (`h1:` of the module tree) line for + /// `module`: the line go records for a version whose packages the build + /// loads, unlike the `/go.mod` line it writes for every version MVS + /// reads. + pub(crate) fn zip_version_of(&self, module: &str) -> Option<&'a str> { + (!self.go_mod && self.module == module).then_some(self.version) + } +} + /// Every line of `text` with at least three tokens, in file order; shorter /// lines are skipped. Each caller keeps its own token rule /// ([`GoSumLine::extra_tokens`]) and hash filter. diff --git a/crates/socket-patch-core/src/vendor/golang.rs b/crates/socket-patch-core/src/vendor/golang.rs index 7507fd7b7..0db0e2878 100644 --- a/crates/socket-patch-core/src/vendor/golang.rs +++ b/crates/socket-patch-core/src/vendor/golang.rs @@ -242,6 +242,14 @@ pub async fn vendor_go_module<'a>( { return refused("vendor_go_workspace_conflict", refusal); } + // A replace for a version the build list doesn't select is ignored + // by go (#392): refuse before fetching or wiring anything. + if let Some(refusal) = + crate::patch::redirect::golang_local::selection_refusal(project_root, module, version) + .await + { + return refused("vendor_go_version_not_selected", refusal); + } } let mut warnings: Vec = Vec::new(); diff --git a/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden b/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden index 13672fd32..ba561f902 100644 --- a/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden +++ b/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden @@ -1,302 +1,302 @@ # One seeded go.mod / go.sum pair + overrides, or the re-run over its output. # -0-19 8affc108e0fea83c 092dfcac98e17988 -20-39 2e8996839f016121 8902142b9c0cb4ea -40-59 a86f60d4d868c79c 96b1243d2bc88be3 -60-79 802e356468c4982c 66a514779cd1889d -80-99 35a6b1febe605c4f 6786aa45f089843a -100-119 bd3577de6dc6538e 2356e1031ccde0d0 -120-139 4f16b94967700492 f740a4ed46b5ecc4 -140-159 e54c48f83f4f8b9f 5b3b62679e62fad7 -160-179 515b4fd117413b07 36a0264761ab0ca4 -180-199 5109488d3766b78d 9dffe0a52eef4233 -200-219 28f6319a64dc3fbb c4295f304acd9fd0 -220-239 39618d849d8fda8c 2b480f15ea6f3288 -240-259 bdae75a5c4e825a6 5e5e1bc7ec9ba66e -260-279 c745748faa22afc7 f41e772e7e00368d -280-299 1011d2e2a4d5b0ec 0dd08c4e9531625d -300-319 4b16f76f77f2864d 5da1e905d5398b67 -320-339 39bd1e211fc7dc2d 67c81fe1b0d44667 -340-359 3301fbd0c3711331 702c8b6cd982666d -360-379 df998500fae69f60 9de75bdf4286492a -380-399 723fe126896b13a6 ea83175776691a85 -400-419 8af5995cf2f76dea 38b2ce0983cb153b -420-439 0fe2bc1416e755d6 3b3805acbd2ee3a9 -440-459 3df2c0d14ad9d565 3bc9150926060145 -460-479 560a1006d9373f2b 0b6f59ea243c11bd -480-499 26d14b61cf702dab f9c76cdcc6b2a113 -500-519 3961ba4ed9781da7 1a81932ca027a6cf -520-539 1b38432bef93a466 2636ece160a37adb -540-559 267215b64ecec280 ede0eeddee390828 -560-579 21b9a5619b7413e4 dbda0d4acac4278a -580-599 b5545d490aac09a9 23e06e5a481388c4 -600-619 e8dd88bc440f73f8 eccc39ef3eaf6f42 -620-639 af3b19b435cf6fac b3ae41db7894e4ce -640-659 bae7e86659b156c3 469b01b8e29e7322 -660-679 d3f04db367baa7ca fa245cb001882c77 -680-699 963d9788fb95ba48 a2359f1ea009d636 -700-719 d9f7740b1b0aaa1d 44f223df91df58c5 -720-739 61b704425a73b56d 7e9b4810a1b738ea -740-759 a1539928074f96af 11fe0627578c5d20 -760-779 a71deef5b9b2c42e c7a6b857d0b0dbb6 -780-799 6dc37e510ac31cbf c526714d87c0a1ea -800-819 0116b8f40912ff10 75bddb967cc35886 -820-839 a894c27412910ab3 e7fcffc66ea52de8 -840-859 9e9eaed3d98358ee 008b6bdac3eb877c -860-879 92d169f487a02ea0 d3d61c35045ff9b4 -880-899 6cda3400ac7fe005 7c3cd88332a15e60 -900-919 1deaf15626d0a873 dea1750b2e7e8d57 -920-939 1de022c7c9306a5e f6352469d5d6fc35 -940-959 b1abc86bef6b3063 43bd42598b1f3369 -960-979 91a059a20d7961f4 f1b67bc624abb8ae -980-999 6642fbe828ba9cd2 d0a91c99b9bf0546 -1000-1019 0ff1157561f6e666 c16ce92021d598ef -1020-1039 6170e2fe8d9c912f 6ed85b928d59d3a3 -1040-1059 2c7a678f7bef19b5 92b2edf8b44c665c -1060-1079 d7df42b4f6319ea4 c9fb1931453ed76d -1080-1099 38a227b69e3f6ae7 67b812d28ed6ba62 -1100-1119 cec7880ed7eec8b2 5761ac6c3bd964a4 -1120-1139 fde72a21c0a18004 6cb374ca67e82a5e -1140-1159 83e9cba84dde6ad3 95ff4b583aca31da -1160-1179 7f90cc68e619694e 3e7763ecd7281026 -1180-1199 f06d71f7c517a0a2 28a5ecf8437175b0 -1200-1219 1da3d2d5deb1cf4d 1aa79ea633d7757e -1220-1239 0722c3f2eb664b0c 8c1ec4652337983a -1240-1259 1c0655e6360b5de5 6dff50c6e74e14c6 -1260-1279 a1579b39d5ac7bbe 1805cca3ea201640 -1280-1299 cd1ee243a50b3619 4f7dc54253034c35 -1300-1319 206339cf9ebd5ae0 14ceb2edcfa5b0eb -1320-1339 86094c70406465d1 618924cd290e49ff -1340-1359 173a4ba8543a2e36 6bdb166096b8cabf -1360-1379 76e4d6fbd8d437ef 971b984d3358adc2 -1380-1399 559ca65099b73a05 d50e0fd1cc0e88d9 -1400-1419 39dd13b5fbbbf0e6 f4be69578987a2ac -1420-1439 d00c1c4afb54a563 f3706e29249525ed -1440-1459 4785cfd44210b032 194563b6ca508709 -1460-1479 db68a6b46a1304ea ab3b75fce7ec72ca -1480-1499 32c66b76e3c3b526 01800bbe9380dbc6 -1500-1519 943c1df4bb8cb02a bb65c8ae5a10ea9d -1520-1539 a289fd547fd60656 deb66d54ca0199e2 -1540-1559 4874a2ac9f5077ce 11542d0e64ae794c -1560-1579 f0f5333624b0a0bc 74769ac2fff04c5b -1580-1599 ceb59f6639170e68 1becf822e32d70c4 -1600-1619 cb6f199400f49f4d c620a9a9fd767361 -1620-1639 071ea36a03e8574e 02022947783c8b11 -1640-1659 84bdbdd5d83a725a 3f6ee8ecab25b341 -1660-1679 f51bf7929ee1247d 7a8afeda5f113996 -1680-1699 4bb10b37555656a4 2bf1238eee77489d -1700-1719 f3adc36c8c101d61 1b653caf28ccbc15 -1720-1739 1b3bc33b47ee5644 0926193ac302e2fd -1740-1759 dc1f5483bef22de7 4102204b4d9428fc -1760-1779 51bcf5fe24b39c29 2656ce8616467139 -1780-1799 02fe708f48a2ddc2 a1149f37ee472b2a -1800-1819 a384e804075283ca 7ef0d048f91e4b7a -1820-1839 c0673d4164f0b499 eae596be6cdfeb33 -1840-1859 19e9bb194bab6da5 8141909471e0d585 -1860-1879 957e33f771f9e3ff f8b0ef99f5afa5ad -1880-1899 d87b280fd9a2982c 23b8267e013c080d -1900-1919 318c8bfcb6dea857 c9a9dffd5f39857b -1920-1939 25f470ea3ae09040 34112c942d74fba3 -1940-1959 87bd939828a4bb17 0a1b81ef05e50138 -1960-1979 d8986cdd634f0838 86f86ca37ddb9123 -1980-1999 eb596ffc2b2cc984 b6799f7ada3850a9 -2000-2019 325f5116c5754a2d d2d19905d406f554 -2020-2039 349e0ed883787441 e405d9fd2963a3e0 -2040-2059 dadd4a7567aa2a18 01d339231300c811 -2060-2079 c0e53141ba27f125 7f853322f64a8a30 -2080-2099 63078052a0b9d316 0c27fdc1961a61d8 -2100-2119 aac3f4e372a600d3 7c189230ebf89f96 -2120-2139 4ddd3fe935626962 6d46143f756b845c -2140-2159 cbb1adf75e0f8653 09bbad24c84e9218 -2160-2179 a6ad34863cec0838 048e5c47a8fd74b7 -2180-2199 d922850193265596 957b2ecc89969ea7 -2200-2219 ab4d606834bf2fbf 566852473a1c46ba -2220-2239 f01c3cbe182ea5e2 5352ea2d58d04892 -2240-2259 8c36183efdcf7430 9b3fc6ab99b8bbf6 -2260-2279 8b2db3ee961a62bf 47e39ef58564fed0 -2280-2299 0d81bf57b17a39eb c21a81a099a9ba9e -2300-2319 e5e930944795cc37 57830a8e3ecfcb22 -2320-2339 da479c323a75e58b f667462b0b5e979a -2340-2359 a3696ebf2cfceeff c1fc492cca84f605 -2360-2379 fd76d24f7a5bd6d4 f44dfe4a0ff994c1 -2380-2399 3649464b5f3252bc 0315dfbb6d522e22 -2400-2419 668ec474d9ac2f10 1704b1e0cbe726e9 -2420-2439 a7a961307a292779 30eb21f4162d46d2 -2440-2459 6f8712b563d282ea 29725f17fff0f524 -2460-2479 7b3164c923dbdb29 ca3b204ee4655985 -2480-2499 61b9bae3cbbd6142 ecd55ff71d834f73 -2500-2519 f0d126572f954122 326e6b723810c3bc -2520-2539 fd9e5e7b86fed4e5 a08ed1dd294d0d20 -2540-2559 ee6195b09b6a225b 7f7df9e8d25a9848 -2560-2579 080aa26be640e3cb b7edf906876333fc -2580-2599 0c5a33a5fe1024be e67f8635ae09806e -2600-2619 6cc48c2f3d711fc6 e1fc66eee8ea93a3 -2620-2639 c99ab863ceb2b70f 6c2d3cc054cbb6ec -2640-2659 259fbae892ffe01b 285bc03761d7f432 -2660-2679 e9b861ecdaa9aaf5 78ca278ca6239de2 -2680-2699 85009c71a9067ae9 d12fdb1bd70119a3 -2700-2719 77de60f9977e14d6 d6f8a8e01b0484ff -2720-2739 2617ec02575b373a e7236f35c7918294 -2740-2759 bbe6acac5ba729f0 34d1a4091a2470e6 -2760-2779 4f89f3064ce83b6e 92baf3ab29dd76e4 -2780-2799 ce182a92235c7631 fe4ff6b8e465203b -2800-2819 e509eb852f613541 98ac66ec6502ea9f -2820-2839 712799bcc42c6179 f95e02796c3b45e4 -2840-2859 c474f41591dd5d3e 941683e22d99e9dc -2860-2879 b54bdba568e1e578 6ef7364d473f1168 -2880-2899 93aad789093cd996 3d539c724f7ef5bb -2900-2919 30ea69cb9a9d6f6a 4c8bd7fed2f362c0 -2920-2939 9a5db4b416448ad6 3681e68e429d5c67 -2940-2959 a71f706ece681ae9 d995604d623ae2e1 -2960-2979 ccfaef92b67c8b08 5802ad380154401a +0-19 8affc108e0fea83c 5d1500b5b4977ade +20-39 2e8996839f016121 c80ab901c7d5b941 +40-59 a86f60d4d868c79c 2e909e42af42722b +60-79 802e356468c4982c b245ef007ac0b199 +80-99 35a6b1febe605c4f 26acb1aeae48527e +100-119 bd3577de6dc6538e d15fd20decc538fb +120-139 4f16b94967700492 40e0fd55af6490f0 +140-159 e54c48f83f4f8b9f d871d7c8f33b72f0 +160-179 515b4fd117413b07 707af78283831bfe +180-199 5109488d3766b78d 72d95ca487598d96 +200-219 28f6319a64dc3fbb 6552b5ff60b78d97 +220-239 39618d849d8fda8c 9c2e15b3f3049b99 +240-259 bdae75a5c4e825a6 41c99dad7f2e3602 +260-279 c745748faa22afc7 76165fd567760411 +280-299 1011d2e2a4d5b0ec 275c3f2d119bc0f6 +300-319 4b16f76f77f2864d a7190ce45da8958a +320-339 39bd1e211fc7dc2d f4e561020df30e61 +340-359 3301fbd0c3711331 369bde31013dca2a +360-379 df998500fae69f60 4b1e82141d27189b +380-399 723fe126896b13a6 60206a56295ee542 +400-419 8af5995cf2f76dea 972ee531f7ddc456 +420-439 0fe2bc1416e755d6 8e4faa46d7325ae2 +440-459 3df2c0d14ad9d565 8446bffed5561778 +460-479 560a1006d9373f2b c5e02b7d16a597a0 +480-499 26d14b61cf702dab 49106da4a1cdbc14 +500-519 3961ba4ed9781da7 ee933152d800e737 +520-539 1b38432bef93a466 299781a948107003 +540-559 267215b64ecec280 ce1d1a302ddd7b06 +560-579 21b9a5619b7413e4 592e031820a92b43 +580-599 b5545d490aac09a9 a4b02077cd035dca +600-619 e8dd88bc440f73f8 8ed4b6b4e2705663 +620-639 af3b19b435cf6fac 80e45fef2ba9b7ee +640-659 bae7e86659b156c3 265afadf377a60d5 +660-679 d3f04db367baa7ca 5950a39899436cdb +680-699 963d9788fb95ba48 3383458ee06c46ee +700-719 d9f7740b1b0aaa1d 057ff27adc6597af +720-739 61b704425a73b56d 1adda77f86ae5aac +740-759 a1539928074f96af 2de512cbe5d91507 +760-779 a71deef5b9b2c42e 2f6a2475ea5b0e7e +780-799 6dc37e510ac31cbf 264a55bc20f3a5ba +800-819 0116b8f40912ff10 520fcd83b84e076a +820-839 a894c27412910ab3 5ae5ca5d7565d1ad +840-859 9e9eaed3d98358ee 9e25271e20274ae6 +860-879 92d169f487a02ea0 579ad35d2e5f5140 +880-899 6cda3400ac7fe005 dc477036e0314f55 +900-919 1deaf15626d0a873 edaa35cce5de94a1 +920-939 1de022c7c9306a5e 419ed39e277b4c68 +940-959 b1abc86bef6b3063 f9be5f3be10ff19b +960-979 91a059a20d7961f4 bd71450798f80a7f +980-999 6642fbe828ba9cd2 1bdc9c24bf30fc00 +1000-1019 0ff1157561f6e666 921d23d15c5a1561 +1020-1039 6170e2fe8d9c912f 75a335142f43ac6c +1040-1059 2c7a678f7bef19b5 e1b08e754a9131ca +1060-1079 d7df42b4f6319ea4 af19bc9047bbec4d +1080-1099 38a227b69e3f6ae7 4b6ebf00be884797 +1100-1119 cec7880ed7eec8b2 6b16dcd4dc45d0e8 +1120-1139 fde72a21c0a18004 86549fdd4ac01168 +1140-1159 83e9cba84dde6ad3 108c2ecc04a0b9c6 +1160-1179 7f90cc68e619694e d82f6cc1d451d04e +1180-1199 f06d71f7c517a0a2 27acfc18a192ebad +1200-1219 1da3d2d5deb1cf4d 162ef86770340975 +1220-1239 0722c3f2eb664b0c 029990abac2ce1a2 +1240-1259 1c0655e6360b5de5 2960dcc658ea15cd +1260-1279 a1579b39d5ac7bbe ec24c00a782c8392 +1280-1299 cd1ee243a50b3619 50a5c592fca564fa +1300-1319 206339cf9ebd5ae0 9da1f118e6281ad3 +1320-1339 86094c70406465d1 843228f91976f783 +1340-1359 173a4ba8543a2e36 ce1d121649b8de95 +1360-1379 76e4d6fbd8d437ef 7c1524bb5898cf98 +1380-1399 559ca65099b73a05 d8d5b091dbe09161 +1400-1419 39dd13b5fbbbf0e6 fa2e5a1cb0d90b3d +1420-1439 d00c1c4afb54a563 8675a5d1b1d4b29f +1440-1459 4785cfd44210b032 51fd00e57d6c28f2 +1460-1479 db68a6b46a1304ea 7d65a1803ce740bc +1480-1499 32c66b76e3c3b526 71bb147db3c84baf +1500-1519 943c1df4bb8cb02a 6d8630ab68222c60 +1520-1539 a289fd547fd60656 0f478b01daada006 +1540-1559 4874a2ac9f5077ce 16d7107248fe975d +1560-1579 f0f5333624b0a0bc 47695aa42e4b17da +1580-1599 ceb59f6639170e68 4a62b8608e7b78c9 +1600-1619 cb6f199400f49f4d b5b2328f35c8282e +1620-1639 071ea36a03e8574e a717104555cfa813 +1640-1659 84bdbdd5d83a725a 7dd553b1add1e56b +1660-1679 f51bf7929ee1247d c9561ef9e2907285 +1680-1699 4bb10b37555656a4 1036165fdca98e1c +1700-1719 f3adc36c8c101d61 a021be2f591dbfe0 +1720-1739 1b3bc33b47ee5644 7cac9d4936cf164d +1740-1759 dc1f5483bef22de7 3983d04d45f5fbe9 +1760-1779 51bcf5fe24b39c29 bc0a93fb8397b578 +1780-1799 02fe708f48a2ddc2 4ad4fb36903b43fd +1800-1819 a384e804075283ca db598643cfa97580 +1820-1839 c0673d4164f0b499 884b3dab8addd003 +1840-1859 19e9bb194bab6da5 55f0cda04c87985d +1860-1879 957e33f771f9e3ff 005a850d0d0bb1b1 +1880-1899 d87b280fd9a2982c 6451ac8f70c04fb2 +1900-1919 318c8bfcb6dea857 7d42198f42c032c3 +1920-1939 25f470ea3ae09040 ae453d09052765a2 +1940-1959 87bd939828a4bb17 f2a313dea0b18229 +1960-1979 d8986cdd634f0838 a4e5e1f14ad17d7c +1980-1999 eb596ffc2b2cc984 ddc6a7cdcee976e7 +2000-2019 325f5116c5754a2d d8283a917a56d4c5 +2020-2039 349e0ed883787441 91c76a17d9fc079d +2040-2059 dadd4a7567aa2a18 3681265c16cdb43d +2060-2079 c0e53141ba27f125 ff698929528104a3 +2080-2099 63078052a0b9d316 0ecd0173e5be615f +2100-2119 aac3f4e372a600d3 6d9cbd5a433a64af +2120-2139 4ddd3fe935626962 759d533141801279 +2140-2159 cbb1adf75e0f8653 2c40d27dbc4c0e66 +2160-2179 a6ad34863cec0838 e3dab784978a5529 +2180-2199 d922850193265596 a7388bfb07346ab2 +2200-2219 ab4d606834bf2fbf d63fa48fc1c5dc49 +2220-2239 f01c3cbe182ea5e2 4af5a8e74b9d3bf6 +2240-2259 8c36183efdcf7430 4407e89bffadbe28 +2260-2279 8b2db3ee961a62bf db0e45610ca582d4 +2280-2299 0d81bf57b17a39eb 26e3b4c9df9656d1 +2300-2319 e5e930944795cc37 7cbc659af7036cab +2320-2339 da479c323a75e58b 136f25eeb69a9240 +2340-2359 a3696ebf2cfceeff c06c42eeae4f591a +2360-2379 fd76d24f7a5bd6d4 924dd0f6a51443bd +2380-2399 3649464b5f3252bc d5b56f4ba544147e +2400-2419 668ec474d9ac2f10 50c80c0123cc916b +2420-2439 a7a961307a292779 64af9fc1d75610da +2440-2459 6f8712b563d282ea a41e602ed9b20e4a +2460-2479 7b3164c923dbdb29 e744808301c7b46e +2480-2499 61b9bae3cbbd6142 6ae89ae5b6837a1c +2500-2519 f0d126572f954122 0f22804cf0f3f7fa +2520-2539 fd9e5e7b86fed4e5 dae36c9ce8735cd8 +2540-2559 ee6195b09b6a225b ca2727ea2c4d7bb8 +2560-2579 080aa26be640e3cb b43561f6226a9e5c +2580-2599 0c5a33a5fe1024be b5c4146fabc91aae +2600-2619 6cc48c2f3d711fc6 5aa6ccb1128ada34 +2620-2639 c99ab863ceb2b70f 3a88b168816147be +2640-2659 259fbae892ffe01b d149856ad38631bf +2660-2679 e9b861ecdaa9aaf5 72ff227f3e7004f6 +2680-2699 85009c71a9067ae9 4aa604f6370d651e +2700-2719 77de60f9977e14d6 cbb312c36e0a0a2b +2720-2739 2617ec02575b373a 9fa4a9c03b32b3f4 +2740-2759 bbe6acac5ba729f0 c7c505a0333eff76 +2760-2779 4f89f3064ce83b6e 6c8fbf5f121fb2ce +2780-2799 ce182a92235c7631 e008c2cbb273ef1e +2800-2819 e509eb852f613541 cb11990f4d3f1371 +2820-2839 712799bcc42c6179 98458058a8aebe5e +2840-2859 c474f41591dd5d3e 57c02bdcc1773fda +2860-2879 b54bdba568e1e578 d03d379f20c82719 +2880-2899 93aad789093cd996 932f0f69c07a500f +2900-2919 30ea69cb9a9d6f6a da1f38cc6b66d3b1 +2920-2939 9a5db4b416448ad6 e5d8fd89af61327c +2940-2959 a71f706ece681ae9 456c7fff17bdcd7d +2960-2979 ccfaef92b67c8b08 d6a2ad74dc4d0565 2980-2999 35102d0285ad9ac3 526ead31b21b42ec -3000-3019 9a0ab906304ae3e9 7eb1a9ea97afeed1 -3020-3039 ac5801e4552dd8ce 6e749f11db334beb -3040-3059 3fcebf290c7d6da0 e3a018f0d165ef07 -3060-3079 6869220edc877255 62beb8906bb95c0f -3080-3099 47b7a9f3a80b3aaf 20c9252e25925e18 -3100-3119 ab7a9ad89a568877 0e033b9e2bbf6aba -3120-3139 756f8e0584318bc5 4bd210dc9c341319 -3140-3159 29080f093dfc4d87 17b19b9f43741d28 -3160-3179 f0313ed29b76b2df 4fbe4523f68225d2 -3180-3199 c05fc95c6136d794 5b5fe7aaecc25c80 -3200-3219 982685991c110eb9 365c1beef97fc444 -3220-3239 2bf501211a23d7d6 6e64d03c1bf15c2d -3240-3259 b24df1f4abfe4f76 cce913d31e160685 -3260-3279 914f9f9b3cebc5f2 bb1fa9fbb351f9b0 -3280-3299 49f1db970edd8dd9 e5c6ce9f229f91c3 -3300-3319 81f3c893279db890 b19a868b881409af -3320-3339 653c87469aaf67da f1438e86f39f54de -3340-3359 e126c2aa29229924 cfb2cf5fb324c23b -3360-3379 caadf2de729c96fb c2aaf4261beddc56 -3380-3399 e0ac63aa0438a819 46c9d4f1fb8e81a1 -3400-3419 148fa49915966b39 b3370d4f2e990338 -3420-3439 d21d7ab89c261e21 d0ecbd8dfeb91a5f -3440-3459 8986785d84078afb 810daa8e7edfed4a -3460-3479 a3871fff7ee3a317 0dd06d5e3dd959f1 -3480-3499 a0a6ff0d603577c7 bec8d6095f162fd3 -3500-3519 896f05102f69aca2 ec038d3e7d3d3e61 -3520-3539 78e4432a544cdfc1 0e8ac5564d2ac024 -3540-3559 053bbabaad351348 4237805f245ac6cb -3560-3579 f85bfacb7b79dd46 fa688032754d19a6 -3580-3599 bb0896a97cccc631 9d90e59fa2098dfa -3600-3619 a934668f7f478d63 5b1ebbe0584a59e9 -3620-3639 7f2d5173fd8aca03 ac98539765412e06 -3640-3659 b2b08ad8d0acffbd 2442d0ae0b446b81 -3660-3679 e496a59dc9e6cf78 f469e2e1bf13dbc0 -3680-3699 906777b2aaf09979 92f8ec6cf37910b8 -3700-3719 8021d268b1c46124 2591cfc4020d5e95 -3720-3739 e1608a68de7441bf 0f32bb533a85df67 -3740-3759 187e19f3133b00e3 16502bedd5efd26b -3760-3779 a15955249e18baa9 5d39caa71dfe30ca -3780-3799 bf6cd2e656df3e7d fb5e12bb34006984 -3800-3819 9835bc7593be9cbf 82ebf700c978bf37 -3820-3839 ca1fee48b41c2890 f1f335204296cdbf -3840-3859 c174b010a0404c5e 9a5ce03fc5d4760c -3860-3879 119fa6b73a3338c1 1bfbfb4d0a043114 -3880-3899 02be9399d5302237 d9cccf5db3fedfde -3900-3919 c98b3f853c873bc0 9b456e3889b25db0 -3920-3939 da476b6754ae3446 5d4c617fd558e09e +3000-3019 9a0ab906304ae3e9 d0832a2303175697 +3020-3039 ac5801e4552dd8ce 918ec9cd8afe3558 +3040-3059 3fcebf290c7d6da0 5b703a7c1edbd04b +3060-3079 6869220edc877255 3c9188868d643184 +3080-3099 47b7a9f3a80b3aaf ef3831cee924838e +3100-3119 ab7a9ad89a568877 8ee8000f0500d2fe +3120-3139 756f8e0584318bc5 e9c3f6043f955b00 +3140-3159 29080f093dfc4d87 29f49d954d19ee49 +3160-3179 f0313ed29b76b2df 72dcc94b61217766 +3180-3199 c05fc95c6136d794 96cc0ee877382501 +3200-3219 982685991c110eb9 9e577277423c94bb +3220-3239 2bf501211a23d7d6 3fcb55f49ecb2670 +3240-3259 b24df1f4abfe4f76 ba9586fa228afb39 +3260-3279 914f9f9b3cebc5f2 9894a06923e7349c +3280-3299 49f1db970edd8dd9 932a2c552baf6a3a +3300-3319 81f3c893279db890 c78b52cc1f2bdcc8 +3320-3339 653c87469aaf67da d30143d13c9bcd4f +3340-3359 e126c2aa29229924 301ec6b923aa8659 +3360-3379 caadf2de729c96fb 615d36fd4bb92904 +3380-3399 e0ac63aa0438a819 4219b1b28efeda29 +3400-3419 148fa49915966b39 2280ab296a6fb0b0 +3420-3439 d21d7ab89c261e21 33aec05674a3558f +3440-3459 8986785d84078afb 8dff898d4d330d5d +3460-3479 a3871fff7ee3a317 226d40ab28cf895a +3480-3499 a0a6ff0d603577c7 3e641f5be543dfcc +3500-3519 896f05102f69aca2 5ef8c3b617b6e9c3 +3520-3539 78e4432a544cdfc1 5e43b05c21f3d27a +3540-3559 053bbabaad351348 b26f7cadbcfbf7ef +3560-3579 f85bfacb7b79dd46 5af753e26d418c35 +3580-3599 bb0896a97cccc631 b00c1dcf1e401473 +3600-3619 a934668f7f478d63 179c7a315dbcb60b +3620-3639 7f2d5173fd8aca03 285b0b1d2b8f2be4 +3640-3659 b2b08ad8d0acffbd c10c3da3cc5384f6 +3660-3679 e496a59dc9e6cf78 17e1fbdc9fc417ff +3680-3699 906777b2aaf09979 1a99da3aeb3c767c +3700-3719 8021d268b1c46124 f9553e2d057ff730 +3720-3739 e1608a68de7441bf 391776527c7c1536 +3740-3759 187e19f3133b00e3 c41c7f070ab0c1f2 +3760-3779 a15955249e18baa9 eeccedbada811e66 +3780-3799 bf6cd2e656df3e7d aece8f98d7c8c05b +3800-3819 9835bc7593be9cbf 39073f5f9b251f1e +3820-3839 ca1fee48b41c2890 1466cfd01b3efbe7 +3840-3859 c174b010a0404c5e df7217dd548d52a4 +3860-3879 119fa6b73a3338c1 5ba3c998848ba836 +3880-3899 02be9399d5302237 8bdd8129827ea879 +3900-3919 c98b3f853c873bc0 2573240ba09cdf35 +3920-3939 da476b6754ae3446 c9a9da7d60feacf5 3940-3959 ab36db310aa8158a 14b3a77075691611 -3960-3979 027f1794db03104a 90ab011560f4f370 -3980-3999 f759257256eff31f 09cf343c03a81603 -4000-4019 b0407445f0c0f422 cbc89282db9e388b -4020-4039 23d69df558a9bac7 b2e5170b1fbafe31 -4040-4059 428232094dfea685 f657ce97eb3b4f12 -4060-4079 ee64aca81df7f525 a26db6b2f0d26260 -4080-4099 08e40210db249512 81aa31ef783ee6d7 -4100-4119 d5b69997ff78cc2f c23f891b80f5d49c -4120-4139 038b12e0eb745067 b90560991d98122e -4140-4159 6dc5e81b12b5d286 2d5bf22ebb9c692b -4160-4179 d1107eab387957eb 7444d5ba30a158b1 -4180-4199 68b35dda89fa2148 26fbce455f9e5d61 -4200-4219 93b4144d34e1e1bc fe80c90312aa7707 -4220-4239 21e9315592104765 739ec4ecd192eb41 -4240-4259 83a3c76cb0dadb60 2b3aea5cc751ab3e -4260-4279 afc13a46a11f9c54 93af00524ac551fb -4280-4299 3325d6fe7efffd39 ff09ed58cf39405a -4300-4319 2c6e7cdd6cb38dfe 30be0f61fadcdc69 -4320-4339 5edeea2de70f4957 c2ca7af16a2a1b08 -4340-4359 ca6904e0bfa42a63 b0f1a9d357f9a2b4 -4360-4379 1735c316065edcca f509c14da16c5c1a -4380-4399 9fd6f6e6344df8c0 ed191725a55b25d1 -4400-4419 f277ade63fd98e74 b7961817d63f0330 -4420-4439 1a3c06c5b6dc9620 25413fca8ac2da80 -4440-4459 6f4f4501f5e08f53 7d9ecf9ae893009b -4460-4479 bb2cc34d8b7a829f dffdd56525cca681 -4480-4499 f915542aa3602eeb a7511a77bfb9716b -4500-4519 bcf8dc911c6794ee c27a61aa38ef1fcf -4520-4539 37ea918f8a516f7a d74a1b2fb3efa16a -4540-4559 0f3192f4bc58acf9 210a216d18d735ca -4560-4579 a59c8c8cecd95029 5989d29b837a9c9a -4580-4599 32086aaa5312e148 3540de1bed30af90 -4600-4619 958c9fb5ec4058c9 ade05e6f1b4be0bb -4620-4639 c185ecb19834f1c9 2a5c2ab90a6842a2 -4640-4659 6e6f7f2e8c0508cd 55f218ce20d3de2e -4660-4679 ba49e7c4631a5da3 2e7fd91cdb1f9d39 -4680-4699 25e96ebd7af9ad7c 763a3d8a98c67b79 -4700-4719 48e53b8e1634bfdc b4314ee0caefeaea -4720-4739 2765c4157850ac0c 3f3094216e71d478 -4740-4759 960f3e6338bbcee7 21e65fc90837ec66 -4760-4779 899832b024b60495 a58a4bff7b927c7a -4780-4799 2083775b22c08a78 01721898507c4196 -4800-4819 047415e908159297 1f406d41dfa8a214 -4820-4839 850982f4053e31c2 e50f38f8a522f8e7 -4840-4859 e4ba494b9dd5eab2 df470d1febfa52cb -4860-4879 0cd98484940640c6 a332b7fcba3af947 -4880-4899 907e91fa363dfff1 e0f58c2814c4ffcb -4900-4919 e16d234fe23b3ec3 7768f4378ba8e59e -4920-4939 00e999b1cd8b21af aef8e7e80d563f2e +3960-3979 027f1794db03104a 87d70cdb56bedc81 +3980-3999 f759257256eff31f 713aeabd22c0e1c2 +4000-4019 b0407445f0c0f422 cc2dcdade89b2e91 +4020-4039 23d69df558a9bac7 1814496534142b3a +4040-4059 428232094dfea685 8b2d17845464fc09 +4060-4079 ee64aca81df7f525 b8e6127540ac70d9 +4080-4099 08e40210db249512 225453b20f6fa200 +4100-4119 d5b69997ff78cc2f 0dd3ec1813885749 +4120-4139 038b12e0eb745067 3ff6966261635b0c +4140-4159 6dc5e81b12b5d286 37b642ebebc51ed0 +4160-4179 d1107eab387957eb 1bfb62918ed43452 +4180-4199 68b35dda89fa2148 64ad486bb64a4dc0 +4200-4219 93b4144d34e1e1bc 4de503fad8f4db45 +4220-4239 21e9315592104765 3d9a0a34a7c53ff8 +4240-4259 83a3c76cb0dadb60 776fb04478f73076 +4260-4279 afc13a46a11f9c54 6ee899041a418417 +4280-4299 3325d6fe7efffd39 edbf356b7edcfafc +4300-4319 2c6e7cdd6cb38dfe 68d48951ecc8ac97 +4320-4339 5edeea2de70f4957 8e99585e52203bb1 +4340-4359 ca6904e0bfa42a63 424b6c0a85c7c176 +4360-4379 1735c316065edcca 7286f919341a6ecc +4380-4399 9fd6f6e6344df8c0 7b40e965e52d3791 +4400-4419 f277ade63fd98e74 13b8cbf07dd1ffa6 +4420-4439 1a3c06c5b6dc9620 44c82e441c46c70b +4440-4459 6f4f4501f5e08f53 c0d5c69dcb5d1c57 +4460-4479 bb2cc34d8b7a829f 056102ad10b46e8c +4480-4499 f915542aa3602eeb 47605be4a01fd433 +4500-4519 bcf8dc911c6794ee 23c910fa4caa2e08 +4520-4539 37ea918f8a516f7a 3ca299ee0b698550 +4540-4559 0f3192f4bc58acf9 42af273935e5fa30 +4560-4579 a59c8c8cecd95029 631348dfb8c9912b +4580-4599 32086aaa5312e148 58f124f28cedac21 +4600-4619 958c9fb5ec4058c9 c1352804e4bce1cb +4620-4639 c185ecb19834f1c9 41db6c0cd2ff36aa +4640-4659 6e6f7f2e8c0508cd c274c48ec807bc66 +4660-4679 ba49e7c4631a5da3 768d13e3bb7cda87 +4680-4699 25e96ebd7af9ad7c 0cd398186c21f873 +4700-4719 48e53b8e1634bfdc cf9f93b1b70ddbf3 +4720-4739 2765c4157850ac0c a401710f5ae7503a +4740-4759 960f3e6338bbcee7 8202cbbeda281630 +4760-4779 899832b024b60495 56221c0d9375df23 +4780-4799 2083775b22c08a78 9e862d9ea16d7ccd +4800-4819 047415e908159297 7fa016bf24060b46 +4820-4839 850982f4053e31c2 2460694d0f947cff +4840-4859 e4ba494b9dd5eab2 375ef26b357398f8 +4860-4879 0cd98484940640c6 72486d77fd626acb +4880-4899 907e91fa363dfff1 9a60a277da4e1edd +4900-4919 e16d234fe23b3ec3 fc9e0db3d04be9c2 +4920-4939 00e999b1cd8b21af 63b445b3e2d9b738 4940-4959 eee3ad4e714b56db 5e49ce5b9597ffde -4960-4979 cb7f3086ee803ad3 b5333249f513f6ee -4980-4999 169b233699a55212 7c9e40b246910b87 -5000-5019 033535d87f667548 a1d00e3ce59d9612 -5020-5039 493b1fd8900b5c00 f5dcc96921a5fce7 -5040-5059 4db1e6ec7510f8e0 a1b94c07f36c4fd9 -5060-5079 3a700fa57d9051a0 cd5ea389290e9da7 -5080-5099 6d2ed04b5107d197 d86f5d76a0e6ec83 -5100-5119 b6f38fb1a4a4b251 7ce713d215664691 -5120-5139 e16f3933ba5b17a7 81e3cd8cd6935f29 -5140-5159 9e889880394621ec d3742b3f48f5be6a -5160-5179 3304e6c6e83b77e4 2959d47d253ec370 -5180-5199 ed29793cd6d2cfea 1fd8e9eb61fc2349 -5200-5219 d9c93081624bf463 2506f6ed18a43b2a -5220-5239 82e20114df8bb4da 4cefc9ee2ad83079 -5240-5259 3ad400fbcfb0c73c d8fbe28887e841fc -5260-5279 b0124204abda917d 5cd9ef113d49ca21 -5280-5299 56fe5b24df61f9ea f17a3576b845e498 -5300-5319 7fddda4649deea19 65e046922f0a186e -5320-5339 d000c13e57c12c61 149974cc9b39dc88 -5340-5359 4e213dfece0d3540 f64b655ea3207a9d -5360-5379 ce888aa8130cc5c8 6e97d17a5cfc0590 -5380-5399 e900edcb9c11f809 84bc7361866aa699 -5400-5419 e59ee86f05b7b954 d9cbb238b7ba25fc -5420-5439 d42a808c053e5235 ecd0c36dd65330f9 -5440-5459 7de5e8f7c42f423d 7181b849201cf33c -5460-5479 7207311b1a516a80 4f631ae2e3b6d70a -5480-5499 f4c4a38bce615fad 1e26786dae7bc4cc -5500-5519 fdef567a2a681265 47f52c4859300252 -5520-5539 2d71cd1f4b5e1a7e 64f5cb7430e62715 -5540-5559 dd85b7984bcdfa3c 961b364bfcdb0995 -5560-5579 02508d460551610b e09cb096fd4cd973 -5580-5599 748a06d020cc8af0 093d7dbbbcc57a05 -5600-5619 3b28bdc009f4c967 e2ee64e8aa9859ec -5620-5639 916cd9558dc06074 6a86c81062076892 -5640-5659 24e14cdedd9bfd1f c4e2aa96610a1ae6 -5660-5679 2b50d91e0f2e895d 2cf7ecf13c7ab6af -5680-5699 3fc3b812649bd56e 2d3a9fa39059910e -5700-5719 062ac098b8450ac3 4547597f6b2b3ae2 -5720-5739 b1d9966911b13003 75a33bfa45c7848d -5740-5759 f57706c708958d95 ec6b36a9d6a68015 -5760-5779 9f1d45f92b056a83 756ff1f4c0128fb8 -5780-5799 395460c3d6bf2509 7bb1ba842e474faf -5800-5819 07cf8ea8bb4e35b3 747c4bec40c60d4e -5820-5839 a1621eab8920e2f8 046e414cfc31fc74 -5840-5859 269e9700efe874a5 2d912543194018b9 -5860-5879 85b9ed84639cb2fe 9aeb3720f5cbe7f1 -5880-5899 ea2abbe034b2a0a4 fe6391cd37095230 -5900-5919 27bd9015ce0266c0 781908b645b290d9 -5920-5939 eea6098e14c30e8d 29eb70132bd2b7ed -5940-5959 5343ec7763ca4275 9e8afaf24f63bd30 -5960-5979 c5fe4f4fa5da17fb 75b0c5b301e8ce24 -5980-5999 8af6e2a947835e9b 4676b33d57ff51fc +4960-4979 cb7f3086ee803ad3 95b0fcbf9f41919c +4980-4999 169b233699a55212 663bbb78fe171cf6 +5000-5019 033535d87f667548 ae5fab2530ddb4aa +5020-5039 493b1fd8900b5c00 934ce1fb6e7b61e4 +5040-5059 4db1e6ec7510f8e0 2e363ae68f5f9454 +5060-5079 3a700fa57d9051a0 a69c75e13fe4a27e +5080-5099 6d2ed04b5107d197 352c62f20b1728f3 +5100-5119 b6f38fb1a4a4b251 7c431defd6abb9e6 +5120-5139 e16f3933ba5b17a7 9a42858604acf11b +5140-5159 9e889880394621ec fc8b8d5bb02df5ad +5160-5179 3304e6c6e83b77e4 5113ebbbd5c669db +5180-5199 ed29793cd6d2cfea 1f91ac12e38f9461 +5200-5219 d9c93081624bf463 b10b97e902436d81 +5220-5239 82e20114df8bb4da db7f4299331ed77c +5240-5259 3ad400fbcfb0c73c 11a5c4b628d97897 +5260-5279 b0124204abda917d f782f32df479e2b7 +5280-5299 56fe5b24df61f9ea 9fbe7f7220402d70 +5300-5319 7fddda4649deea19 b136054ff5c47cbe +5320-5339 d000c13e57c12c61 af2df603f89252b0 +5340-5359 4e213dfece0d3540 87bb18313d5161bc +5360-5379 ce888aa8130cc5c8 b843f5886c1c44e7 +5380-5399 e900edcb9c11f809 93378aa4828189f7 +5400-5419 e59ee86f05b7b954 3dbec30dc72607b9 +5420-5439 d42a808c053e5235 451a65df4c392f43 +5440-5459 7de5e8f7c42f423d 5810c3cb2806af61 +5460-5479 7207311b1a516a80 b5e5d9fb5d1a5c28 +5480-5499 f4c4a38bce615fad 42e1e9f93f265fad +5500-5519 fdef567a2a681265 1b13444c679bce71 +5520-5539 2d71cd1f4b5e1a7e ec7498226cff09e5 +5540-5559 dd85b7984bcdfa3c 310446d9f3c4b196 +5560-5579 02508d460551610b 8965604b74866255 +5580-5599 748a06d020cc8af0 0ce014681c229d06 +5600-5619 3b28bdc009f4c967 e6ae4a8e04e1a00b +5620-5639 916cd9558dc06074 5921ec1bb4f402d0 +5640-5659 24e14cdedd9bfd1f dcba53a83ce6f693 +5660-5679 2b50d91e0f2e895d ba677f2989462c80 +5680-5699 3fc3b812649bd56e 7dc4080d3788a96f +5700-5719 062ac098b8450ac3 e03d64b605240bb9 +5720-5739 b1d9966911b13003 6382aea2d07bc3f1 +5740-5759 f57706c708958d95 86511de3bcc89f07 +5760-5779 9f1d45f92b056a83 295d9dde2fc29a37 +5780-5799 395460c3d6bf2509 d1c7e012e8a82fa3 +5800-5819 07cf8ea8bb4e35b3 8939c1d9f3659111 +5820-5839 a1621eab8920e2f8 8e3ee3624ee1c2c8 +5840-5859 269e9700efe874a5 e516ac2a5203cc08 +5860-5879 85b9ed84639cb2fe 300fc71e737c86a3 +5880-5899 ea2abbe034b2a0a4 31b1e3d45ed580a6 +5900-5919 27bd9015ce0266c0 e889ed128ebb892c +5920-5939 eea6098e14c30e8d fb7cf9db3352c3ab +5940-5959 5343ec7763ca4275 f27d90a5f62fd433 +5960-5979 c5fe4f4fa5da17fb 6b65baf729c07908 +5980-5999 8af6e2a947835e9b 5b84ce92fad17828 From 3add9b2c8e2dfa82f842f143d6d74de01093fcc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 23:24:29 +0000 Subject: [PATCH 5/6] Mirror the vendor gate in the service preflight The download plan has to name exactly the modules vendor asks the service for, so it now skips a version the build doesn't select, as vendor itself does. Adds a regression test for vendoring an unselected version in a go 1.16 graph. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-core/src/vendor/golang.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/socket-patch-core/src/vendor/golang.rs b/crates/socket-patch-core/src/vendor/golang.rs index 0db0e2878..cbe6cafb0 100644 --- a/crates/socket-patch-core/src/vendor/golang.rs +++ b/crates/socket-patch-core/src/vendor/golang.rs @@ -181,6 +181,18 @@ pub(crate) async fn service_preflight( .await .ok() .filter(|p| !p.copy_was_ok)?; + // `vendor_go_module` refuses a version the build doesn't select before + // it asks the service for anything (#392). + if crate::patch::redirect::golang_local::selection_refusal( + project_root, + &prelude.module, + &prelude.version, + ) + .await + .is_some() + { + return None; + } // `go_service_redirect` extracts the module zip (its literal // `{module}@{version}/` prefix stripped) into the copy dir's stage. let prefix = format!("{}@{}/", prelude.module, prelude.version); @@ -817,6 +829,11 @@ mod tests { }; let (dir, blobs, pristine, record) = fixture().await; let root = dir.path(); + // A go 1.16 go.mod listing no version of the module: neither + // version is shown unselected, so both ask for a grant. + tokio::fs::write(root.join("go.mod"), "module example.com/app\n\ngo 1.16\n") + .await + .unwrap(); let server = wiremock::MockServer::start().await; mount_no_results(&server).await; let cfg = service_cfg(&server.uri(), crate::vendor::VendorSource::Service, false); @@ -911,6 +928,37 @@ mod tests { } } + /// #392 (vendored variant): in the go 1.16 graph where go.sum shows the + /// build selects v1.5.0, vendoring a v1.4.2 patch would commit a replace + /// go ignores. Refuse before fetching or writing anything. + #[tokio::test] + async fn test_vendor_refuses_a_version_the_build_does_not_select() { + let (dir, blobs, pristine, record) = fixture().await; + let root = dir.path(); + let go_mod = "module example.com/app\n\ngo 1.16\n\nrequire example.com/mid v1.0.0\n"; + tokio::fs::write(root.join("go.mod"), go_mod).await.unwrap(); + tokio::fs::write( + root.join("go.sum"), + "github.com/foo/bar v1.4.2/go.mod h1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n\ + github.com/foo/bar v1.5.0 h1:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\n", + ) + .await + .unwrap(); + let outcome = run_vendor(PURL, root, &blobs, &pristine, &record, false).await; + let detail = expect_refused(outcome, "vendor_go_version_not_selected"); + assert!( + detail.contains("the build selects github.com/foo/bar v1.5.0"), + "{detail}" + ); + assert_eq!( + tokio::fs::read_to_string(root.join("go.mod")) + .await + .unwrap(), + go_mod + ); + assert!(!root.join(".socket/vendor").exists()); + } + #[tokio::test] async fn test_happy_path_wires_copy_replace_and_marker() { let (dir, blobs, pristine, record) = fixture().await; From 598055a3d91f4705c5be49353c73ca9b101eb26a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 23:34:21 +0000 Subject: [PATCH 6/6] Expect apply to refuse a bumped Go version The vendor suite pinned apply exiting 0 when go.mod requires a newer version than the patch, though the replace it wrote was ignored. It now expects the refusal that names the selected version (#392); apply --check still reports the drift. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/vendor/e2e_golang_redirect.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/socket-patch-cli/tests/vendor/e2e_golang_redirect.rs b/crates/socket-patch-cli/tests/vendor/e2e_golang_redirect.rs index 3bcfee7d5..12c8ada3e 100644 --- a/crates/socket-patch-cli/tests/vendor/e2e_golang_redirect.rs +++ b/crates/socket-patch-cli/tests/vendor/e2e_golang_redirect.rs @@ -253,13 +253,18 @@ fn check_detects_resolved_version_mismatch() { ); // apply must NOT silently paper over it: a version bump means the manifest - // is stale (it patches v1.4.2, the build wants v1.5.0). apply re-affirms the - // v1.4.2 redirect but cannot make the build use it, so check STAYS red until - // a human re-scans. (Fail-closed stays closed — never a false "in sync".) + // is stale (it patches v1.4.2, the build wants v1.5.0). A v1.4.2 redirect + // can't make the build use it, so apply refuses and names the selected + // version (#392) instead of reporting the patch applied, and check STAYS + // red until a human re-scans. (Fail-closed stays closed.) + let (code, so, se) = apply(root, &gomodcache); assert_eq!( - apply(root, &gomodcache).0, - 0, - "apply itself succeeds (re-affirms v1.4.2)" + code, 1, + "apply must refuse an unselected version.\n{so}\n{se}" + ); + assert!( + format!("{so}{se}").contains("v1.5.0"), + "the refusal names the version the build selects.\n{so}\n{se}" ); assert_eq!( check(root, &gomodcache),