diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md
index d52add4f8..6064214b1 100644
--- a/crates/socket-patch-cli/CLI_CONTRACT.md
+++ b/crates/socket-patch-cli/CLI_CONTRACT.md
@@ -218,7 +218,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc
**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 in `redirect.skipped[]` as `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.
-The rewriter reads a fixed set of candidate files from the project root: the npm-family locks (`package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, `shrinkwrap.yaml`, `yarn.lock`, plus `.yarnrc.yml` for the berry cache-config gate, `bun.lock` / `bun.lockb`, and `vlt-lock.json` with `vlt.json` and `node_modules/.vlt-lock.json` read only), `requirements.txt` / `uv.lock` / `Pipfile.lock` (pipfile-spec 6; see the Pipenv section below) / `poetry.lock` (every Poetry lock generation from 1.0 on — the 0.12 `[metadata.hashes]` layout is refused because that installer ignores URL sources; a Poetry < 1.4 writer additionally gets `redirect_poetry_stale_install_risk`, see `docs/testing/poetry-compatibility.md`) / `pdm.lock` (PDM lock formats `2` and `4.3`–`4.5.1`; the identity-losing `3.1` / `4.0`–`4.2` formats and unknown future formats are refused with `redirect_pdm_refused`, and a lock-format-`2` writer additionally gets `redirect_pdm_legacy_sync_required`, see `docs/testing/pdm-compatibility.md`; when `uv.lock` or `poetry.lock` sits beside it they drive and `pdm.lock` is left alone), `Cargo.toml` / `Cargo.lock` / `.cargo/config.toml` (plus the legacy extensionless `.cargo/config` — cargo reads that spelling in preference when both exist, so the managed `[registries.…]` block is written into whichever one is present; **cargo also reads every workspace-member manifest** — the `[workspace] members` globs minus `exclude` — and every in-root path-dependency manifest, recursively, reached without crossing a symbolic link and never under `.socket/`, and pins the crate in each one that declares it, so those `/Cargo.toml` files can appear in `rewrittenFiles`. A crate is redirected only when every declaration pins and every other `Cargo.lock` package depending on it is a planned member: one a registry or git crate — or a path package outside the root or behind a link — also depends on is refused `redirect_cargo_transitive_dependents` (a pin reaches only the declarations it sits on), a crate no manifest declares keeps `redirect_cargo_toml_dep_not_found` with a transitive-only detail naming `--mode vendored`, a crate every declaration of which requires another version (no requirement accepts the patched version) is refused `redirect_cargo_toml_dep_unrewritable`, and so is a requirement that also matches another locked version of the crate — each a transactional skip, never recorded or attested. With NO `Cargo.lock` there is no resolved graph to ask, so the dependents question is answered from the manifests instead: a crate declared beside any other dependency — anything but a path dependency on a manifest this run also pins, or a `workspace = true` inheritor of a table it scans — or beside a workspace member this run did not read (a `members` glob, or a member outside the project or behind a symbolic link, which member discovery drops) is refused `redirect_cargo_lockless_dependents`, whose detail names the remedies (commit a lockfile, or `--mode vendored`); a project whose only dependency is the patched crate has nothing that could pull it in and still redirects. All-CRLF manifests, locks and configs are rewritten with CRLF kept (mixed endings keep refusing where the grammar does not match), and `remove` / rollback match the recorded fragments across a later CRLF↔LF checkout conversion), `composer.lock`, `nuget.config` / `packages.lock.json` (plus, v5.0 #353/#514, every lock a project under the root restores into: a member project's `packages.lock.json`, a per-project `packages..lock.json`, a literal `NuGetLockFilePath` — each pinned with the root config; a `NuGetLockFilePath` it cannot evaluate warns `redirect_nuget_lock_path_unresolved` and a project tree it cannot list warns `redirect_nuget_lock_unreadable`, both skipping the nuget redirect with nothing written), `Gemfile` / `Gemfile.lock`, `pom.xml` (+ `.mvn/maven.config` / `.mvn/checksums/checksums.sha256` for maven Trusted Checksums merge, and, for a Gradle build, every settings, build, `buildSrc`, included-build, applied and plugin-source script, version catalog and lock file the script graph reaches, plus `gradle/verification-metadata.xml`, `gradle/wrapper/gradle-wrapper.properties` and the owned `.socket/gradle/` files), and the sbt build files (`socket-patch.sbt`, `socket-patch-vendor.sbt`, `build.sbt`, `project/build.properties`, `.sbtopts`, `.jvmopts`; `build.sbt.lock` and the Mill / scala-cli build files `build.mill`, `build.mill.yaml`, `build.sc`, `.mill-version`, `project.scala` for their presence only) — read, never edited; `socket-patch.sbt` is the only sbt file hosted mode writes (see **Hosted sbt** below). **npm-family flavor coverage**: package-lock / npm-shrinkwrap (a package the project patches itself with npm ≥ 12.1's native `npm patch` — a root `patchedDependencies` key, or the lock entry's `patched` record — is left on its registry entry in every npm lock, `redirect_npm_patched_dependency_skipped`), pnpm (root OR any nested `*/pnpm-lock.yaml`), yarn classic (a yarn 2+ install migrates a v1 `yarn.lock` and drops its pins, so a run whose v1 lock carries a hosted pin warns `redirect_yarn_classic_berry_migration_risk` — the hosted twin of the vendored `yarn_classic_berry_migration_risk` — unless the root `package.json`, read as advisory input, declares `"packageManager": "yarn@1…"`), **yarn berry** (the pin yarn writes for a root `resolutions` entry: the root `package.json` — edited only beside a berry `yarn.lock` — gains one `"@npm:": ""` selector per locked range (`redirect_yarn_berry_resolution` edits), and only that `yarn.lock` entry is re-keyed `"@"` with the same `resolution:` + `yarnBerry10c0` checksum (`redirect_yarn_berry_entry`), moved to yarn's key order; never an `npm:` locator, whose fetcher sends npm registry auth to the patch host, nor a tarball locator under an `npm:` key, which hardened mode rejects (YN0078). An older release's `npm:::__archiveUrl=` pin is still recognized and is re-pinned on the next run; rollback rebuilds the key from the selectors and drops them. Refused, nothing written: a user-authored `resolutions` entry for the package `redirect_yarn_berry_resolutions_conflict`, no root manifest `redirect_yarn_berry_manifest_missing`, a builtin `patch:` entry wrapping the same descriptor `redirect_yarn_berry_shared_descriptor`, an artifact URL yarn cannot fetch as a tarball `redirect_yarn_berry_artifact_url_unsupported`; cacheKey `10c0` and `.yarnrc.yml compressionLevel 0` gated by `redirect_yarn_berry_cache_unsupported`), and **bun** (text `bun.lock` lockfileVersion 0, 1 or 2 — 0 is the `--save-text-lockfile` opt-in lock of Bun 1.1.39–1.1.45, 1 the 1.2–1.3 default, 2 the 1.4+ default; all three emit one `packages` grammar, so the registry 4-tuple → URL 3-tuple rewrite is version-independent and the lock's own version line is kept. Any other or missing version, or a `packages` section outside bun's single-line grammar, is refused `redirect_bun_lock_unsupported` — the detail is the shared version gate's text (a newer version: update socket-patch, re-locking would reproduce it; no integer: re-lock with Bun ≥ 1.2), identical to the vendored refusal. A version-0 lock holding `workspace:` packages is refused `redirect_bun_workspace_unsupported` (its 2-tuple workspace grammar cannot keep the hosted tuple through a frozen install); the remedy is to delete `bun.lock` and re-run `bun install` with Bun ≥ 1.2, which writes lockfileVersion 1 (accepted). A plain in-place `bun install` bumps the version only when a workspace depends on another workspace (e.g. root → member — the shape the matrix measured); otherwise Bun 1.2.0 keeps version 0 and Bun 1.2.23+ fail to resolve, so the in-place bump is not the documented remedy. Bun lock version, grammar and workspace compatibility are checked before a vendored takeover, including during dry-run: these refusals preserve the existing lock, artifact and vendor ledger. Version-1 and version-2 workspace locks are rewritten, nested versions included. A granted dep with no rewritable entry warns `redirect_bun_entry_not_found` (or `redirect_bun_non_registry_entry_skipped` when the only same-version entry is a user URL / `file:` tarball, #497), a grant without a sha512 `redirect_bun_missing_sha512`; a CRLF lock keeps `\r\n` on the rewritten line, and a hosted URL left by an earlier grant of the same `name@version` is re-pinned in place. **Digest-less re-saves (Bun 1.1.39–1.3.9)**: every text-lock Bun below 1.3.10 re-saves a URL tuple WITHOUT its `sha512` whenever the lock is re-saved for another reason (`bun add`, `bun install` after a package.json or workspace change), leaving the 2-tuple `["name@", {meta}]` — the spec Bun installs from is intact. The CLI treats that spelling as its own wiring: a repeat hosted run counts the dep as redirected (no `redirect_bun_entry_not_found`) and HEALS the line back to the 3-tuple with the current `sha512`, recording the heal as a further `redirect_bun_lock_package` edit whose `original` is the 2-tuple (a stale URL is re-pinned from either spelling); `rollback`, scoped `rollback ` / `remove ` and the vendored takeover accept the digest-less spelling of a recorded `new` line (same key, spec and meta, only the trailing `"sha512-…"` missing) and restore the recorded original over it, so the chain always unwinds to the pristine registry line. Anything else — another uuid/token, another version, a re-laid meta object — is still drift. **Native `bun.lockb`**: when no text `bun.lock` exists, binary format versions 1, 2 and 3 are read and rewritten directly. Socket Patch does not invoke Bun or convert the project to a text lockfile. Exact matching package records are rewritten to hosted tarballs with the granted integrity, preserving dependency resolution IDs, workspace/dependency topology and unrelated package metadata; binary pointers and the package metadata hash are updated. Per-package `redirect_bun_lockb_package` snapshots support scoped rollback, repeat runs, superseding grants and hosted ↔ vendored takeover. A regular binary lock is discoverable even with no Bun runtime or `node_modules`; a dry run previews the same binary edits without writing them. A malformed, unreadable, unsupported or unverified binary structure is `redirect_bun_lockb_invalid` (exit 0, `redirected: 0`), and it refuses the npm rewrite before any takeover or sibling npm-family lock mutation. A symlinked binary write target is `redirect_symlinked_file_unsupported` (exit 1, including dry-run). `bun.lock` wins when both spellings exist. Binary-only projects do not receive `redirect_npm_no_lockfile`. Measured boundaries and the real-Bun matrix: `docs/testing/bun-compatibility.md`), and **vlt** (`vlt-lock.json` without `lockfileVersion`, `0` or `1`; see the vlt hosted-mode contract below). **Rush monorepos**: when `rush.json` is present the rewriter also reads `common/config/rush/pnpm-lock.yaml` and each `common/config/subspaces//pnpm-lock.yaml` (sorted for determinism) under their repo-relative keys and repoints them in place; editing them emits `redirect_rush_repo_state_stale` when the `repo-state.json` beside a rewritten lock exists (`common/config/rush/repo-state.json` for the common lock, `common/config/subspaces//repo-state.json` for a subspace lock; the `pnpmShrinkwrapHash` desync is refreshed by `rush update`, which the redirect survives), and `redirect_pnpm_trust_lockfile` carries the Rush pnpm >=11 install remedy (see the trust paragraph above). **maven** is fail-closed via version suffixing: a `mavenSuffixedVersion` + `mavenPomSha256` override pins the Socket-only `-socket.` by rewriting the literal `` (`redirect_maven_dep_version`) or adding a `` entry (`redirect_maven_dep_management_added`), plus optional Trusted Checksums (`redirect_maven_trusted_checksums`, conflicts as `redirect_maven_trusted_checksums_conflict`; when `.mvn/wrapper/maven-wrapper.properties` pins a Maven older than 3.9.4, which ignores those files, the additive warning `redirect_maven_trusted_checksums_unenforced`); a `${property}` version is refused (`redirect_maven_dep_unpinned`), a reactor root (a `` / `` declaration, a profile's included) is refused whole with `redirect_maven_multimodule_unsupported`: `pom.xml` and `.mvn/` are left untouched and the dep is not counted as redirected (a Gradle build beside it is still planned by the Gradle rewriter, but a dep in a root with a `pom.xml` is confirmed only when the pom pins it too), since a module's own literal `` would shadow a root pin (use `--mode vendored`; `vex` omits a hosted pin in a reactor root as `vex_maven_reactor_root`), a non-matching literal skipped (`redirect_maven_dep_version_mismatch`), and an override without a suffixed version falls back to same-GAV repository injection (`redirect_maven_same_gav_fallback`, NOT fail-closed). **gradle** (v5.0) is automated wiring, no longer a pasted snippet: the owned settings script `.socket/gradle/socket-patch.hosted.settings.gradle` with its index `.socket/gradle/hosted-index.tsv`, one apply line per build's settings file, every lock entry of the GA moved to the suffixed version, and the suffixed component in an existing `gradle/verification-metadata.xml`. A refused dep writes nothing and keeps `redirect_gradle_manual_snippet` as its fallback; same-GAV grants are refused (`redirect_gradle_same_gav_unsupported`). Rules, refusals and codes: [Gradle builds](#gradle-builds-v50).
+The rewriter reads a fixed set of candidate files from the project root: the npm-family locks (`package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, `shrinkwrap.yaml`, `yarn.lock`, plus `.yarnrc.yml` for the berry cache-config gate, `bun.lock` / `bun.lockb`, and `vlt-lock.json` with `vlt.json` and `node_modules/.vlt-lock.json` read only), `requirements.txt` / `uv.lock` / `Pipfile.lock` (pipfile-spec 6; see the Pipenv section below) / `poetry.lock` (every Poetry lock generation from 1.0 on — the 0.12 `[metadata.hashes]` layout is refused because that installer ignores URL sources; a Poetry < 1.4 writer additionally gets `redirect_poetry_stale_install_risk`, see `docs/testing/poetry-compatibility.md`) / `pdm.lock` (PDM lock formats `2` and `4.3`–`4.5.1`; the identity-losing `3.1` / `4.0`–`4.2` formats and unknown future formats are refused with `redirect_pdm_refused`, and a lock-format-`2` writer additionally gets `redirect_pdm_legacy_sync_required`, see `docs/testing/pdm-compatibility.md`; when `uv.lock` or `poetry.lock` sits beside it they drive and `pdm.lock` is left alone), `Cargo.toml` / `Cargo.lock` / `.cargo/config.toml` (plus the legacy extensionless `.cargo/config` — cargo reads that spelling in preference when both exist, so the managed `[registries.…]` block is written into whichever one is present; **cargo also reads every workspace-member manifest** — the `[workspace] members` globs minus `exclude` — and every in-root path-dependency manifest, recursively, reached without crossing a symbolic link and never under `.socket/`, and pins the crate in each one that declares it, so those `/Cargo.toml` files can appear in `rewrittenFiles`. A crate is redirected only when every declaration pins and every other `Cargo.lock` package depending on it is a planned member: one a registry or git crate — or a path package outside the root or behind a link — also depends on is refused `redirect_cargo_transitive_dependents` (a pin reaches only the declarations it sits on), a crate no manifest declares keeps `redirect_cargo_toml_dep_not_found` with a transitive-only detail naming `--mode vendored`, a crate every declaration of which requires another version (no requirement accepts the patched version) is refused `redirect_cargo_toml_dep_unrewritable`, and so is a requirement that also matches another locked version of the crate — each a transactional skip, never recorded or attested. With NO `Cargo.lock` there is no resolved graph to ask, so the dependents question is answered from the manifests instead: a crate declared beside any other dependency — anything but a path dependency on a manifest this run also pins, or a `workspace = true` inheritor of a table it scans — or beside a workspace member this run did not read (a `members` glob, or a member outside the project or behind a symbolic link, which member discovery drops) is refused `redirect_cargo_lockless_dependents`, whose detail names the remedies (commit a lockfile, or `--mode vendored`); a project whose only dependency is the patched crate has nothing that could pull it in and still redirects. All-CRLF manifests, locks and configs are rewritten with CRLF kept (mixed endings keep refusing where the grammar does not match), and `remove` / rollback match the recorded fragments across a later CRLF↔LF checkout conversion), `composer.lock`, `nuget.config` / `packages.lock.json` (plus, v5.0 #353/#514, every lock a project under the root restores into: a member project's `packages.lock.json`, a per-project `packages..lock.json`, a literal `NuGetLockFilePath` — each pinned with the root config; a `NuGetLockFilePath` it cannot evaluate warns `redirect_nuget_lock_path_unresolved` and a project tree it cannot list warns `redirect_nuget_lock_unreadable`, both skipping the nuget redirect with nothing written), `Gemfile` / `Gemfile.lock`, `pom.xml` (+ `.mvn/maven.config` / `.mvn/checksums/checksums.sha256` for maven Trusted Checksums merge, and, for a Gradle build, every settings, build, `buildSrc`, included-build, applied and plugin-source script, version catalog and lock file the script graph reaches, plus `gradle/verification-metadata.xml`, `gradle/wrapper/gradle-wrapper.properties` and the owned `.socket/gradle/` files), and the sbt build files (`socket-patch.sbt`, `socket-patch-vendor.sbt`, `build.sbt`, `project/build.properties`, `.sbtopts`, `.jvmopts`; `build.sbt.lock` and the Mill / scala-cli build files `build.mill`, `build.mill.yaml`, `build.sc`, `.mill-version`, `project.scala` for their presence only) — read, never edited; `socket-patch.sbt` is the only sbt file hosted mode writes (see **Hosted sbt** below). **npm-family flavor coverage**: package-lock / npm-shrinkwrap (a package the project patches itself with npm ≥ 12.1's native `npm patch` — a root `patchedDependencies` key, or the lock entry's `patched` record — is left on its registry entry in every npm lock, `redirect_npm_patched_dependency_skipped`), pnpm (root OR any nested `*/pnpm-lock.yaml`), yarn classic (a yarn 2+ install migrates a v1 `yarn.lock` and drops its pins, so a run whose v1 lock carries a hosted pin warns `redirect_yarn_classic_berry_migration_risk` — the hosted twin of the vendored `yarn_classic_berry_migration_risk` — unless the root `package.json`, read as advisory input, declares `"packageManager": "yarn@1…"`), **yarn berry** (the pin yarn writes for a root `resolutions` entry: the root `package.json` — edited only beside a berry `yarn.lock` — gains one `"@npm:": ""` selector per locked range (`redirect_yarn_berry_resolution` edits), and only that `yarn.lock` entry is re-keyed `"@"` with the same `resolution:` + `yarnBerry10c0` checksum (`redirect_yarn_berry_entry`), moved to yarn's key order; never an `npm:` locator, whose fetcher sends npm registry auth to the patch host, nor a tarball locator under an `npm:` key, which hardened mode rejects (YN0078). An older release's `npm:::__archiveUrl=` pin is still recognized and is re-pinned on the next run; rollback rebuilds the key from the selectors and drops them. Refused, nothing written: a user-authored `resolutions` entry for the package `redirect_yarn_berry_resolutions_conflict`, no root manifest `redirect_yarn_berry_manifest_missing`, a builtin `patch:` entry wrapping the same descriptor `redirect_yarn_berry_shared_descriptor`, an artifact URL yarn cannot fetch as a tarball `redirect_yarn_berry_artifact_url_unsupported`; cacheKey `10c0` and `.yarnrc.yml compressionLevel 0` gated by `redirect_yarn_berry_cache_unsupported`), and **bun** (text `bun.lock` lockfileVersion 0, 1 or 2 — 0 is the `--save-text-lockfile` opt-in lock of Bun 1.1.39–1.1.45, 1 the 1.2–1.3 default, 2 the 1.4+ default; all three emit one `packages` grammar, so the registry 4-tuple → URL 3-tuple rewrite is version-independent and the lock's own version line is kept. Any other or missing version, or a `packages` section outside bun's single-line grammar, is refused `redirect_bun_lock_unsupported` — the detail is the shared version gate's text (a newer version: update socket-patch, re-locking would reproduce it; no integer: re-lock with Bun ≥ 1.2), identical to the vendored refusal. A version-0 lock holding `workspace:` packages is refused `redirect_bun_workspace_unsupported` (its 2-tuple workspace grammar cannot keep the hosted tuple through a frozen install); the remedy is to delete `bun.lock` and re-run `bun install` with Bun ≥ 1.2, which writes lockfileVersion 1 (accepted). A plain in-place `bun install` bumps the version only when a workspace depends on another workspace (e.g. root → member — the shape the matrix measured); otherwise Bun 1.2.0 keeps version 0 and Bun 1.2.23+ fail to resolve, so the in-place bump is not the documented remedy. Bun lock version, grammar and workspace compatibility are checked before a vendored takeover, including during dry-run: these refusals preserve the existing lock, artifact and vendor ledger. Version-1 and version-2 workspace locks are rewritten, nested versions included. A granted dep with no rewritable entry warns `redirect_bun_entry_not_found` (or `redirect_bun_non_registry_entry_skipped` when the only same-version entry is a user URL / `file:` tarball, #497), a grant without a sha512 `redirect_bun_missing_sha512`; a CRLF lock keeps `\r\n` on the rewritten line, and a hosted URL left by an earlier grant of the same `name@version` is re-pinned in place. **Digest-less re-saves (Bun 1.1.39–1.3.9)**: every text-lock Bun below 1.3.10 re-saves a URL tuple WITHOUT its `sha512` whenever the lock is re-saved for another reason (`bun add`, `bun install` after a package.json or workspace change), leaving the 2-tuple `["name@", {meta}]` — the spec Bun installs from is intact. The CLI treats that spelling as its own wiring: a repeat hosted run counts the dep as redirected (no `redirect_bun_entry_not_found`) and HEALS the line back to the 3-tuple with the current `sha512`, recording the heal as a further `redirect_bun_lock_package` edit whose `original` is the 2-tuple (a stale URL is re-pinned from either spelling); `rollback`, scoped `rollback ` / `remove ` and the vendored takeover accept the digest-less spelling of a recorded `new` line (same key, spec and meta, only the trailing `"sha512-…"` missing) and restore the recorded original over it, so the chain always unwinds to the pristine registry line. Anything else — another uuid/token, another version, a re-laid meta object — is still drift. **Native `bun.lockb`**: when no text `bun.lock` exists, binary format versions 1, 2 and 3 are read and rewritten directly. Socket Patch does not invoke Bun or convert the project to a text lockfile. Exact matching package records are rewritten to hosted tarballs with the granted integrity, preserving dependency resolution IDs, workspace/dependency topology and unrelated package metadata; binary pointers and the package metadata hash are updated. Per-package `redirect_bun_lockb_package` snapshots support scoped rollback, repeat runs, superseding grants and hosted ↔ vendored takeover. A regular binary lock is discoverable even with no Bun runtime or `node_modules`; a dry run previews the same binary edits without writing them. A malformed, unreadable, unsupported or unverified binary structure is `redirect_bun_lockb_invalid` (exit 0, `redirected: 0`), and it refuses the npm rewrite before any takeover or sibling npm-family lock mutation. A symlinked binary write target is `redirect_symlinked_file_unsupported` (exit 1, including dry-run). `bun.lock` wins when both spellings exist. Binary-only projects do not receive `redirect_npm_no_lockfile`. Measured boundaries and the real-Bun matrix: `docs/testing/bun-compatibility.md`), and **vlt** (`vlt-lock.json` without `lockfileVersion`, `0` or `1`; see the vlt hosted-mode contract below). **Rush monorepos**: when `rush.json` is present the rewriter also reads `common/config/rush/pnpm-lock.yaml` and each `common/config/subspaces//pnpm-lock.yaml` (sorted for determinism) under their repo-relative keys and repoints them in place; editing them emits `redirect_rush_repo_state_stale` when the `repo-state.json` beside a rewritten lock exists (`common/config/rush/repo-state.json` for the common lock, `common/config/subspaces//repo-state.json` for a subspace lock; the `pnpmShrinkwrapHash` desync is refreshed by `rush update`, which the redirect survives), and `redirect_pnpm_trust_lockfile` carries the Rush pnpm >=11 install remedy (see the trust paragraph above). **maven** is fail-closed via version suffixing: a `mavenSuffixedVersion` + `mavenPomSha256` override pins the Socket-only `-socket.` by rewriting the literal `` (`redirect_maven_dep_version`) or adding a `` entry (`redirect_maven_dep_management_added`), plus optional Trusted Checksums (`redirect_maven_trusted_checksums`, conflicts as `redirect_maven_trusted_checksums_conflict`; when `.mvn/wrapper/maven-wrapper.properties` pins a Maven older than 3.9.4, which ignores those files, the additive warning `redirect_maven_trusted_checksums_unenforced`); a `${property}` version is refused (`redirect_maven_dep_unpinned`), a reactor root (a `` / `` declaration, a profile's included) is refused whole with `redirect_maven_multimodule_unsupported`: `pom.xml` and `.mvn/` are left untouched and the dep is not counted as redirected (a Gradle build beside it is still planned by the Gradle rewriter, but a dep in a root with a `pom.xml` is confirmed only when the pom pins it too), since a module's own literal `` would shadow a root pin (use `--mode vendored`; `vex` omits a hosted pin in a reactor root as `vex_maven_reactor_root`), a non-matching literal skipped (`redirect_maven_dep_version_mismatch`), a `` variant (sources, tests, a native build) left at its version (`redirect_maven_classifier_unsupported`; the grant serves the main jar only, and the dep is skipped when a variant other than `sources` / `javadoc` names the patched release), a versioned declaration inside `` (a literal or a `${property}`) left as-is (`redirect_maven_profile_dependency_unpatched`; the dep is skipped when a profile holds its only declaration), and a pom not readable as one left untouched (`redirect_maven_pom_unreadable`). Matches and anchors skip comments, CDATA, ``, ``, ``, `` and `` (the scope `vex` reads), and an existing self-closed `` / `` is expanded in place, never duplicated; an override without a suffixed version falls back to same-GAV repository injection (`redirect_maven_same_gav_fallback`, NOT fail-closed). **gradle** (v5.0) is automated wiring, no longer a pasted snippet: the owned settings script `.socket/gradle/socket-patch.hosted.settings.gradle` with its index `.socket/gradle/hosted-index.tsv`, one apply line per build's settings file, every lock entry of the GA moved to the suffixed version, and the suffixed component in an existing `gradle/verification-metadata.xml`. A refused dep writes nothing and keeps `redirect_gradle_manual_snippet` as its fallback; same-GAV grants are refused (`redirect_gradle_same_gav_unsupported`). Rules, refusals and codes: [Gradle builds](#gradle-builds-v50).
**Hosted sbt (v5.0, additive)**: an sbt build root (`project/build.properties` naming an `sbt.version`, 0.13.18 or later) is wired through ONE generated root file, `socket-patch.sbt` — no user file is edited. It pins every granted Maven patch build-wide (a `ThisBuild` `dependencyOverrides +=` of the Socket-only `-socket.` version plus a `file:` resolver over `.socket/sbt-hosted/maven2/`, moved ahead of the default repositories on sbt 0.13 / 1.x so an unreachable one never blocks it offline), downloads the pinned pom and jar there on the first sbt load (sha256-checked, gitignored by the file itself), and installs a load-time verifier that fails `update` when any project resolves another version or a pinned artifact whose bytes are not pinned. Edits: `redirect_sbt_pin` (added), `redirect_sbt_pin_updated` (an existing row replaced: same GA and base under a new uuid, or the same uuid with new served values; `original` names the previous uuid and version), `redirect_sbt_pin_rechecked` (an existing row re-verified after the build's dependencies changed: its dependency digest is recorded anew, `original`/`new` are `{deps}`). The load-time verifier also fails `update` when a project declares a pinned GA at a version newer than the pin's base (the build-wide override would otherwise force it back down). A new pin is gated on sbt's own resolution records under `target/` (never the machine-wide cache): run-level stops wire nothing, warn once and exit 0 — `redirect_sbt_no_resolution_evidence` (none; run `sbt update` first; always the in-memory engine's answer), `redirect_sbt_resolution_incomplete` (a declared project left no evidence, or the project definitions cannot be read statically), `redirect_sbt_resolution_stale` (a build source is newer than some project's evidence: each project is dated by its own newest record, so a partial `sbt /update` does not vouch for the others). Per-patch refusals (never confirmed): `redirect_sbt_missing_override` (no `maven2` override or no suffixed version), `redirect_sbt_integrity_missing` (jar or pom sha256 missing), `redirect_sbt_unsafe_value` (a value unsafe in a Scala literal, or an index URL not naming the uuid), `redirect_sbt_version_conflict` (some project resolves another version, or a build source declares the GA newer than the patch's base), `redirect_sbt_override_conflict` (two patches for one GA in a run, or another base already pinned), `redirect_sbt_vendored_conflict` (the GA is pinned by `socket-patch-vendor.sbt`, or that file cannot be parsed — then every Maven patch), `redirect_sbt_owned_file_modified` / `redirect_sbt_owned_file_foreign` (`socket-patch.sbt` edited, or not socket-patch's — every Maven patch), `redirect_sbt_owned_file_unreadable` (a whole-run refusal: `socket-patch.sbt` is on disk but cannot be read as UTF-8 text, so writing it would replace it; nothing is written), `redirect_sbt_unsupported_version`, `redirect_sbt_build_root_unknown` (sbt files but no versioned build root — every Maven patch), `redirect_sbt_overrides_assignment` / `redirect_sbt_resolvers_assignment` (a build source reassigns `dependencyOverrides` / `resolvers` with `:=`, `~=` or `--=`), `redirect_sbt_dependency_lock_present` (a `build.sbt.lock`), `redirect_sbt_scala_runtime_unsupported` (`org.scala-lang`), `redirect_sbt_classifier_unsupported`; a GA no library configuration resolves is skipped silently (`redirect_sbt_meta_build_only` when only the meta-build resolves it). Advisories: `redirect_sbt_version_untested` (sbt 2.1+, still wired), `redirect_sbt_override_build_repos` (`sbt.override.build.repos=true`), `redirect_maven_pom_ignored_sbt_build` (a `pom.xml` beside the sbt build, which sbt never reads; the Maven rewriter still edits it for the Maven build). A re-run keeps an existing row and re-checks it. When the build's dependency digest changed since the pin, evidence resolved after the change (fresh, newer than the generated file) re-verifies it and the row's digest is refreshed (`redirect_sbt_pin_rechecked`); the uuid is NOT confirmed on `redirect_sbt_pin_declared_newer` (a build source now declares the GA newer than the pin's base; the row stays, sbt's load-time verifier fails the build, and the remedy is `socket-patch rollback` or declaring the base again), `redirect_sbt_pin_unverifiable` (the digest changed and the evidence predates the change, or the digest cannot be computed: run `sbt update`, then re-run socket-patch), `redirect_sbt_override_shadowed` (the evidence still resolves the base version) or `redirect_sbt_resolved_elsewhere` (the pinned version resolves from outside the pin repository from a file whose sha256 is not the pinned jar's; a copy holding the pinned bytes, such as the Ivy cache a second checkout reads, is fine — at most 64 pinned artifact files of up to 256 MiB are hashed, anything else counts as elsewhere), and also when a build source now reassigns `dependencyOverrides` / `resolvers` or a `build.sbt.lock` appeared (the same `redirect_sbt_overrides_assignment` / `redirect_sbt_resolvers_assignment` / `redirect_sbt_dependency_lock_present` codes; the row stays and sbt's load-time verifier fails the build). For a pure sbt root (no `pom.xml` / Gradle script beside it), maven confirmation is decided only by the sbt rewriter's report; on a mixed root a uuid the sbt rewriter refused is still confirmed by the Maven rewriter's own `pom.xml` pin (the generated sbt files never prove a pin by substring). **Mill and scala-cli** are guidance only: per Maven patch `redirect_mill_manual_snippet` / `redirect_scala_cli_manual_snippet` carry a paste-able snippet (repository + forced suffixed version), nothing is written or confirmed, and a pure Mill / scala-cli root gets no `redirect_maven_no_pom`; there, a Maven patch the server sent without a `maven2` registry override gets `redirect_maven_missing_override` instead of a snippet (with a `pom.xml` beside the Mill / scala-cli files the pom rewriter reports it). `rollback` / `remove` restore `socket-patch.sbt` offline (the rows removed, the file deleted with its last pin; the gitignored downloads are left). Manifest-less VEX reads every strictly parsed pin as a hosted reference but grants it the lockfile basis only when the local evidence shows every recorded version of the GA is the pinned one and every recorded artifact hashes to a pinned sha256 (else `sbt_resolution_unverified`).
@@ -436,7 +436,7 @@ Recognition rules that hold for every ecosystem:
* **Patch hosts.** A hosted reference counts only on `https://patch.socket.dev` or the `--patch-server-url` / `SOCKET_PATCH_SERVER_URL` origin, with no userinfo. The uuid is the URL's LAST canonical-uuid path segment, because grant tokens may themselves be uuid-shaped. The Go module prefix is fixed. `socket-patch-` registry / repository / source names count only through a pin. For a URL on any other host, see **Patch hosts** above.
* **Pins, not definitions.** A registry, index or source *definition* alone (cargo `[registries]`, nuget ``, pom ``, uv index tables, `.npmrc`) never makes a reference, because it survives a reverted pin. Sections the package manager ignores are not read: npm's v2 `dependencies` mirror, a `.cargo/config.toml` shadowed by `.cargo/config`. A Socket pin inside a maven `` is diagnosed, never a reference.
* **Contested locks.** When one lock wires a package to a patch and another lock resolves the same `name@version` from a non-Socket source, the build's bytes depend on which package manager runs. The reference is then dropped with a `patched_ref_unattributable` diagnostic naming both files. This applies across npm / pnpm / yarn / bun and across uv / pylock / poetry / pdm / Pipfile.lock / requirements. PEP 723 script locks neither contest nor are contested. The root `requirements.txt` and its in-root `-r` includes count as one lock here: pip reads them as one requirement set, where a direct reference wins over a compatible `==` pin of the same version in another file of the set (#1086). A **bundled** npm copy (`inBundle: true`, or v1 `bundled: true`) of the same `name@version` contests the reference too, in the same lock, in the other npm lock of a shrinkwrap/package-lock pair, or in any other lock. npm unpacks it from the parent package's tarball, so no rewire reaches it and it stays unpatched. Bun and vlt unpack bundled copies the same way (#469, #471). pnpm unpacks bundled copies too, but its lock cannot tie one to a reference (see **Unattested references** below). For Bun, that is a `bun.lock` entry whose meta is `{ "bundled": true }`, or a `bun.lockb` record that a dependency edge with the `bundled` behavior bit reaches, including one Bun shares with a regular install. Such an entry is never a reference, and it contests the reference the same way. For vlt, the lock records no node for a bundled copy, so the copy is found in the installed store: a real package directory inside a store package's own `node_modules`. Hosted and vendored scans skip these copies with `redirect_bun_bundled_instance_skipped` / `redirect_vlt_bundled_instance_skipped` / `vendor_bundled_instance_skipped`. When a bundled copy is the only instance, vendoring refuses with `vendor_lock_entry_not_rewritable`. A reference withheld only because such an unreachable copy installs beside it (a bundled npm / Bun / vlt copy, an npm copy beneath a `hasShrinkwrap` package (#753) or one npm installs from a git / url / `file:` spec, or a yarn classic git or `file:` directory block of the same `name@version`) is still the rewriter's own wiring of exactly one package version: it is never attested, but `rollback`, `remove`, `list` and the hosted → vendored takeover treat it as an ordinary hosted pin and restore its upstream registry entry (#828), rather than refusing it with `hosted_wiring_contested`. Another entry of the **same** npm, Bun or yarn lock that resolves the wired `name@version` from a non-Socket source (for example a workspace member added after the rewire, then `npm install` / `bun install` / `yarn install`; for yarn berry, a registry locator such as `left-pad@npm:1.3.0` beside the hosted `…::__archiveUrl=` one) contests the reference too (#588). The package manager installs both entries, and that copy stays unpatched. Re-running `scan` / `vendor` rewires every copy.
-* **Unattested references.** Some evidence shows a build may run a copy no wiring reaches, but cannot be tied to the reference's exact `name@version` or cannot say the build runs it. The reference then stays a reference: `list`, `rollback` and `remove` find it, and the ledgers' liveness gates (`vendor --check`, `scan`) keep treating the wiring as live, because re-running `scan` / `vendor` could never clear the evidence. Only `vex` omits it, as a run warning and as the `failed[].reason`. Three cases besides Gradle's (`vex_gradle_lock_above_base`): **pnpm bundled copies** (`vex_pnpm_bundled_copy`): a `packages:` entry's `bundledDependencies:` names the copies pnpm unpacks from that package's own tarball, but pnpm never locks them, so the bundled version is not in the lock. A pnpm reference whose package name a `bundledDependencies` list in the same lock names is omitted whatever its version (a missed attestation when the bundled copy is another version, never a false one), and `bundledDependencies: true` (or a value that cannot be read) omits every reference of that lock. It reaches no other lock. **deno.lock** (`vex_deno_lock_copy`): `deno install` installs a `package.json` project's npm dependencies from `deno.lock` and never reads `package-lock.json` / `pnpm-lock.yaml` / `yarn.lock`, so an npm-family reference whose `name@version` the `deno.lock` npm section also locks is omitted (#406). Whether Deno or another package manager populates `node_modules` (`nodeModulesDir: "manual"` allows either) is not in the files, so this holds whatever `nodeModulesDir` says. **Maven reactor roots** (`vex_maven_reactor_root`): a hosted pin in a root `pom.xml` that declares `` / `` is omitted, because a module's own literal `` overrides a root pin and module poms are not read (#261); hosted scan refuses such roots (`redirect_maven_multimodule_unsupported`), so this only covers pins an older release wrote.
+* **Unattested references.** Some evidence shows a build may run a copy no wiring reaches, but cannot be tied to the reference's exact `name@version` or cannot say the build runs it. The reference then stays a reference: `list`, `rollback` and `remove` find it, and the ledgers' liveness gates (`vendor --check`, `scan`) keep treating the wiring as live, because re-running `scan` / `vendor` could never clear the evidence. Only `vex` omits it, as a run warning and as the `failed[].reason`. Four cases besides Gradle's (`vex_gradle_lock_above_base`): **pnpm bundled copies** (`vex_pnpm_bundled_copy`): a `packages:` entry's `bundledDependencies:` names the copies pnpm unpacks from that package's own tarball, but pnpm never locks them, so the bundled version is not in the lock. A pnpm reference whose package name a `bundledDependencies` list in the same lock names is omitted whatever its version (a missed attestation when the bundled copy is another version, never a false one), and `bundledDependencies: true` (or a value that cannot be read) omits every reference of that lock. It reaches no other lock. **deno.lock** (`vex_deno_lock_copy`): `deno install` installs a `package.json` project's npm dependencies from `deno.lock` and never reads `package-lock.json` / `pnpm-lock.yaml` / `yarn.lock`, so an npm-family reference whose `name@version` the `deno.lock` npm section also locks is omitted (#406). Whether Deno or another package manager populates `node_modules` (`nodeModulesDir: "manual"` allows either) is not in the files, so this holds whatever `nodeModulesDir` says. **Maven reactor roots** (`vex_maven_reactor_root`): a hosted pin in a root `pom.xml` that declares `` / `` is omitted, because a module's own literal `` overrides a root pin and module poms are not read (#261); hosted scan refuses such roots (`redirect_maven_multimodule_unsupported`), so this only covers pins an older release wrote. **Maven classifier copies** (`vex_maven_classifier_unpatched`): a `` dependency of a hosted-pinned GA is its own coordinate; the classifier-less pin does not retarget it and the grant serves only the main jar, so Maven fetches it from the next repository. When a live, non-profile classifier copy other than `sources` / `javadoc` (`tests`, a native or JDK-specific build) resolves any version other than the suffixed pin — its own literal ``, a `` entry with the same classifier, or no version in the root pom — the pin is omitted. `sources` / `javadoc` copies never run on a classpath and do not withhold the statement; hosted scan only warns about them (`redirect_maven_classifier_unsupported`) and skips the dep when an executable copy names the patched release.
* **Shrinkwrap without a twin (#899).** npm >= 12 never reads `npm-shrinkwrap.json`: on a project whose only npm lock is the shrinkwrap it resolves the tree from the registry and writes a fresh `package-lock.json`, so a Socket reference there reaches npm <= 11 only. The reference is still discovered, so `list`, `rollback` and `remove` manage it, but `vex` omits the patch (`vex_npm_shrinkwrap_only`, as a run warning and as the `failed[].reason`). A `package-lock.json` beside it (wired the same way) makes it attestable again. Hosted and vendored runs still rewire the shrinkwrap and warn `redirect_npm_shrinkwrap_only` / `vendor_npm_shrinkwrap_only`.
* **Lockless pins.** With no lock to name a version, a `Cargo.toml` pin (every declaration on `socket-patch-`, that registry defined on the patch host for the same uuid) or an exclusive nuget exact-id mapping is never a reference on its own, so v5.0 does not attest it (nor does `list` show it, or `rollback` / `remove` restore it — restore those files from version control). Only a pre-v5 redirect-ledger record naming a version the pin admits keeps it live. The same holds for a gem wired only in the `Gemfile` (the pre-bundler-2.6 mixed state, lock not converged).
@@ -2349,6 +2349,7 @@ match it, withholds the statement:
| `vex_pnpm_bundled_copy` | run warning and `failed[].reason` | An npm pin is wired, but its pnpm lock names a package whose `bundledDependencies` may ship an unpatched copy of it (the lock does not record that copy's version); no statement while that package bundles it. `vendor --check` and `scan` still treat the wiring as live. |
| `vex_deno_lock_copy` | run warning and `failed[].reason` | An npm pin is wired, but `deno.lock` locks the same `name@version`, which `deno install` installs without the wiring; no statement while it does. `vendor --check` and `scan` still treat the wiring as live. |
| `vex_maven_reactor_root` | run warning and `failed[].reason` | A hosted Maven pin sits in a root `pom.xml` that declares `` / `` (a profile's included), where a module's own literal `` overrides it; only the root pom is read, so no statement (#261). `list`, `rollback` and `remove` still find the pin. |
+| `vex_maven_classifier_unpatched` | run warning and `failed[].reason` | A hosted Maven pin's GA also has a live executable `` copy (not `sources` / `javadoc`) at a version other than the pin, which the classifier-less pin does not retarget and the grant does not serve; no statement while it does. `list`, `rollback` and `remove` still find the pin. |
| `vex_gradle_derived_cache_unchecked` | run warning | The derived-cache walk was cut short (a very large transforms cache); the statement is still emitted, the copies the walk did reach were checked. |
A vendored Gradle entry attests only while its wiring is live (apply line, index rows,
diff --git a/crates/socket-patch-cli/src/commands/vex_sources.rs b/crates/socket-patch-cli/src/commands/vex_sources.rs
index 9b039a96f..a5843f37a 100644
--- a/crates/socket-patch-cli/src/commands/vex_sources.rs
+++ b/crates/socket-patch-cli/src/commands/vex_sources.rs
@@ -251,6 +251,11 @@ fn unattested_note(kind: UnattestedKind) -> (&'static str, &'static str) {
"not attested while the root declares modules (roll it back and re-patch the \
reactor with `scan --mode vendored`)",
),
+ UnattestedKind::MavenClassifierUnpatched => (
+ NOTE_MAVEN_CLASSIFIER_UNPATCHED,
+ "not attested while that classifier copy resolves an unpatched version; the \
+ wiring itself is intact, so re-running `scan` does not change this",
+ ),
}
}
@@ -261,6 +266,10 @@ pub(crate) const NOTE_NPM_SHRINKWRAP_ONLY: &str = "vex_npm_shrinkwrap_only";
/// Omission tag and note: a hosted Maven pin sits in a reactor root, where a
/// module's own `` may override it (`vex::Unattested`, #261).
pub(crate) const NOTE_MAVEN_REACTOR_ROOT: &str = "vex_maven_reactor_root";
+/// Omission tag and note: a hosted Maven pin's GA also has an executable
+/// classifier copy at another version, which the pin does not reach
+/// (`vex::Unattested`).
+pub(crate) const NOTE_MAVEN_CLASSIFIER_UNPATCHED: &str = "vex_maven_classifier_unpatched";
fn note(code: &'static str, detail: String) -> PlanNote {
PlanNote { code, detail }
diff --git a/crates/socket-patch-core/src/formats/maven/mod.rs b/crates/socket-patch-core/src/formats/maven/mod.rs
index cea11a5da..7d6cb9d38 100644
--- a/crates/socket-patch-core/src/formats/maven/mod.rs
+++ b/crates/socket-patch-core/src/formats/maven/mod.rs
@@ -44,66 +44,168 @@ pub(crate) struct PomDep {
/// Trimmed literal version, `${prop}` resolved from the root
/// `` when possible; `None` when the declaration has none.
pub(crate) version: Option,
+ /// The non-empty ``: a sibling artifact of the GA (sources,
+ /// tests, a native build), managed and resolved apart from the main jar.
+ pub(crate) classifier: Option,
/// Inside ``.
pub(crate) managed: bool,
pub(crate) in_profile: bool,
}
+/// Where Maven reads a root pom's model from: its text with comments and
+/// CDATA blanked (offsets kept), and the sections that are not the
+/// project's own resolution. `` / `` hold plugin
+/// classpaths, `` / `` plugin
+/// and deploy repositories; `` apply only when activated. Shared
+/// by the VEX reader ([`parse_pom`]) and the hosted rewriter, so the
+/// rewriter edits exactly the markup VEX later attests.
+#[derive(Debug)]
+pub(crate) struct PomScope {
+ /// The pom with comments and CDATA blanked byte-for-byte.
+ pub(crate) masked: String,
+ ignored: Vec,
+ profiles: Vec,
+}
+
+impl PomScope {
+ /// Fails (fail-closed) on a pom that is not well-formed where it
+ /// matters: no ``, or an unterminated element or CDATA.
+ pub(crate) fn new(raw: &str) -> Result {
+ let masked = blank_non_markup(raw)?;
+ if open_tags(&masked, "project")?.is_empty() || !masked.contains("") {
+ return Err("no element".to_string());
+ }
+ let ignored = [
+ elements(&masked, "build")?,
+ elements(&masked, "reporting")?,
+ elements(&masked, "pluginRepositories")?,
+ elements(&masked, "distributionManagement")?,
+ ]
+ .concat();
+ let profiles = elements(&masked, "profiles")?;
+ Ok(PomScope {
+ masked,
+ ignored,
+ profiles,
+ })
+ }
+
+ /// Inside a plugin / deploy section (never project resolution).
+ pub(crate) fn is_ignored(&self, pos: usize) -> bool {
+ inside(pos, &self.ignored)
+ }
+
+ /// Inside `` (read only when that profile is active).
+ pub(crate) fn in_profile(&self, pos: usize) -> bool {
+ inside(pos, &self.profiles)
+ }
+
+ /// Every `` element outside ignored sections (profiles included).
+ pub(crate) fn elements(&self, name: &str) -> Result, String> {
+ Ok(elements(&self.masked, name)?
+ .into_iter()
+ .filter(|e| !self.is_ignored(e.start))
+ .collect())
+ }
+
+ /// Every `` element Maven always reads: outside ignored sections
+ /// and profiles.
+ pub(crate) fn live(&self, name: &str) -> Result, String> {
+ Ok(self
+ .elements(name)?
+ .into_iter()
+ .filter(|e| !self.in_profile(e.start))
+ .collect())
+ }
+
+ /// The `<` of the closing `` tag.
+ pub(crate) fn project_close(&self) -> Option {
+ self.masked.rfind("")
+ }
+}
+
+fn inside(pos: usize, set: &[Element]) -> bool {
+ set.iter().any(|e| pos >= e.start && pos < e.end)
+}
+
+/// One `` element of a [`PomScope`] (outside ignored sections),
+/// with offsets into the pom.
+#[derive(Debug)]
+pub(crate) struct ScopedDep {
+ pub(crate) element: Element,
+ pub(crate) group: Option,
+ pub(crate) artifact: Option,
+ /// Inner-text byte range of the literal ``, when present.
+ pub(crate) version_inner: Option<(usize, usize)>,
+ /// Trimmed `` text as written (no property resolution).
+ pub(crate) version_text: Option,
+ pub(crate) type_text: Option,
+ /// Trimmed non-empty `` text.
+ pub(crate) classifier: Option,
+ pub(crate) in_profile: bool,
+}
+
+impl PomScope {
+ /// Every `` outside ignored sections, children read with
+ /// `` blanked (they carry their own groupId/artifactId).
+ pub(crate) fn dependencies(&self) -> Result, String> {
+ let mut out = Vec::new();
+ for dep in self.elements("dependency")? {
+ let body = blank_elements(dep.inner(&self.masked), "exclusions")?;
+ let version = elements(&body, "version")?.into_iter().next();
+ out.push(ScopedDep {
+ element: dep,
+ group: child_text(&body, "groupId")?,
+ artifact: child_text(&body, "artifactId")?,
+ version_inner: version.map(|v| {
+ (
+ dep.inner_start + v.inner_start,
+ dep.inner_start + v.inner_end,
+ )
+ }),
+ version_text: version.map(|v| v.inner(&body).trim().to_string()),
+ type_text: child_text(&body, "type")?,
+ classifier: child_text(&body, "classifier")?.filter(|c| !c.is_empty()),
+ in_profile: self.in_profile(dep.start),
+ });
+ }
+ Ok(out)
+ }
+}
+
/// A bounded, dependency-free scan of the pom's element structure — enough
/// for the handful of elements the Socket wirings touch. Comments and CDATA
/// sections are blanked first (Maven reads neither as elements); an
/// unterminated element or CDATA section is an error (fail-closed: nothing
/// is discovered from a pom that is not well-formed where it matters).
pub(crate) fn parse_pom(raw: &str) -> Result {
- let text = blank_non_markup(raw)?;
- if open_tags(&text, "project")?.is_empty() || !text.contains("") {
- return Err("no element".to_string());
- }
- let ignored = [
- elements(&text, "build")?,
- elements(&text, "reporting")?,
- elements(&text, "pluginRepositories")?,
- elements(&text, "distributionManagement")?,
- ]
- .concat();
- let profiles = elements(&text, "profiles")?;
- let managed = elements(&text, "dependencyManagement")?;
- let inside = |pos: usize, set: &[Element]| set.iter().any(|e| pos >= e.start && pos < e.end);
-
- let properties = properties(&text, &ignored, &profiles)?;
+ let scope = PomScope::new(raw)?;
+ let text = &scope.masked;
+ let managed = elements(text, "dependencyManagement")?;
+
+ let properties = properties(text, &scope.ignored, &scope.profiles)?;
let mut pom = Pom::default();
- for repo in elements(&text, "repository")? {
- if inside(repo.start, &ignored) {
- continue;
- }
- let body = repo.inner(&text);
+ for repo in scope.elements("repository")? {
+ let body = repo.inner(text);
pom.repos.push(PomRepo {
id: child_text(body, "id")?.unwrap_or_default(),
url: child_text(body, "url")?.unwrap_or_default(),
- in_profile: inside(repo.start, &profiles),
+ in_profile: scope.in_profile(repo.start),
});
}
- for dep in elements(&text, "dependency")? {
- if inside(dep.start, &ignored) {
- continue;
- }
- // `` carry their own groupId/artifactId children.
- let body = blank_elements(dep.inner(&text), "exclusions")?;
- let (Some(group), Some(artifact)) = (
- child_text(&body, "groupId")?,
- child_text(&body, "artifactId")?,
- ) else {
+ for dep in scope.dependencies()? {
+ let (Some(group), Some(artifact)) = (dep.group, dep.artifact) else {
continue;
};
- let version = child_text(&body, "version")?.map(|v| resolve_property(&v, &properties));
pom.deps.push(PomDep {
group,
artifact,
- version,
- managed: inside(dep.start, &managed),
- in_profile: inside(dep.start, &profiles),
+ version: dep.version_text.map(|v| resolve_property(&v, &properties)),
+ classifier: dep.classifier,
+ managed: inside(dep.element.start, &managed),
+ in_profile: dep.in_profile,
});
}
Ok(pom)
@@ -195,6 +297,50 @@ fn blank_elements(text: &str, name: &str) -> Result {
#[cfg(test)]
mod tests {
+ use super::PomScope;
+
+ #[test]
+ fn scope_skips_comments_and_plugin_sections_and_flags_profiles() {
+ let pom = "\
+ \
+ ga 1 \
+ xy\
+ ga1sources\
+ \
+ ga2\
+ ga3\
+ ";
+ let scope = PomScope::new(pom).unwrap();
+ let deps = scope.dependencies().unwrap();
+ let seen: Vec<_> = deps
+ .iter()
+ .map(|d| {
+ (
+ d.group.as_deref().unwrap(),
+ d.version_text.as_deref().unwrap(),
+ d.classifier.as_deref(),
+ d.in_profile,
+ )
+ })
+ .collect();
+ assert_eq!(
+ seen,
+ [
+ ("g", "1", None, false),
+ ("g", "1", Some("sources"), false),
+ ("g", "3", None, true),
+ ]
+ );
+ let (s, e) = deps[0].version_inner.unwrap();
+ assert_eq!(&pom[s..e], " 1 ", "offsets index the original pom");
+ let repos = scope.live("repositories").unwrap();
+ assert_eq!(repos.len(), 1, "the profile's section is not live");
+ assert_eq!(repos[0].open_tag(pom), "");
+ assert_eq!(scope.project_close(), pom.rfind(""));
+ assert!(PomScope::new("").is_err());
+ assert!(PomScope::new("").is_err());
+ }
+
#[test]
fn suffix_grammar_is_exact() {
assert_eq!(
diff --git a/crates/socket-patch-core/src/patch/redirect/maven_index.rs b/crates/socket-patch-core/src/patch/redirect/maven_index.rs
new file mode 100644
index 000000000..9a2acbdf2
--- /dev/null
+++ b/crates/socket-patch-core/src/patch/redirect/maven_index.rs
@@ -0,0 +1,513 @@
+//! The hosted Maven rewriter's view of `pom.xml`: every ``, live
+//! `` and section anchor it reads, located once through
+//! [`PomScope`] (comments, CDATA, ``, ``,
+//! ``, `` and `` are
+//! never anchors) and then kept in step with the rewriter's own edits by
+//! shifting offsets, so a pom with many patched dependencies is scanned once,
+//! not once per dependency.
+
+use crate::formats::maven::PomScope;
+use crate::formats::xml::{child_text, children, elements, Element};
+
+/// One `` outside ignored sections.
+#[derive(Debug, Clone)]
+pub(super) struct IndexedDep {
+ pub(super) group: Option,
+ pub(super) artifact: Option,
+ /// Inner-text byte range of the literal ``.
+ pub(super) version_inner: Option<(usize, usize)>,
+ pub(super) version_text: Option,
+ pub(super) type_text: Option,
+ pub(super) classifier: Option,
+ pub(super) in_profile: bool,
+}
+
+/// One live ``.
+#[derive(Debug, Clone)]
+struct IndexedRepo {
+ id: Option,
+ /// Inner-text byte range of its ``.
+ url_inner: Option<(usize, usize)>,
+}
+
+/// A section the rewriter inserts into.
+#[derive(Debug, Clone, Copy)]
+enum Anchor {
+ Repositories,
+ Dm,
+ DmDeps,
+}
+
+#[derive(Debug, Clone)]
+pub(super) struct PomIndex {
+ deps: Vec,
+ repos: Vec,
+ /// The project's own ``.
+ repositories: Option,
+ /// The project's own `` and its ``.
+ dm: Option,
+ dm_deps: Option,
+ /// The `<` of ``.
+ project_close: usize,
+}
+
+/// The `` block the rewriter inserts (releases enabled with
+/// `checksumPolicy=fail` for the transport check against the served
+/// `.jar.sha1`; snapshots disabled).
+fn repository_block(id: &str, url: &str) -> String {
+ format!(
+ " \n {id}\n {url}\n \n true\n fail\n \n \n false\n \n "
+ )
+}
+
+/// The `` entry the rewriter inserts.
+fn managed_block(group: &str, artifact: &str, version: &str) -> String {
+ format!(
+ " \n {group}\n {artifact}\n {version}\n "
+ )
+}
+
+fn shift_pos(pos: &mut usize, at: usize, delta: isize) {
+ if *pos >= at {
+ *pos = pos
+ .checked_add_signed(delta)
+ .expect("offsets stay in the pom");
+ }
+}
+
+/// An end-side offset (just past a `>`) moves on a pure insertion only when
+/// the text lands strictly before it: text inserted right where an element
+/// ends lies outside it.
+fn shift_end(pos: &mut usize, at: usize, delta: isize, insertion: bool) {
+ if !insertion || *pos > at {
+ shift_pos(pos, at, delta);
+ }
+}
+
+fn shift_element(e: &mut Element, at: usize, delta: isize, insertion: bool) {
+ // A self-closed element's inner offsets sit at its end.
+ let self_closed = e.inner_start == e.end;
+ shift_pos(&mut e.start, at, delta);
+ if self_closed {
+ shift_end(&mut e.inner_start, at, delta, insertion);
+ shift_end(&mut e.inner_end, at, delta, insertion);
+ } else {
+ shift_pos(&mut e.inner_start, at, delta);
+ shift_pos(&mut e.inner_end, at, delta);
+ }
+ shift_end(&mut e.end, at, delta, insertion);
+}
+
+/// The element `…` whose open tag starts `text[at..]`, with
+/// offsets into `text` (the rewriter's own markup: no attributes, no
+/// comments).
+fn authored_element(text: &str, at: usize, name: &str) -> Element {
+ let open = format!("<{name}>");
+ let close = format!("{name}>");
+ debug_assert!(text[at..].starts_with(&open));
+ let inner_start = at + open.len();
+ let inner_end = inner_start
+ + text[inner_start..]
+ .find(&close)
+ .expect("authored markup closes");
+ Element {
+ start: at,
+ inner_start,
+ inner_end,
+ end: inner_end + close.len(),
+ }
+}
+
+impl PomIndex {
+ /// Fails (fail-closed) when the pom is not readable as one.
+ pub(super) fn build(pom: &str) -> Result {
+ let scope = PomScope::new(pom)?;
+ let masked = &scope.masked;
+ let deps = scope
+ .dependencies()?
+ .into_iter()
+ .map(|d| IndexedDep {
+ group: d.group,
+ artifact: d.artifact,
+ version_inner: d.version_inner,
+ version_text: d.version_text,
+ type_text: d.type_text,
+ classifier: d.classifier,
+ in_profile: d.in_profile,
+ })
+ .collect();
+ let mut repos = Vec::new();
+ for repo in scope.live("repository")? {
+ let url = children(masked, &repo, "url")?
+ .first()
+ .map(|u| (u.inner_start, u.inner_end));
+ repos.push(IndexedRepo {
+ id: child_text(repo.inner(masked), "id")?,
+ url_inner: url,
+ });
+ }
+ let dm = scope.live("dependencyManagement")?.first().copied();
+ let dm_deps = match &dm {
+ Some(dm) => children(masked, dm, "dependencies")?.first().copied(),
+ None => None,
+ };
+ Ok(PomIndex {
+ deps,
+ repos,
+ repositories: scope.live("repositories")?.first().copied(),
+ dm,
+ dm_deps,
+ project_close: scope.project_close().ok_or("no tag")?,
+ })
+ }
+
+ /// The indices of the `` elements naming `group:artifact`.
+ pub(super) fn matches(&self, group: &str, artifact: &str) -> Vec {
+ (0..self.deps.len())
+ .filter(|&i| {
+ self.deps[i].group.as_deref() == Some(group)
+ && self.deps[i].artifact.as_deref() == Some(artifact)
+ })
+ .collect()
+ }
+
+ pub(super) fn dep(&self, i: usize) -> &IndexedDep {
+ &self.deps[i]
+ }
+
+ /// `(id, url)` of each live repository that has an ``.
+ pub(super) fn repository_ids_and_urls(&self, pom: &str) -> Vec<(String, Option)> {
+ self.repos
+ .iter()
+ .filter_map(|r| {
+ let id = r.id.clone()?;
+ let url = r.url_inner.map(|(s, e)| pom[s..e].trim().to_string());
+ Some((id, url))
+ })
+ .collect()
+ }
+
+ /// Whether a live repository has the `` `id`.
+ pub(super) fn has_repository(&self, id: &str) -> bool {
+ self.repos.iter().any(|r| r.id.as_deref() == Some(id))
+ }
+
+ /// Every offset at or past `at` moves by `delta` (on a pure
+ /// `insertion`, an element end exactly at `at` stays: see [`shift_end`]).
+ fn shift(&mut self, at: usize, delta: isize, insertion: bool) {
+ for d in &mut self.deps {
+ if let Some((s, e)) = &mut d.version_inner {
+ shift_pos(s, at, delta);
+ shift_pos(e, at, delta);
+ }
+ }
+ for r in &mut self.repos {
+ if let Some((s, e)) = &mut r.url_inner {
+ shift_pos(s, at, delta);
+ shift_pos(e, at, delta);
+ }
+ }
+ for e in [&mut self.repositories, &mut self.dm, &mut self.dm_deps]
+ .into_iter()
+ .flatten()
+ {
+ shift_element(e, at, delta, insertion);
+ }
+ shift_pos(&mut self.project_close, at, delta);
+ }
+
+ /// `pom[start..end]` replaced by `text`, offsets kept in step.
+ fn splice(&mut self, pom: &mut String, start: usize, end: usize, text: &str) {
+ pom.replace_range(start..end, text);
+ let delta = text.len() as isize - (end - start) as isize;
+ if start == end {
+ self.shift(start, delta, true);
+ } else {
+ self.shift(end, delta, false);
+ }
+ }
+
+ fn anchor_mut(&mut self, anchor: Anchor) -> &mut Option {
+ match anchor {
+ Anchor::Repositories => &mut self.repositories,
+ Anchor::Dm => &mut self.dm,
+ Anchor::DmDeps => &mut self.dm_deps,
+ }
+ }
+
+ /// `text` inserted at the inner start of `anchor` (its first content),
+ /// which keeps its inner start.
+ fn insert_first(&mut self, pom: &mut String, anchor: Anchor, text: &str) -> usize {
+ let at = self.anchor_mut(anchor).expect("anchor present").inner_start;
+ self.splice(pom, at, at, text);
+ if let Some(e) = self.anchor_mut(anchor).as_mut() {
+ e.inner_start = at;
+ }
+ at
+ }
+
+ /// Rewrite dependency `i`'s literal version to `version`.
+ pub(super) fn set_version(&mut self, pom: &mut String, i: usize, version: &str) {
+ let (s, e) = self.deps[i].version_inner.expect("a literal version");
+ self.splice(pom, s, e, version);
+ self.deps[i].version_inner = Some((s, s + version.len()));
+ self.deps[i].version_text = Some(version.to_string());
+ }
+
+ /// Register the repository element the rewriter placed at `at`.
+ fn add_repo(&mut self, pom: &str, at: usize) {
+ let repo = authored_element(pom, at, "repository");
+ let body = repo.inner(pom);
+ let url = elements(body, "url")
+ .ok()
+ .and_then(|u| u.first().copied())
+ .map(|u| {
+ (
+ repo.inner_start + u.inner_start,
+ repo.inner_start + u.inner_end,
+ )
+ });
+ self.repos.push(IndexedRepo {
+ id: child_text(body, "id").ok().flatten(),
+ url_inner: url,
+ });
+ }
+
+ /// Insert the socket-patch ``: first in the project's own
+ /// `` (so it is consulted before the project's other
+ /// repositories; a self-closed `` is expanded in place),
+ /// else in a new `` section right before ``.
+ pub(super) fn insert_repository(&mut self, pom: &mut String, id: &str, url: &str) {
+ let block = repository_block(id, url);
+ match self.repositories {
+ Some(repos) if repos.inner_start == repos.end => {
+ let text = format!("\n{block}\n ");
+ self.splice(pom, repos.start, repos.end, &text);
+ self.repositories = Some(authored_element(pom, repos.start, "repositories"));
+ self.add_repo(pom, repos.start + "\n".len() + 4);
+ }
+ Some(_) => {
+ let at = self.insert_first(pom, Anchor::Repositories, &format!("\n{block}"));
+ self.add_repo(pom, at + 1 + 4);
+ }
+ None => {
+ let at = self.project_close;
+ let text = format!(" \n{block}\n \n");
+ self.splice(pom, at, at, &text);
+ self.repositories = Some(authored_element(pom, at + 2, "repositories"));
+ self.add_repo(pom, at + " \n".len() + 4);
+ }
+ }
+ }
+
+ /// Register the managed `` inserted at `at`.
+ fn add_managed(&mut self, pom: &str, at: usize, group: &str, artifact: &str, version: &str) {
+ let dep = authored_element(pom, at, "dependency");
+ let v = dep.inner_start
+ + dep.inner(pom).find("").expect("authored")
+ + "".len();
+ self.deps.push(IndexedDep {
+ group: Some(group.to_string()),
+ artifact: Some(artifact.to_string()),
+ version_inner: Some((v, v + version.len())),
+ version_text: Some(version.to_string()),
+ type_text: None,
+ classifier: None,
+ in_profile: false,
+ });
+ }
+
+ /// Add a `` version pin: right after the opening
+ /// `` of the project's own ``
+ /// (expanding a self-closed `` or
+ /// ``, adding `` when the section has
+ /// none), else in a new section right before ``.
+ pub(super) fn insert_dependency_management(
+ &mut self,
+ pom: &mut String,
+ group: &str,
+ artifact: &str,
+ version: &str,
+ ) {
+ let block = managed_block(group, artifact, version);
+ let dependencies = format!("\n{block}\n ");
+ // The `` sits this far into `…`.
+ let entry = "\n".len() + 6;
+ let Some(dm) = self.dm else {
+ let at = self.project_close;
+ let text = format!(
+ " \n {dependencies}\n \n"
+ );
+ self.splice(pom, at, at, &text);
+ self.dm = Some(authored_element(pom, at + 2, "dependencyManagement"));
+ let deps_at = at + " \n ".len();
+ self.dm_deps = Some(authored_element(pom, deps_at, "dependencies"));
+ self.add_managed(pom, deps_at + entry, group, artifact, version);
+ return;
+ };
+ if dm.inner_start == dm.end {
+ let text =
+ format!("\n {dependencies}\n ");
+ self.splice(pom, dm.start, dm.end, &text);
+ self.dm = Some(authored_element(pom, dm.start, "dependencyManagement"));
+ let deps_at = dm.start + "\n ".len();
+ self.dm_deps = Some(authored_element(pom, deps_at, "dependencies"));
+ self.add_managed(pom, deps_at + entry, group, artifact, version);
+ return;
+ }
+ match self.dm_deps {
+ Some(deps) if deps.inner_start == deps.end => {
+ self.splice(pom, deps.start, deps.end, &dependencies);
+ self.dm_deps = Some(authored_element(pom, deps.start, "dependencies"));
+ self.add_managed(pom, deps.start + entry, group, artifact, version);
+ }
+ Some(_) => {
+ let at = self.insert_first(pom, Anchor::DmDeps, &format!("\n{block}"));
+ self.add_managed(pom, at + 1 + 6, group, artifact, version);
+ }
+ None => {
+ let at = self.insert_first(pom, Anchor::Dm, &format!("\n {dependencies}"));
+ let deps_at = at + "\n ".len();
+ self.dm_deps = Some(authored_element(pom, deps_at, "dependencies"));
+ self.add_managed(pom, deps_at + entry, group, artifact, version);
+ }
+ }
+ }
+
+ /// Remove the rewriter's `` with `` `id` (see
+ /// [`super::remove_maven_repository`]); `false` when it is not there on
+ /// lines of its own.
+ pub(super) fn remove_repository(&mut self, pom: &mut String, id: &str) -> bool {
+ let Some((cut, end)) = super::maven_repository_removal(pom, id) else {
+ return false;
+ };
+ self.repos.retain(|r| {
+ r.url_inner.is_none_or(|(s, _)| s < cut || s >= end) && r.id.as_deref() != Some(id)
+ });
+ self.splice(pom, cut, end, "");
+ true
+ }
+
+ /// Point the one live repository with `` `id` at `url`; `false`
+ /// when there is no such single repository, it has no ``, or it
+ /// already points there.
+ pub(super) fn refresh_repository_url(&mut self, pom: &mut String, id: &str, url: &str) -> bool {
+ let mut ours = self
+ .repos
+ .iter()
+ .enumerate()
+ .filter(|(_, r)| r.id.as_deref() == Some(id));
+ let (Some((i, repo)), None) = (ours.next(), ours.next()) else {
+ return false;
+ };
+ let Some((s, e)) = repo.url_inner else {
+ return false;
+ };
+ if pom[s..e].trim() == url {
+ return false;
+ }
+ self.splice(pom, s, e, url);
+ self.repos[i].url_inner = Some((s, s + url.len()));
+ true
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// After any sequence of the rewriter's edits, the kept index is the
+ /// index a fresh build of the edited pom reads.
+ fn assert_in_step(index: &PomIndex, pom: &str) {
+ let fresh = PomIndex::build(pom).expect("the edited pom reads");
+ let key = |i: &PomIndex| {
+ let mut deps: Vec<_> = i
+ .deps
+ .iter()
+ .map(|d| {
+ (
+ d.group.clone(),
+ d.artifact.clone(),
+ d.version_inner,
+ d.version_text.clone(),
+ )
+ })
+ .collect();
+ deps.sort();
+ let mut repos: Vec<_> = i
+ .repos
+ .iter()
+ .map(|r| (r.id.clone(), r.url_inner))
+ .collect();
+ repos.sort();
+ let el = |e: &Option| e.map(|e| (e.start, e.inner_start, e.inner_end, e.end));
+ (
+ deps,
+ repos,
+ el(&i.repositories),
+ el(&i.dm),
+ el(&i.dm_deps),
+ i.project_close,
+ )
+ };
+ assert_eq!(key(index), key(&fresh), "{pom}");
+ }
+
+ #[test]
+ fn edits_keep_the_index_in_step() {
+ for pom in [
+ "\n \n ga1\n \n\n",
+ "\n \n \n \n ga1\n \n\n",
+ "\n \n \n \n \n \n xu\n \n \n ga1\n \n\n",
+ "\n \n \n \n ga1\n \n\n",
+ ] {
+ let mut pom = pom.to_string();
+ let mut index = PomIndex::build(&pom).unwrap();
+ let [i] = index.matches("g", "a")[..] else {
+ panic!("one match");
+ };
+ index.set_version(&mut pom, i, "1-socket.12345678");
+ assert_in_step(&index, &pom);
+ for n in 0..2 {
+ index.insert_repository(&mut pom, &format!("r{n}"), &format!("https://h/{n}"));
+ assert_in_step(&index, &pom);
+ index.insert_dependency_management(&mut pom, "t", &format!("m{n}"), "2");
+ assert_in_step(&index, &pom);
+ }
+ assert!(index.refresh_repository_url(&mut pom, "r0", "https://h/rotated"));
+ assert_in_step(&index, &pom);
+ assert!(!index.refresh_repository_url(&mut pom, "r0", "https://h/rotated"));
+ assert_eq!(pom.matches("` lands outside a
+ /// self-closed section that ends there, so expanding that section
+ /// afterwards keeps the new one.
+ #[test]
+ fn insertion_after_a_self_closed_section_stays_outside_it() {
+ for tail in ["", ""] {
+ for dm_first in [true, false] {
+ let mut pom = format!(
+ "\n \n ga\n \n {tail}\n"
+ );
+ let mut index = PomIndex::build(&pom).unwrap();
+ if dm_first {
+ index.insert_dependency_management(&mut pom, "g", "a", "1-socket.1");
+ assert_in_step(&index, &pom);
+ }
+ index.insert_repository(&mut pom, "r", "https://h/r");
+ assert_in_step(&index, &pom);
+ if !dm_first {
+ index.insert_dependency_management(&mut pom, "g", "a", "1-socket.1");
+ assert_in_step(&index, &pom);
+ }
+ assert!(pom.contains("1-socket.1"), "{pom}");
+ assert!(pom.contains("r"), "{pom}");
+ assert_eq!(pom.matches(" String {
/// A `` block matched by groupId:artifactId, with the byte offsets
/// of its literal `` inner text (None when the dep carries no literal
-/// version — inherited/managed) and its trimmed version/type text. Mirrors the
-/// TS `MavenDependencyMatch`.
+/// version — inherited/managed) and its trimmed version/type/classifier text.
+/// Mirrors the TS `MavenDependencyMatch`.
struct MavenDependencyMatch {
+ /// The dependency's index in the [`maven_index::PomIndex`].
+ idx: usize,
version_inner: Option<(usize, usize)>,
version_text: Option,
type_text: Option,
+ classifier: Option,
+ in_profile: bool,
}
/// Inner-text byte range of the first `…` inside `pom[from, to)`, or
@@ -7405,35 +7410,38 @@ fn maven_tag_text_in(pom: &str, tag: &str, from: usize, to: usize) -> Option` block whose `` + `` match, with
-/// its literal `` range/text and `` text (twin of the TS
-/// `findDependencyMatches`). A `` inside ``
-/// is matched the same way as a direct one — the suffixing path tells "managed
-/// in an unseen parent" (no literal version → depMgmt pin) from "pinned here"
-/// (rewrite the literal) purely by whether ANY match carries a literal
-/// ``. Returns ALL matches so a managed base-version entry gets
-/// rewritten even when a direct dependency declares no version.
+/// Every `` whose `` + `` match, with its
+/// literal `` range/text, `` and `` (twin of the TS
+/// `findDependencyMatches`). Read through `PomScope` (see
+/// [`maven_index`]): markup inside comments, CDATA, `` / ``
+/// (plugin classpaths, which the project's `` does not
+/// reach and the Socket repository — not a `` — cannot
+/// serve) is no declaration. A match inside `` is returned flagged:
+/// Maven reads it only while that profile is active. A `` inside
+/// `` is matched the same way as a direct one — the
+/// suffixing path tells "managed in an unseen parent" (no literal version →
+/// depMgmt pin) from "pinned here" (rewrite the literal) purely by whether
+/// ANY match carries a literal ``.
fn find_maven_dependency_matches(
- pom: &str,
+ index: &maven_index::PomIndex,
group_id: &str,
artifact_id: &str,
) -> Vec {
- let mut matches = vec![];
- for m in MAVEN_DEPENDENCY_BLOCK_RE.find_iter(pom) {
- let (dep_open, dep_close) = (m.start(), m.end());
- let g = maven_tag_text_in(pom, "groupId", dep_open, dep_close);
- let a = maven_tag_text_in(pom, "artifactId", dep_open, dep_close);
- if g.as_deref() != Some(group_id) || a.as_deref() != Some(artifact_id) {
- continue;
- }
- let version_inner = maven_tag_inner_range(pom, "version", dep_open, dep_close);
- matches.push(MavenDependencyMatch {
- version_text: version_inner.map(|(s, e)| pom[s..e].trim().to_string()),
- version_inner,
- type_text: maven_tag_text_in(pom, "type", dep_open, dep_close),
- });
- }
- matches
+ index
+ .matches(group_id, artifact_id)
+ .into_iter()
+ .map(|idx| {
+ let d = index.dep(idx);
+ MavenDependencyMatch {
+ idx,
+ version_inner: d.version_inner,
+ version_text: d.version_text.clone(),
+ type_text: d.type_text.clone(),
+ classifier: d.classifier.clone(),
+ in_profile: d.in_profile,
+ }
+ })
+ .collect()
}
fn rewrite_maven_pom(
@@ -7489,10 +7497,14 @@ fn rewrite_maven_pom(
// One pass over the pom's repositories: `(id, url)` of each, which also
// answers the per-dep URL-refresh check below while the pom is still
// unchanged (a no-op rescan then never re-scans the pom per dep).
- let original_repos: Vec<(String, Option)> = pom
- .as_deref()
- .map(maven_repository_ids_and_urls)
- .unwrap_or_default();
+ // The hosted rewriter's view of the pom, read once (through `PomScope`)
+ // and kept in step with every edit below.
+ let mut index: Option> =
+ pom.as_deref().map(maven_index::PomIndex::build);
+ let original_repos: Vec<(String, Option)> = match (&index, pom.as_deref()) {
+ (Some(Ok(index)), Some(pom)) => index.repository_ids_and_urls(pom),
+ _ => Vec::new(),
+ };
let hosted_repo_generations: std::collections::BTreeSet = original_repos
.iter()
.filter_map(|(id, _)| generation::pin_name_uuid(id, false).map(str::to_string))
@@ -7550,6 +7562,94 @@ fn rewrite_maven_pom(
};
// Unique-per-patch repository id (valid chars: alnum, `-`, `_`, `.`).
let repo_id = generation::hosted_pin_name(&dep.patch_uuid);
+ // Every match and anchor is read through the pom's live scope; a pom
+ // that is not readable as one is never edited (fail-closed).
+ let idx = match index.as_mut().expect("a pom has an index") {
+ Ok(idx) => idx,
+ Err(e) => {
+ result.warnings.push(RewriteWarning {
+ code: "redirect_maven_pom_unreadable".into(),
+ detail: format!(
+ "pom.xml is not readable as a Maven pom ({e}); {group_id}:{artifact_id} skipped"
+ ),
+ });
+ continue;
+ }
+ };
+ let all_matches = find_maven_dependency_matches(idx, &group_id, &artifact_id);
+ // Only the GA's main jar is granted: the served maven2 tail holds
+ // `-.jar/.pom` and nothing else. A classifier variant
+ // (sources, tests, a native build) is its own artifact — managed and
+ // resolved apart from the main jar — so it is left alone, and the
+ // main jar's literal-vs-depMgmt decision is made without it.
+ let (variants, live): (Vec<_>, Vec<_>) = all_matches
+ .into_iter()
+ .partition(|m| m.classifier.is_some());
+ // A variant that carries code onto a classpath (tests, a native
+ // build, a JDK-specific jar) at the patched release would keep the
+ // unpatched bytes there while the GA counts as redirected and VEX
+ // attests it: the dep is skipped. `sources` / `javadoc` jars are
+ // never executed, so they only warn.
+ let mut executable_variant = false;
+ for variant in &variants {
+ let Some(v) = variant.version_text.as_deref() else {
+ continue;
+ };
+ let names_release = v == dep.version
+ || crate::formats::maven::split_socket_version(v)
+ .is_some_and(|(base, _)| base == dep.version);
+ if names_release {
+ let classifier = variant.classifier.as_deref().unwrap_or_default();
+ let executable = !matches!(classifier, "sources" | "javadoc");
+ executable_variant |= executable;
+ result.warnings.push(RewriteWarning {
+ code: "redirect_maven_classifier_unsupported".into(),
+ detail: format!(
+ "{group_id}:{artifact_id}:{classifier} {v} is a \
+ classifier variant; the Socket patch covers only the main jar, so it \
+ is left as-is{}",
+ if executable {
+ " and the dep is not redirected (that variant would keep the \
+ unpatched code on the classpath)"
+ } else {
+ ""
+ }
+ ),
+ });
+ }
+ }
+ if executable_variant {
+ continue;
+ }
+ // A `` declaration is read only while its profile is
+ // active, and then its own `` (a literal or a `${property}`)
+ // beats the top-level pin: it is never edited, and is reported as
+ // unpatched. When versioned profile declarations are the GA's only
+ // ones, nothing this pom always reads uses it, and a top-level pin
+ // would be inert exactly when the profile pulls it in: the dep is
+ // skipped rather than reported redirected. A versionless profile
+ // declaration takes the managed version, so the pin covers it.
+ let (profiled, matches): (Vec<_>, Vec<_>) = live.into_iter().partition(|m| m.in_profile);
+ let profile_version = profiled.iter().find_map(|m| m.version_text.as_deref());
+ if let Some(profile_version) = profile_version {
+ result.warnings.push(RewriteWarning {
+ code: "redirect_maven_profile_dependency_unpatched".into(),
+ detail: format!(
+ "{group_id}:{artifact_id} {profile_version} inside \
+ is not edited; while that profile is active it resolves that version, not \
+ the patched artifact{}",
+ if matches.is_empty() {
+ " (no declaration outside , so nothing is pinned)"
+ } else {
+ ""
+ }
+ ),
+ });
+ if matches.is_empty() {
+ continue;
+ }
+ }
+ let has_live_repo = idx.has_repository(&repo_id);
// LEGACY same-GAV fallback: no suffixed version means the patched jar is
// served under its original GAV. Add the repository (transport checksum
@@ -7557,7 +7657,6 @@ fn rewrite_maven_pom(
let Some(suffixed_version) = suffixed_version else {
// Verify-only inspection: warn when the redirect can't take effect.
// Only the FIRST match matters here (legacy behavior).
- let matches = find_maven_dependency_matches(pom_text, &group_id, &artifact_id);
match matches.first() {
None => {
result.warnings.push(RewriteWarning {
@@ -7608,10 +7707,10 @@ fn rewrite_maven_pom(
"{group_id}:{artifact_id} is patched at its original GAV; a Socket-repo failure falls back to the unpatched artifact — not fail-closed. The backend will serve suffixed versions once the upstream pom is available."
),
});
- if pom_text.contains(&format!("{repo_id}")) {
+ if has_live_repo {
continue;
}
- *pom_text = insert_maven_repository(pom_text, &repo_id, &ov.index_url);
+ idx.insert_repository(pom_text, &repo_id, &ov.index_url);
pom_changed = true;
result.edits.push(FileEdit {
path: "pom.xml".into(),
@@ -7627,7 +7726,6 @@ fn rewrite_maven_pom(
// FAIL-CLOSED: pin the suffixed version explicitly. Scan every matching
// , tracking depMgmt containment via the version presence
// so we can tell a literal pin here from a version managed elsewhere.
- let matches = find_maven_dependency_matches(pom_text, &group_id, &artifact_id);
// An unsupported on any match: the single-jar repo can't serve
// it — skip the whole dep (no version edit, no repo, no checksum).
@@ -7663,13 +7761,13 @@ fn rewrite_maven_pom(
}
let mut pin_landed = false;
- // Literal versions among the matches, with their inner ranges.
+ // Literal versions among the matches: (index, inner start, text).
let versioned: Vec<(usize, usize, String)> = matches
.iter()
.filter_map(|m| {
m.version_inner
.zip(m.version_text.clone())
- .map(|((s, e), v)| (s, e, v))
+ .map(|((s, _), v)| (m.idx, s, v))
})
.collect();
// A `-socket.` literal for this release that is not the
@@ -7685,17 +7783,17 @@ fn rewrite_maven_pom(
.any(|uuid| uuid.starts_with(hex8))
})
};
- // Rewrite base (or a prior generation) → suffixed. Descending offset
- // order so earlier edits don't shift later matches' offsets.
+ // Rewrite base (or a prior generation) → suffixed, in descending
+ // offset order (the index keeps every other offset in step).
let mut to_rewrite: Vec<(usize, usize, String)> = versioned
.iter()
.filter(|(_, _, v)| *v == dep.version || prior_generation(v))
- .map(|(s, e, v)| (*s, *e, v.clone()))
+ .cloned()
.collect();
- to_rewrite.sort_by(|a, b| b.0.cmp(&a.0));
+ to_rewrite.sort_by(|a, b| b.1.cmp(&a.1));
let mut superseded_versions: Vec = vec![];
- for (start, end, was) in &to_rewrite {
- pom_text.replace_range(*start..*end, &suffixed_version);
+ for (i, _, was) in &to_rewrite {
+ idx.set_version(pom_text, *i, &suffixed_version);
pom_changed = true;
pin_landed = true;
if *was != dep.version && !superseded_versions.contains(was) {
@@ -7732,12 +7830,7 @@ fn rewrite_maven_pom(
// as a versioned match, so `versioned` is non-empty and this branch is
// skipped (idempotent).
if versioned.is_empty() {
- *pom_text = insert_maven_dependency_management(
- pom_text,
- &group_id,
- &artifact_id,
- &suffixed_version,
- );
+ idx.insert_dependency_management(pom_text, &group_id, &artifact_id, &suffixed_version);
pom_changed = true;
pin_landed = true;
result.edits.push(FileEdit {
@@ -7789,8 +7882,7 @@ fn rewrite_maven_pom(
continue;
}
let old_id = generation::hosted_pin_name(&old_uuid);
- if let Some(next) = remove_maven_repository(pom_text, &old_id) {
- *pom_text = next;
+ if idx.remove_repository(pom_text, &old_id) {
pom_changed = true;
result.edits.push(FileEdit {
path: "pom.xml".into(),
@@ -7816,31 +7908,18 @@ fn rewrite_maven_pom(
// A rotated grant token keeps the uuid but changes the repository
// URL: refresh the existing `socket-patch-` repository in place
// so the pin keeps resolving through the current grant.
- // While the pom is unchanged, `original_repos` already says whether
- // the refresh would change anything (exactly one such repository,
- // with a `` that differs); skip the scan when it would not.
- let refresh_may_apply = pom_changed || {
- let mut ours = original_repos.iter().filter(|(id, _)| *id == repo_id);
- match (ours.next(), ours.next()) {
- (Some((_, Some(url))), None) => *url != ov.index_url,
- _ => false,
- }
- };
- if refresh_may_apply
- && (pin_landed || versioned.iter().any(|(_, _, v)| *v == suffixed_version))
+ if (pin_landed || versioned.iter().any(|(_, _, v)| *v == suffixed_version))
+ && idx.refresh_repository_url(pom_text, &repo_id, &ov.index_url)
{
- if let Some(next) = refresh_maven_repository_url(pom_text, &repo_id, &ov.index_url) {
- *pom_text = next;
- pom_changed = true;
- result.edits.push(FileEdit {
- path: "pom.xml".into(),
- kind: "redirect_maven_repository".into(),
- action: "rewritten".into(),
- key: Some(repo_id.clone()),
- original: None,
- new: Some(json!({ "id": repo_id, "url": ov.index_url })),
- });
- }
+ pom_changed = true;
+ result.edits.push(FileEdit {
+ path: "pom.xml".into(),
+ kind: "redirect_maven_repository".into(),
+ action: "rewritten".into(),
+ key: Some(repo_id.clone()),
+ original: None,
+ new: Some(json!({ "id": repo_id, "url": ov.index_url })),
+ });
}
// A pin landed this run: inject the repository (idempotent via the
@@ -7850,8 +7929,8 @@ fn rewrite_maven_pom(
if !pin_landed {
continue;
}
- if !pom_text.contains(&format!("{repo_id}")) {
- *pom_text = insert_maven_repository(pom_text, &repo_id, &ov.index_url);
+ if !idx.has_repository(&repo_id) {
+ idx.insert_repository(pom_text, &repo_id, &ov.index_url);
pom_changed = true;
result.edits.push(FileEdit {
path: "pom.xml".into(),
@@ -7994,26 +8073,14 @@ static MAVEN_REPOSITORY_RE: LazyLock = LazyLock::new(|| {
Regex::new(r"(?s).*?").expect("static repository regex is valid")
});
-/// The trimmed `` and `` texts of every `` element of
-/// `pom` that has an ``, in document order: the same elements, ids and
-/// URLs [`maven_repositories_with_id`] and [`refresh_maven_repository_url`]
-/// read, in one pass.
-fn maven_repository_ids_and_urls(pom: &str) -> Vec<(String, Option)> {
- MAVEN_REPOSITORY_RE
- .find_iter(pom)
- .filter_map(|m| {
- let id = maven_tag_text_in(pom, "id", m.start(), m.end())?;
- let url = maven_tag_text_in(pom, "url", m.start(), m.end());
- Some((id, url))
- })
- .collect()
-}
-
/// The `` elements of `pom` whose `` is `id`, as byte spans.
+/// A commented-out twin is not one: the rewriter writes a live repository
+/// beside it, and rollback must still find exactly that one.
pub(crate) fn maven_repositories_with_id(pom: &str, id: &str) -> Vec<(usize, usize)> {
+ let masked = crate::formats::xml::blank_non_markup(pom).unwrap_or_else(|_| pom.to_string());
MAVEN_REPOSITORY_RE
- .find_iter(pom)
- .filter(|m| maven_tag_text_in(pom, "id", m.start(), m.end()).as_deref() == Some(id))
+ .find_iter(&masked)
+ .filter(|m| maven_tag_text_in(&masked, "id", m.start(), m.end()).as_deref() == Some(id))
.map(|m| (m.start(), m.end()))
.collect()
}
@@ -8023,6 +8090,12 @@ pub(crate) fn maven_repositories_with_id(pom: &str, id: &str) -> Vec<(usize, usi
/// one such element sits on lines of its own (anything else is not the
/// rewriter's insertion, and stays).
pub(crate) fn remove_maven_repository(pom: &str, id: &str) -> Option {
+ let (cut, end) = maven_repository_removal(pom, id)?;
+ Some(format!("{}{}", &pom[..cut], &pom[end..]))
+}
+
+/// The byte range [`remove_maven_repository`] removes.
+pub(crate) fn maven_repository_removal(pom: &str, id: &str) -> Option<(usize, usize)> {
let [(start, end)] = maven_repositories_with_id(pom, id)[..] else {
return None;
};
@@ -8037,68 +8110,7 @@ pub(crate) fn remove_maven_repository(pom: &str, id: &str) -> Option {
} else {
line_start
};
- Some(format!("{}{}", &pom[..cut], &pom[end..]))
-}
-
-/// `pom` with the `` of its one `` whose `` is `id` set
-/// to `url`; `None` when there is no such single element or it already
-/// points there.
-fn refresh_maven_repository_url(pom: &str, id: &str, url: &str) -> Option {
- let [(start, end)] = maven_repositories_with_id(pom, id)[..] else {
- return None;
- };
- let (s, e) = maven_tag_inner_range(pom, "url", start, end)?;
- if pom[s..e].trim() == url {
- return None;
- }
- let mut out = pom.to_string();
- out.replace_range(s..e, url);
- Some(out)
-}
-
-/// Insert the socket-patch `` block: releases enabled with
-/// `fail` (the transport-level check against
-/// the served `.jar.sha1`); snapshots disabled (patched artifacts are always
-/// released versions). Prefer an existing `` element (single
-/// replace, inserted first so it's consulted before the project's other
-/// repositories); otherwise author a full `` section immediately
-/// before the closing ``. `` is matched exactly so it
-/// never collides with ``.
-fn insert_maven_repository(pom: &str, id: &str, url: &str) -> String {
- let block = format!(
- " \n {id}\n {url}\n \n true\n fail\n \n \n false\n \n "
- );
- if pom.contains("") {
- return pom.replacen("", &format!("\n{block}"), 1);
- }
- let section = format!(" \n{block}\n ");
- pom.replacen("", &format!("{section}\n"), 1)
-}
-
-/// Add a `` version pin. Prefer extending an existing
-/// `` element (insert right after the
-/// opening `` tag); otherwise author a full
-/// `` section before ``. Mirrors the TS
-/// `insertDependencyManagement`.
-fn insert_maven_dependency_management(
- pom: &str,
- group_id: &str,
- artifact_id: &str,
- version: &str,
-) -> String {
- let block = format!(
- " \n {group_id}\n {artifact_id}\n {version}\n "
- );
- let dm_re = Regex::new(r"(?s)\s*")
- .expect("static dependencyManagement regex is valid");
- if let Some(m) = dm_re.find(pom) {
- let matched = m.as_str();
- return pom.replacen(matched, &format!("{matched}\n{block}"), 1);
- }
- let section = format!(
- " \n \n{block}\n \n "
- );
- pom.replacen("", &format!("{section}\n"), 1)
+ Some((cut, end))
}
/// Merge trusted-checksums resolver args into `.mvn/maven.config` (one arg per
@@ -8757,6 +8769,80 @@ mod tests {
r.warnings.iter().map(|w| w.code.as_str()).collect()
}
+ /// A profile's own `` (a property or any literal) beats the
+ /// top-level pin while the profile is active: when the profile holds the
+ /// GA's only versioned declaration nothing is pinned, and beside a
+ /// top-level declaration it is reported unpatched.
+ #[test]
+ fn maven_versioned_profile_declarations_are_never_pinned_over() {
+ let profile = |version: &str| {
+ format!(
+ " \n \n p\n \n \n org.slf4j\n slf4j-api\n {version}\n \n \n \n \n\n"
+ )
+ };
+ for version in ["${slf4j.version}", "1.7.30", "1.7.36"] {
+ let only = pom_with_dep("", "")
+ .replace(
+ " \n \n org.slf4j\n slf4j-api\n \n \n",
+ "",
+ )
+ .replace("\n", &profile(version));
+ assert!(!only.contains("\n "), "{only}");
+ let files = BTreeMap::from([("pom.xml".to_string(), only)]);
+ let r = rewrite_registry_redirect(&files, &[maven_override()]);
+ assert!(r.files.is_empty(), "{version}: {:?}", r.files);
+ assert!(
+ warning_codes(&r).contains(&"redirect_maven_profile_dependency_unpatched"),
+ "{version}: {:?}",
+ r.warnings
+ );
+
+ let both = pom_with_dep("\n 1.7.36", "")
+ .replace("\n", &profile(version));
+ let files = BTreeMap::from([("pom.xml".to_string(), both)]);
+ let r = rewrite_registry_redirect(&files, &[maven_override()]);
+ let out = r.files.get("pom.xml").expect("top-level pinned");
+ assert!(
+ out.contains(&format!("{version}")),
+ "{out}"
+ );
+ assert!(
+ warning_codes(&r).contains(&"redirect_maven_profile_dependency_unpatched"),
+ "{version}: {:?}",
+ r.warnings
+ );
+ }
+ }
+
+ /// A classifier variant at the patched release that carries code
+ /// (`tests`, a native build) keeps the unpatched bytes on a classpath,
+ /// so the dep is not redirected; a `sources` / `javadoc` jar only warns.
+ #[test]
+ fn maven_executable_classifier_variant_blocks_the_redirect() {
+ let variant = |classifier: &str| {
+ pom_with_dep("\n 1.7.36", "").replace(
+ " ",
+ &format!(" \n org.slf4j\n slf4j-api\n 1.7.36\n {classifier}\n \n "),
+ )
+ };
+ for classifier in ["tests", "linux-x86_64"] {
+ let files = BTreeMap::from([("pom.xml".to_string(), variant(classifier))]);
+ let r = rewrite_registry_redirect(&files, &[maven_override()]);
+ assert!(r.files.is_empty(), "{classifier}: {:?}", r.files);
+ assert!(warning_codes(&r).contains(&"redirect_maven_classifier_unsupported"));
+ }
+ for classifier in ["sources", "javadoc"] {
+ let files = BTreeMap::from([("pom.xml".to_string(), variant(classifier))]);
+ let r = rewrite_registry_redirect(&files, &[maven_override()]);
+ let out = r.files.get("pom.xml").expect("main jar pinned");
+ assert!(
+ out.contains(&format!("{MAVEN_SUFFIXED}")),
+ "{out}"
+ );
+ assert!(warning_codes(&r).contains(&"redirect_maven_classifier_unsupported"));
+ }
+ }
+
/// Fail-closed literal pin: the `` is rewritten to the suffixed
/// value, the repository + trusted-checksum files are emitted, and a re-run
/// over the fully-pinned output records nothing (idempotent).
diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/maven.rs b/crates/socket-patch-core/src/patch/redirect/upstream/maven.rs
index df9a89276..f0298ecb7 100644
--- a/crates/socket-patch-core/src/patch/redirect/upstream/maven.rs
+++ b/crates/socket-patch-core/src/patch/redirect/upstream/maven.rs
@@ -77,8 +77,11 @@ fn dep_matches(pom: &str, group: &str, artifact: &str) -> Vec {
/// Is the block at `start` in the rewriter's insertion position: right
/// after ``, with only authored
-/// Socket-pinned entries between?
-fn after_dm_open(pom: &str, start: usize) -> bool {
+/// Socket-pinned entries between? `masked` is the pom with comments blanked
+/// (offsets kept), so a comment between the two tags — which the rewriter
+/// inserts past — reads as the whitespace it is to Maven.
+fn after_dm_open(masked: &str, start: usize) -> bool {
+ let pom = masked;
let open = Regex::new(r"(?s)\s*\z")
.expect("static dependencyManagement-open regex is valid");
let authored = Regex::new(
@@ -151,17 +154,42 @@ fn restore_pin(
"pom.xml declares no {group}:{artifact} {suffixed}"
));
}
- let versioned = matches.iter().filter(|m| m.version.is_some()).count();
+ // The GA's literal versions where Maven reads them for the main jar:
+ // not commented out, outside `` / `` / ``,
+ // no classifier — the same scope the rewriter decides "transitive" by.
let sections = dm_sections(&text);
let managed = |pos: usize| sections.iter().any(|(s, e)| pos >= *s && pos < *e);
- // A versionless direct declaration with nothing but this pom's own
- // `` to manage it: that entry is the user's.
- let entry_is_users = matches
- .iter()
- .any(|m| m.version.is_none() && !managed(m.start))
- && !text.contains("")
- && !text.contains("import");
+ let scoped = crate::formats::maven::PomScope::new(&text).and_then(|s| s.dependencies());
+ // In the same scope: a versionless direct declaration with nothing but
+ // this pom's own `` to manage it means that entry
+ // is the user's (a commented, plugin, profile or classifier sibling is
+ // not such a declaration).
+ let (versioned, versionless_direct) = match &scoped {
+ Ok(deps) => {
+ let main = || {
+ deps.iter().filter(|d| {
+ d.group.as_deref() == Some(group)
+ && d.artifact.as_deref() == Some(artifact)
+ && !d.in_profile
+ && d.classifier.is_none()
+ })
+ };
+ (
+ main().filter(|d| d.version_inner.is_some()).count(),
+ main().any(|d| d.version_inner.is_none() && !managed(d.element.start)),
+ )
+ }
+ Err(_) => (
+ matches.iter().filter(|m| m.version.is_some()).count(),
+ matches
+ .iter()
+ .any(|m| m.version.is_none() && !managed(m.start)),
+ ),
+ };
+ let entry_is_users =
+ versionless_direct && !text.contains("") && !text.contains("import");
+ let masked = crate::formats::xml::blank_non_markup(&text).unwrap_or_else(|_| text.clone());
let mut edits: Vec<(usize, usize, String)> = Vec::new();
for m in matches
.iter()
@@ -170,7 +198,7 @@ fn restore_pin(
let authored = versioned == 1
&& !entry_is_users
&& text[m.start..m.end] == authored_dm_block(group, artifact, &suffixed)
- && after_dm_open(&text, m.start);
+ && after_dm_open(&masked, m.start);
if authored {
edits.push((m.start - DM_ENTRY_LEAD.len(), m.end, String::new()));
} else {
@@ -185,14 +213,16 @@ fn restore_pin(
text.replace_range(s..e, &with);
}
- if text.contains(&suffixed) {
+ // A comment naming the pin (a commented-out earlier wiring) is inert.
+ let live = crate::formats::xml::blank_non_markup(&text).unwrap_or_else(|_| text.clone());
+ if live.contains(&suffixed) {
return Err(format!(
"pom.xml still names {suffixed} outside a version (a property or \
plugin configuration socket-patch did not write)"
));
}
let name = generation::hosted_pin_name(uuid);
- if text.contains(&name) {
+ if live.contains(&name) {
return Err(format!("pom.xml still names {name}"));
}
Ok(text)
@@ -436,6 +466,129 @@ mod tests {
}
}
+ /// The scope-aware rewrites (#259, #262, #342) restore to their input:
+ /// a comment, profile or classifier sibling the rewriter left alone is
+ /// left alone again, and a pin added past a comment inside
+ /// `` is still recognised as the rewriter's own.
+ #[tokio::test]
+ async fn scoped_rewrites_round_trip() {
+ for case in [
+ "comment-dependency",
+ "profile-repositories",
+ "plugin-dependency",
+ "profile-depmgmt",
+ "commented-repositories",
+ "classifier-sources-sibling",
+ "classifier-only-transitive-main",
+ "depmgmt-comment",
+ ] {
+ let (outcome, after) = run(&[("pom.xml", fixture(case, "expected", "pom.xml"))]).await;
+ assert!(
+ matches!(outcome.pins[0].status, PinStatus::Restored),
+ "{case}: {:?}",
+ outcome.pins[0].status
+ );
+ assert_eq!(
+ after["pom.xml"],
+ fixture(case, "input", "pom.xml"),
+ "{case}"
+ );
+ }
+ }
+
+ /// A versionless sibling Maven does not read for the main jar (in a
+ /// comment, a plugin, a profile, or with a classifier) does not make the
+ /// rewriter's own `` pin look user-written: the
+ /// pin is removed, not rewritten to the base version.
+ #[tokio::test]
+ async fn versionless_out_of_scope_siblings_keep_the_pin_authored() {
+ let sibling = "org.slf4jslf4j-api";
+ for extra in [
+ format!(""),
+ format!("{sibling}sources"),
+ format!(
+ "p{sibling}"
+ ),
+ format!(
+ "x{sibling}"
+ ),
+ ] {
+ // Beside the project's own (first) ``: inside it
+ // for a dependency, right after it otherwise.
+ let with = |mut pom: String| {
+ let close = " \n";
+ let at = pom.find(close).expect("project dependencies");
+ if extra.starts_with("") {
+ pom.insert_str(at, &format!(" {extra}\n"));
+ } else {
+ pom.insert_str(at + close.len(), &format!(" {extra}\n"));
+ }
+ pom
+ };
+ let input = with(fixture("transitive-depmgmt", "input", "pom.xml"));
+ let expected = with(fixture("transitive-depmgmt", "expected", "pom.xml"));
+ let (outcome, after) = run(&[("pom.xml", expected)]).await;
+ assert!(
+ matches!(outcome.pins[0].status, PinStatus::Restored),
+ "{extra}: {:?}",
+ outcome.pins[0].status
+ );
+ assert_eq!(after["pom.xml"], input, "{extra}");
+ }
+ }
+
+ /// A commented-out twin of the Socket repository (an earlier wiring the
+ /// user commented out) does not block rollback of the live one the
+ /// rewriter wrote beside it, and stays as it is.
+ #[tokio::test]
+ async fn a_commented_repository_twin_does_not_block_restore() {
+ let twin = |pom: String| {
+ pom.replacen(
+ "old-mirror",
+ &format!("socket-patch-{UUID}"),
+ 1,
+ )
+ };
+ let (outcome, after) = run(&[(
+ "pom.xml",
+ twin(fixture("commented-repositories", "expected", "pom.xml")),
+ )])
+ .await;
+ assert!(
+ matches!(outcome.pins[0].status, PinStatus::Restored),
+ "{:?}",
+ outcome.pins[0].status
+ );
+ assert_eq!(
+ after["pom.xml"],
+ twin(fixture("commented-repositories", "input", "pom.xml"))
+ );
+ }
+
+ /// An expanded self-closed section restores to an empty (still single)
+ /// section: no Socket markup left, and a pom Maven reads.
+ #[tokio::test]
+ async fn expanded_self_closed_sections_restore_to_one_empty_section() {
+ for (case, tag) in [
+ ("self-closed-repositories", ""),
+ ("self-closed-depmgmt", ""),
+ ] {
+ let (outcome, after) = run(&[("pom.xml", fixture(case, "expected", "pom.xml"))]).await;
+ assert!(
+ matches!(outcome.pins[0].status, PinStatus::Restored),
+ "{case}: {:?}",
+ outcome.pins[0].status
+ );
+ let pom = &after["pom.xml"];
+ assert!(
+ !pom.contains("-socket.") && !pom.contains("socket-patch-"),
+ "{case}: {pom}"
+ );
+ assert_eq!(pom.matches(tag).count(), 1, "{case}: {pom}");
+ crate::formats::maven::parse_pom(pom).expect("restored pom reads");
+ }
+ }
+
#[tokio::test]
async fn refusals_change_nothing() {
let hosted = fixture("basic", "expected", "pom.xml");
diff --git a/crates/socket-patch-core/src/vex/discover/maven.rs b/crates/socket-patch-core/src/vex/discover/maven.rs
index 21ea749c5..bac874a30 100644
--- a/crates/socket-patch-core/src/vex/discover/maven.rs
+++ b/crates/socket-patch-core/src/vex/discover/maven.rs
@@ -44,7 +44,12 @@
//! A hosted pin in a reactor root (`` / ``) stays
//! a ref (rollback / remove find it) but marked unattested: a module's
//! own literal `` overrides it, and only the root pom is read here
-//! (the rewriter refuses such roots, #261). A `${property}`
+//! (the rewriter refuses such roots, #261). A `` copy of the
+//! GA is its own coordinate, which the classifier-less pin does not
+//! retarget and the grant does not serve: a live executable one (anything
+//! but `sources` / `javadoc`) that resolves any version other than the pin
+//! — its own literal, a classifier-keyed managed entry, or none in this
+//! pom — keeps the ref but marks it unattested. A `${property}`
//! version is resolved one level from the root ``.
//!
//! Integrity: when the pom sha256 was known the rewriter also writes Maven
@@ -283,6 +288,26 @@ async fn extract_hosted(
UnattestedKind::MavenReactorRoot,
);
}
+ // An executable classifier copy (tests, a native build) is
+ // its own coordinate: the classifier-less pin does not
+ // retarget it and the grant does not serve it, so Maven
+ // fetches it from the next repository — the public release.
+ if let Some((classifier, version)) =
+ unpatched_executable_classifier(&pom.deps, group, artifact, &pinned)
+ {
+ out.unattested(
+ &purl,
+ uuid,
+ POM,
+ format!(
+ "{POM}: {ga}:{classifier} resolves {}, not the \
+ patched {pinned}; the Socket patch covers only the main jar, so that \
+ classifier copy keeps the unpatched code on the classpath",
+ version.unwrap_or("(none in this pom)")
+ ),
+ UnattestedKind::MavenClassifierUnpatched,
+ );
+ }
ties.entry(*uuid).or_default().insert((
purl,
group.clone(),
@@ -614,7 +639,13 @@ struct GaVersions {
fn group_by_ga(deps: &[PomDep]) -> BTreeMap<(String, String), GaVersions> {
let mut map: BTreeMap<(String, String), GaVersions> = BTreeMap::new();
- for dep in deps.iter().filter(|d| !d.in_profile) {
+ // A classifier variant (`sources`, `tests`, a native build) is its own
+ // artifact, managed apart from the main jar: its version neither pins
+ // nor shadows the main jar's.
+ for dep in deps
+ .iter()
+ .filter(|d| !d.in_profile && d.classifier.is_none())
+ {
let Some(version) = &dep.version else {
continue;
};
@@ -630,6 +661,41 @@ fn group_by_ga(deps: &[PomDep]) -> BTreeMap<(String, String), GaVersions> {
map
}
+/// The classifiers whose jars never run on a classpath. Every other
+/// classifier copy of a pinned GA (tests, a native or JDK-specific build)
+/// executes, so it must resolve the patched release too.
+const NON_EXECUTABLE_CLASSIFIERS: &[&str] = &["sources", "javadoc"];
+
+/// The first live executable classifier copy of `group:artifact` that does
+/// not resolve `pinned`, with the version it resolves (`None` when this pom
+/// gives it none). Its version is its own literal, else a
+/// `` entry with the same classifier — Maven keys
+/// management by classifier, so the classifier-less pin never applies.
+fn unpatched_executable_classifier<'a>(
+ deps: &'a [PomDep],
+ group: &str,
+ artifact: &str,
+ pinned: &str,
+) -> Option<(&'a str, Option<&'a str>)> {
+ let of_ga = |d: &&PomDep| !d.in_profile && d.group == group && d.artifact == artifact;
+ deps.iter()
+ .filter(of_ga)
+ .filter(|d| !d.managed)
+ .filter_map(|d| {
+ let classifier = d.classifier.as_deref()?;
+ (!NON_EXECUTABLE_CLASSIFIERS.contains(&classifier)).then_some((d, classifier))
+ })
+ .find_map(|(d, classifier)| {
+ let version = d.version.as_deref().or_else(|| {
+ deps.iter()
+ .filter(of_ga)
+ .find(|m| m.managed && m.classifier.as_deref() == Some(classifier))
+ .and_then(|m| m.version.as_deref())
+ });
+ (version != Some(pinned)).then_some((classifier, version))
+ })
+}
+
#[cfg(test)]
mod tests {
use super::super::testing::*;
@@ -703,6 +769,20 @@ mod tests {
"transitive-depmgmt",
"existing-depmgmt",
"existing-repositories",
+ // The scope-aware rewrites (#259, #262, #342): what the rewriter
+ // reports redirected, VEX attests — a commented-out, profile or
+ // plugin declaration and a classifier sibling left at the base
+ // version neither pin nor shadow the main jar.
+ "comment-dependency",
+ "profile-repositories",
+ "plugin-dependency",
+ "profile-depmgmt",
+ "commented-repositories",
+ "classifier-sources-sibling",
+ "classifier-only-transitive-main",
+ "self-closed-repositories",
+ "depmgmt-comment",
+ "self-closed-depmgmt",
] {
let out = run(&fixture(case)).await;
assert_refs(&out, &[(FX_PURL, FX_UUID, WiringMode::Hosted)]);
@@ -828,6 +908,83 @@ mod tests {
assert!(out.unattested.is_empty(), "{:?}", out.unattested);
}
+ /// A classifier copy is its own coordinate: the classifier-less pin
+ /// does not retarget it and the grant serves only the main jar. An
+ /// executable one (tests, a native build) left at another version keeps
+ /// the public, unpatched jar on the classpath, so the pin stays a ref
+ /// but is not attested. `sources` / `javadoc` never run and do not.
+ #[tokio::test]
+ async fn executable_classifier_copy_off_the_pin_is_not_attested() {
+ fn classified(c: &str, v: Option<&str>) -> String {
+ let v = v.map_or(String::new(), |v| format!("{v}"));
+ format!(
+ "org.slf4jslf4j-api\
+ {v}{c}\n"
+ )
+ }
+ fn project(deps: &str, managed: &str) -> Project {
+ let p = Project::new();
+ p.write(
+ "pom.xml",
+ pom(&format!(
+ "\n\n{managed}\n\
+ \n\n{}{deps}\n\
+ \n{}\n",
+ dep("org.slf4j", "slf4j-api", Some(&suffixed(UUID_A))),
+ hosted_repo(&format!("socket-patch-{UUID_A}"), ®istry_url(UUID_A)),
+ )),
+ );
+ p
+ }
+ let pin = suffixed(UUID_A);
+ let unattested = [
+ // At the base release: the public classifier jar.
+ (classified("tests", Some("1.7.36")), String::new()),
+ (classified("natives-linux", Some("1.7.36")), String::new()),
+ // Another release entirely.
+ (classified("tests", Some("1.7.30")), String::new()),
+ // Versioned by a classifier-keyed managed entry.
+ (
+ classified("tests", None),
+ classified("tests", Some("1.7.36")),
+ ),
+ // No version this pom gives it (a parent's): fail closed.
+ (classified("tests", None), String::new()),
+ ];
+ for (deps, managed) in &unattested {
+ let out = run(&project(deps, managed)).await;
+ assert_refs(&out, &[(FX_PURL, UUID_A, WiringMode::Hosted)]);
+ assert_eq!(out.unattested.len(), 1, "{deps}: {:?}", out.unattested);
+ let u = &out.unattested[0];
+ assert_eq!(u.kind, UnattestedKind::MavenClassifierUnpatched, "{deps}");
+ assert_eq!(
+ (u.purl.as_str(), u.uuid.as_str()),
+ (FX_PURL, UUID_A),
+ "{deps}"
+ );
+ assert!(u.detail.contains("org.slf4j:slf4j-api:"), "{}", u.detail);
+ }
+
+ let attested = [
+ // Never executed.
+ (classified("sources", Some("1.7.36")), String::new()),
+ (classified("javadoc", Some("1.7.36")), String::new()),
+ // Already on the patched release.
+ (classified("tests", Some(&pin)), String::new()),
+ (classified("tests", None), classified("tests", Some(&pin))),
+ // Managed only, never declared: nothing resolves it.
+ (String::new(), classified("tests", Some("1.7.36"))),
+ // No classifier copy at all.
+ (String::new(), String::new()),
+ ];
+ for (deps, managed) in &attested {
+ let out = run(&project(deps, managed)).await;
+ assert_refs(&out, &[(FX_PURL, UUID_A, WiringMode::Hosted)]);
+ assert!(out.unattested.is_empty(), "{deps}: {:?}", out.unattested);
+ assert!(out.refs[0].lockfile_basis_ok(), "{deps}");
+ }
+ }
+
#[tokio::test]
async fn configured_patch_server_origin_is_hosted() {
let origin = "http://127.0.0.1:4545";
diff --git a/crates/socket-patch-core/src/vex/discover/mod.rs b/crates/socket-patch-core/src/vex/discover/mod.rs
index 6f1a26540..6ff3ba108 100644
--- a/crates/socket-patch-core/src/vex/discover/mod.rs
+++ b/crates/socket-patch-core/src/vex/discover/mod.rs
@@ -474,6 +474,11 @@ pub enum UnattestedKind {
/// #261): a module's own literal `` overrides it, and only the
/// root pom is read, so the reactor may build the upstream jar.
MavenReactorRoot,
+ /// A hosted Maven pin whose GA also has a live executable classifier
+ /// copy (`tests`, a native build; not `sources` / `javadoc`) at another
+ /// version: the classifier-less pin does not retarget it and the grant
+ /// serves only the main jar, so the build runs the public classifier jar.
+ MavenClassifierUnpatched,
}
/// A ref discovery emits (so rollback, remove and list find the wiring,
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected-edits.json
new file mode 100644
index 000000000..23df82ecd
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected-edits.json
@@ -0,0 +1,34 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_management",
+ "action": "added",
+ "key": "org.slf4j:slf4j-api",
+ "new": {
+ "groupId": "org.slf4j",
+ "artifactId": "slf4j-api",
+ "version": "1.7.36-socket.77777777"
+ }
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected-warnings.json
new file mode 100644
index 000000000..414c0a5a5
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected-warnings.json
@@ -0,0 +1,4 @@
+[
+ "redirect_maven_classifier_unsupported",
+ "redirect_maven_dep_management_added"
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/pom.xml
new file mode 100644
index 000000000..0a0668c67
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/expected/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+ sources
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/input/pom.xml
new file mode 100644
index 000000000..f9f8482dc
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/input/pom.xml
@@ -0,0 +1,23 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+ sources
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-only-transitive-main/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected-edits.json
new file mode 100644
index 000000000..ba20ca8b9
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected-edits.json
@@ -0,0 +1,31 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_version",
+ "action": "rewritten",
+ "key": "org.slf4j:slf4j-api",
+ "original": "1.7.36",
+ "new": "1.7.36-socket.77777777"
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected-warnings.json
new file mode 100644
index 000000000..7fa3ab269
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected-warnings.json
@@ -0,0 +1,3 @@
+[
+ "redirect_maven_classifier_unsupported"
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/pom.xml
new file mode 100644
index 000000000..fbe9c80bd
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/expected/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+ sources
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/input/pom.xml
new file mode 100644
index 000000000..7d1829359
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/input/pom.xml
@@ -0,0 +1,23 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+ sources
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/classifier-sources-sibling/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected-edits.json
new file mode 100644
index 000000000..23df82ecd
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected-edits.json
@@ -0,0 +1,34 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_management",
+ "action": "added",
+ "key": "org.slf4j:slf4j-api",
+ "new": {
+ "groupId": "org.slf4j",
+ "artifactId": "slf4j-api",
+ "version": "1.7.36-socket.77777777"
+ }
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected-warnings.json
new file mode 100644
index 000000000..fe7c5067d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected-warnings.json
@@ -0,0 +1,3 @@
+[
+ "redirect_maven_dep_management_added"
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/pom.xml
new file mode 100644
index 000000000..6cb83a8a0
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/expected/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/input/pom.xml
new file mode 100644
index 000000000..da8cd6e7f
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/input/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/comment-dependency/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected-edits.json
new file mode 100644
index 000000000..ba20ca8b9
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected-edits.json
@@ -0,0 +1,31 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_version",
+ "action": "rewritten",
+ "key": "org.slf4j:slf4j-api",
+ "original": "1.7.36",
+ "new": "1.7.36-socket.77777777"
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected-warnings.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected-warnings.json
@@ -0,0 +1 @@
+[]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/pom.xml
new file mode 100644
index 000000000..06ff9ee01
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/expected/pom.xml
@@ -0,0 +1,38 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/input/pom.xml
new file mode 100644
index 000000000..7a821ae8e
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/input/pom.xml
@@ -0,0 +1,25 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/commented-repositories/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected-edits.json
new file mode 100644
index 000000000..23df82ecd
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected-edits.json
@@ -0,0 +1,34 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_management",
+ "action": "added",
+ "key": "org.slf4j:slf4j-api",
+ "new": {
+ "groupId": "org.slf4j",
+ "artifactId": "slf4j-api",
+ "version": "1.7.36-socket.77777777"
+ }
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected-warnings.json
new file mode 100644
index 000000000..fe7c5067d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected-warnings.json
@@ -0,0 +1,3 @@
+[
+ "redirect_maven_dep_management_added"
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/pom.xml
new file mode 100644
index 000000000..17b475d8b
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/expected/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+ junit
+ junit
+ 4.13.2
+
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/input/pom.xml
new file mode 100644
index 000000000..c19f5e744
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/input/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+
+
+ junit
+ junit
+ 4.13.2
+
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/depmgmt-comment/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected-edits.json
new file mode 100644
index 000000000..ba20ca8b9
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected-edits.json
@@ -0,0 +1,31 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_version",
+ "action": "rewritten",
+ "key": "org.slf4j:slf4j-api",
+ "original": "1.7.36",
+ "new": "1.7.36-socket.77777777"
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected-warnings.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected-warnings.json
@@ -0,0 +1 @@
+[]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/pom.xml
new file mode 100644
index 000000000..88f8a1799
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/expected/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/input/pom.xml
new file mode 100644
index 000000000..2b233942b
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/input/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/plugin-dependency/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected-edits.json
new file mode 100644
index 000000000..23df82ecd
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected-edits.json
@@ -0,0 +1,34 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_management",
+ "action": "added",
+ "key": "org.slf4j:slf4j-api",
+ "new": {
+ "groupId": "org.slf4j",
+ "artifactId": "slf4j-api",
+ "version": "1.7.36-socket.77777777"
+ }
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected-warnings.json
new file mode 100644
index 000000000..fe7c5067d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected-warnings.json
@@ -0,0 +1,3 @@
+[
+ "redirect_maven_dep_management_added"
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/pom.xml
new file mode 100644
index 000000000..9f69bb4bb
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/expected/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ pinned
+
+
+
+ junit
+ junit
+ 4.13.2
+
+
+
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+ junit
+ junit
+ 4.13.2
+
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/input/pom.xml
new file mode 100644
index 000000000..50db5b567
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/input/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ pinned
+
+
+
+ junit
+ junit
+ 4.13.2
+
+
+
+
+
+
+
+
+ junit
+ junit
+ 4.13.2
+
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-depmgmt/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/expected-edits.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/expected-edits.json
@@ -0,0 +1 @@
+[]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/expected-warnings.json
new file mode 100644
index 000000000..3b7af7dfa
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/expected-warnings.json
@@ -0,0 +1,3 @@
+[
+ "redirect_maven_profile_dependency_unpatched"
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/input/pom.xml
new file mode 100644
index 000000000..8c81aa9cf
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/input/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
+
+ slf4j-direct
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-literal/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected-edits.json
new file mode 100644
index 000000000..ba20ca8b9
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected-edits.json
@@ -0,0 +1,31 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_version",
+ "action": "rewritten",
+ "key": "org.slf4j:slf4j-api",
+ "original": "1.7.36",
+ "new": "1.7.36-socket.77777777"
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected-warnings.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected-warnings.json
@@ -0,0 +1 @@
+[]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/pom.xml
new file mode 100644
index 000000000..94315b269
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/expected/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ corp
+
+
+ corp
+ https://repo.corp.example/maven2
+
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/input/pom.xml
new file mode 100644
index 000000000..9a3dd72f1
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/input/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ corp
+
+
+ corp
+ https://repo.corp.example/maven2
+
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/profile-repositories/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected-edits.json
new file mode 100644
index 000000000..23df82ecd
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected-edits.json
@@ -0,0 +1,34 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_management",
+ "action": "added",
+ "key": "org.slf4j:slf4j-api",
+ "new": {
+ "groupId": "org.slf4j",
+ "artifactId": "slf4j-api",
+ "version": "1.7.36-socket.77777777"
+ }
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected-warnings.json
new file mode 100644
index 000000000..fe7c5067d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected-warnings.json
@@ -0,0 +1,3 @@
+[
+ "redirect_maven_dep_management_added"
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/pom.xml
new file mode 100644
index 000000000..a15b9c67e
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/expected/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/input/pom.xml
new file mode 100644
index 000000000..7929d43b2
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/input/pom.xml
@@ -0,0 +1,18 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.14
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-depmgmt/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected-edits.json
new file mode 100644
index 000000000..ba20ca8b9
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected-edits.json
@@ -0,0 +1,31 @@
+[
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_dep_version",
+ "action": "rewritten",
+ "key": "org.slf4j:slf4j-api",
+ "original": "1.7.36",
+ "new": "1.7.36-socket.77777777"
+ },
+ {
+ "path": "pom.xml",
+ "kind": "redirect_maven_repository",
+ "action": "added",
+ "key": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "new": {
+ "id": "socket-patch-77777777-7777-7777-7777-777777777777",
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2"
+ }
+ },
+ {
+ "path": ".mvn/maven.config",
+ "kind": "redirect_maven_config",
+ "action": "added",
+ "key": "trustedChecksums"
+ },
+ {
+ "path": ".mvn/checksums/checksums.sha256",
+ "kind": "redirect_maven_trusted_checksums",
+ "action": "added"
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected-warnings.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected-warnings.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected-warnings.json
@@ -0,0 +1 @@
+[]
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/.mvn/checksums/checksums.sha256 b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/.mvn/checksums/checksums.sha256
new file mode 100644
index 000000000..71c583f77
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/.mvn/checksums/checksums.sha256
@@ -0,0 +1,2 @@
+cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.jar
+dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd org/slf4j/slf4j-api/1.7.36-socket.77777777/slf4j-api-1.7.36-socket.77777777.pom
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/.mvn/maven.config b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/.mvn/maven.config
new file mode 100644
index 000000000..d3b91570d
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/.mvn/maven.config
@@ -0,0 +1,6 @@
+-Daether.artifactResolver.postProcessor.trustedChecksums=true
+-Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256
+-Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=false
+-Daether.trustedChecksumsSource.summaryFile=true
+-Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums
+-Daether.trustedChecksumsSource.summaryFile.originAware=false
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/pom.xml
new file mode 100644
index 000000000..f2070276a
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/expected/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+ socket-patch-77777777-7777-7777-7777-777777777777
+ https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2
+
+ true
+ fail
+
+
+ false
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36-socket.77777777
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/input/pom.xml b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/input/pom.xml
new file mode 100644
index 000000000..0fc7837ad
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/input/pom.xml
@@ -0,0 +1,18 @@
+
+
+ 4.0.0
+ dev.socket.test
+ consumer
+ 1.0.0
+ jar
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
diff --git a/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/overrides.json b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/overrides.json
new file mode 100644
index 000000000..53deb25b8
--- /dev/null
+++ b/crates/socket-patch-core/tests/fixtures/redirect/maven/pom/self-closed-repositories/overrides.json
@@ -0,0 +1,28 @@
+[
+ {
+ "ecosystem": "maven",
+ "name": "slf4j-api",
+ "namespace": "org.slf4j",
+ "version": "1.7.36",
+ "token": "11111111-1111-1111-1111-111111111111",
+ "patchUuid": "77777777-7777-7777-7777-777777777777",
+ "artifactUrl": "https://patch.socket.dev/patch/maven/org.slf4j/slf4j-api/1.7.36/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/slf4j-api-1.7.36.jar",
+ "registryOverride": {
+ "kind": "maven2",
+ "indexUrl": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "identifiers": {
+ "name": "org.slf4j/slf4j-api",
+ "version": "1.7.36",
+ "mavenGroupId": "org.slf4j",
+ "mavenArtifactId": "slf4j-api",
+ "mavenSuffixedVersion": "1.7.36-socket.77777777",
+ "mavenPomSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
+ }
+ },
+ "integrity": {
+ "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "md5": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ }
+ }
+]
diff --git a/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-maven.json b/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-maven.json
index e1a310465..5d05aa7e7 100644
--- a/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-maven.json
+++ b/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-maven.json
@@ -44,6 +44,231 @@
"elsewhere": [],
"live_claims": []
},
+ "redirect/maven/pom/classifier-only-transitive-main/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/classifier-only-transitive-main/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
+ "redirect/maven/pom/classifier-sources-sibling/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/classifier-sources-sibling/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
+ "redirect/maven/pom/comment-dependency/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/comment-dependency/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
+ "redirect/maven/pom/commented-repositories/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/commented-repositories/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
+ "redirect/maven/pom/depmgmt-comment/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/depmgmt-comment/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
"redirect/maven/pom/existing-depmgmt/expected": {
"refs": [
{
@@ -212,6 +437,149 @@
"elsewhere": [],
"live_claims": []
},
+ "redirect/maven/pom/plugin-dependency/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/plugin-dependency/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
+ "redirect/maven/pom/profile-depmgmt/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/profile-depmgmt/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
+ "redirect/maven/pom/profile-literal/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
+ "redirect/maven/pom/profile-repositories/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/profile-repositories/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
"redirect/maven/pom/property-version-warn/input": {
"refs": [],
"diagnostics": [],
@@ -257,6 +625,96 @@
}
]
},
+ "redirect/maven/pom/self-closed-depmgmt/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/self-closed-depmgmt/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
+ "redirect/maven/pom/self-closed-repositories/expected": {
+ "refs": [
+ {
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "source_file": "pom.xml",
+ "artifact_rel": null,
+ "locked_integrity": "Sha256Hex(\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\")",
+ "integrity_required": false,
+ "url": "https://patch.socket.dev/patch-registry/maven/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/maven2",
+ "lockfile_basis_ok": true
+ }
+ ],
+ "diagnostics": [],
+ "recognized": [
+ {
+ "uuid": "11111111-1111-1111-1111-111111111111",
+ "mode": "hosted",
+ "file": "pom.xml"
+ },
+ {
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "mode": "hosted",
+ "file": "pom.xml"
+ }
+ ],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": [
+ {
+ "mode": "hosted",
+ "uuid": "77777777-7777-7777-7777-777777777777",
+ "purl": "pkg:maven/org.slf4j/slf4j-api@1.7.36"
+ }
+ ]
+ },
+ "redirect/maven/pom/self-closed-repositories/input": {
+ "refs": [],
+ "diagnostics": [],
+ "recognized": [],
+ "unlocked_pins": [],
+ "elsewhere": [],
+ "live_claims": []
+ },
"redirect/maven/pom/transitive-depmgmt/expected": {
"refs": [
{
diff --git a/crates/socket-patch-core/tests/upstream_restore_golden.rs b/crates/socket-patch-core/tests/upstream_restore_golden.rs
index 9681cdcc3..9e2bc7606 100644
--- a/crates/socket-patch-core/tests/upstream_restore_golden.rs
+++ b/crates/socket-patch-core/tests/upstream_restore_golden.rs
@@ -2491,7 +2491,16 @@ async fn maven_goldens_round_trip() {
// resolver lines were the user's is not derivable; they stay (warned).
// no-suffix-fallback: a same-GAV repository is tied to no artifact, so
// discovery reports no pin to restore.
- let not_invertible = ["mvn-config-merge", "no-suffix-fallback"];
+ // self-closed-repositories / self-closed-depmgmt: the rewriter expanded
+ // the user's `` to hold its entry; the restore removes the entry and
+ // leaves the equivalent empty `` (asserted in the upstream
+ // maven unit tests).
+ let not_invertible = [
+ "mvn-config-merge",
+ "no-suffix-fallback",
+ "self-closed-repositories",
+ "self-closed-depmgmt",
+ ];
let mut ran = 0;
for case in load("maven/pom") {
let name = case.dir.file_name().unwrap().to_string_lossy().into_owned();
diff --git a/docs/ecosystems.md b/docs/ecosystems.md
index cb026df0d..6297ded22 100644
--- a/docs/ecosystems.md
+++ b/docs/ecosystems.md
@@ -670,6 +670,21 @@ Honest limits of the Maven and NuGet flows — documented behavior, not bugs:
repository whose grant URL changed (a rotated token) is refreshed in place. A suffixed
literal no hosted repository in the pom minted (a vendored `socket-patch-vendor-`
pin, say) is still a mismatch and is skipped.
+ The rewriter reads the pom the way Maven builds the project: markup inside comments,
+ CDATA, `` / `` (plugin classpaths), ``,
+ `` and `` is never matched or edited, and the
+ repository and `` pin always land in the project's own top-level
+ sections (an existing self-closed `` or `` is
+ expanded in place, so Maven never sees a duplicated tag). A GA declared only in such
+ markup counts as transitive and gets the top-level pin; a versioned declaration inside
+ `` (a literal or a `${property}`) is left as-is and reported
+ (`redirect_maven_profile_dependency_unpatched`), because an active profile's version
+ beats the pin, and when a profile holds the GA's only declaration nothing is pinned.
+ The grant serves the main jar only: a `` variant keeps its version
+ (`redirect_maven_classifier_unsupported`), and the literal-or-pin decision is made from
+ the classifier-less declarations. A `sources` / `javadoc` variant at the patched release
+ only warns; any other variant there (tests, a native build) would keep unpatched code on
+ a classpath, so the dep is not redirected.
* **Multi-module reactors are vendored-only (hosted Maven).** Hosted mode reads only
the root `pom.xml`, and a module's own literal `` always beats a root
`` pin, so a root pin would leave that module on the unpatched