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-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), 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..ec2f342ee 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,28 @@ 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. +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 \ + 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 +1738,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; @@ -1940,6 +2087,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/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index dcdcbe249..90b97ae6f 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,81 @@ 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}@{}: 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 ), }); @@ -21480,6 +21498,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..45a8f881e --- /dev/null +++ b/crates/socket-patch-core/src/vendor/go_build_list.rs @@ -0,0 +1,237 @@ +//! 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_map(|l| l.zip_version_of(module)) + .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()) + } + // 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, + } +} + +/// [`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); + // 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] + 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..6c5fcceb4 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,17 @@ 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_map(|l| l.zip_version_of(module).map(str::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 @@ -363,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..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); @@ -242,6 +254,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(); @@ -809,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); @@ -903,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; 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/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden b/crates/socket-patch-core/tests/equivalence/golang_rewrite.golden index 538d71421..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 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 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 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 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 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 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 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 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 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