diff --git a/crates/socket-patch-bench/README.md b/crates/socket-patch-bench/README.md index 51028a4d9..408e73cbc 100644 --- a/crates/socket-patch-bench/README.md +++ b/crates/socket-patch-bench/README.md @@ -38,9 +38,12 @@ layout: `npm`, `pnpm` (isolated `.pnpm` store with symlinks), `yarn-classic`, `yarn-berry` (node-modules linker), `bun` (text `bun.lock`, hoisted), `bun-isolated` (the same lockfile, Bun 1.3's isolated `.bun` store), `vlt` (`.vlt` store), `pip` (hash-pinned `requirements.txt`), `uv`, `pylock` -(PEP 751), `poetry`, `pipenv`, `pdm`, `bundler`, `composer`, `cargo`, -`golang`, `nuget` and `maven`. Deno has no hosted rewrite and is not -benchmarked separately. +(PEP 751), `poetry`, `pipenv`, `pdm`, `hatch` (lockless: `hatch.toml` +environment pins, rewritten in place), `bundler`, `composer`, `cargo`, +`golang`, `nuget`, `maven` and `gradle` (`gradle.lockfile`, Gradle's +`modules-2/files-2.1` cache; hosted mode wires the build through +`.socket/gradle/`). Deno has no hosted rewrite and is not benchmarked +separately. Sizes are a large-but-ordinary project for the ecosystem (3000 npm-family packages, 1500 for vlt, 400-1200 for the others, so every scan takes about @@ -75,8 +78,9 @@ unexpected). A rescan's first, preparing scan is untimed. The environment is rebuilt from nothing for every run (`env -i`): `HOME`, `XDG_*` and `TMPDIR` point into the fixture, so per-user caches -(`~/.cargo`, `~/go/pkg/mod`, `~/.nuget/packages`, `~/.m2`) are the -fixture's own and the runner's are never read; telemetry, the update check +(`~/.cargo`, `~/go/pkg/mod`, `~/.nuget/packages`, `~/.m2`, and +`GRADLE_USER_HOME`, which the Gradle fixture sets) are the fixture's own +and the runner's are never read; telemetry, the update check and the persisted Socket login are off; and every proxy variable points at a closed port, so a request to anything but the mock fails the run instead of timing the internet. Python fixtures carry a project `.venv` and Ruby ones diff --git a/crates/socket-patch-bench/src/fixtures/mod.rs b/crates/socket-patch-bench/src/fixtures/mod.rs index 8ea199430..d02863f64 100644 --- a/crates/socket-patch-bench/src/fixtures/mod.rs +++ b/crates/socket-patch-bench/src/fixtures/mod.rs @@ -188,6 +188,13 @@ pub static ALL: &[Pm] = &[ patched: 12, build: pypi::build_pdm, }, + Pm { + name: "hatch", + description: "Hatch (hatchling pyproject + hatch.toml envs, .venv)", + packages: 1000, + patched: 25, + build: pypi::build_hatch, + }, Pm { name: "bundler", description: "RubyGems (Gemfile.lock with CHECKSUMS, vendor/bundle)", @@ -230,6 +237,13 @@ pub static ALL: &[Pm] = &[ patched: 25, build: other::build_maven, }, + Pm { + name: "gradle", + description: "Gradle (build.gradle + gradle.lockfile, ~/.gradle/caches)", + packages: 1000, + patched: 25, + build: other::build_gradle, + }, ]; #[cfg(test)] diff --git a/crates/socket-patch-bench/src/fixtures/other.rs b/crates/socket-patch-bench/src/fixtures/other.rs index 09adc07bb..3b4ffdb42 100644 --- a/crates/socket-patch-bench/src/fixtures/other.rs +++ b/crates/socket-patch-bench/src/fixtures/other.rs @@ -712,9 +712,11 @@ pub fn build_nuget(t: &mut Tree, size: Size) -> std::io::Result { // ── Maven ────────────────────────────────────────────────────────────── -pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { - let arts = universe( - "maven", +/// The Maven-coordinate universe the Maven and Gradle fixtures share +/// (`group:artifact` names). +fn jvm_universe(seed: &str, size: Size) -> Vec { + universe( + seed, size, 0.2, |r, i| { @@ -731,11 +733,27 @@ pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { format!("{g}:{a}") }, gen::version, - ); - let ga = |p: &Pkg| -> (String, String) { - let (g, a) = p.name.split_once(':').unwrap(); - (g.to_string(), a.to_string()) - }; + ) +} + +fn ga(p: &Pkg) -> (String, String) { + let (g, a) = p.name.split_once(':').unwrap(); + (g.to_string(), a.to_string()) +} + +/// A dependency's pom, listing its own dependencies. +fn jvm_pom(arts: &[Pkg], p: &Pkg) -> String { + let (g, a) = ga(p); + let mut deps = String::new(); + for &j in &p.deps { + let (dg, da) = ga(&arts[j]); + let _ = write!(deps, " \n {dg}\n {da}\n {}\n \n", arts[j].version); + } + format!("\n\n 4.0.0\n {g}\n {a}\n {}\n \n{deps} \n\n", p.version) +} + +pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { + let arts = jvm_universe("maven", size); let mut pom = String::from("\n\n 4.0.0\n dev.socket.bench\n bench-app\n 1.0.0\n jar\n\n \n 17\n \n\n \n"); for p in arts.iter().filter(|p| p.direct) { let (g, a) = ga(p); @@ -754,12 +772,7 @@ pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { g.replace('.', "/"), p.version ); - let mut deps = String::new(); - for &j in &p.deps { - let (dg, da) = ga(&arts[j]); - let _ = write!(deps, " \n {dg}\n {da}\n {}\n \n", arts[j].version); - } - let pom = format!("\n\n 4.0.0\n {g}\n {a}\n {}\n \n{deps} \n\n", p.version); + let pom = jvm_pom(&arts, p); t.write( &format!("{dir}/{a}-{}.pom.sha1", p.version), gen::sha1_hex(&pom), @@ -778,36 +791,10 @@ pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { .iter() .filter(|p| p.patched) .map(|p| { - let (g, a) = ga(p); - let purl = format!("pkg:maven/{g}/{a}@{}", p.version); - let (uuid, token) = grant(&purl); - let suffixed = format!("{}-socket.{}", p.version, &uuid[..8]); - let url = format!("{PATCH_HOST}/patch/maven/{g}/{a}/{}/{token}/{uuid}/{a}-{suffixed}.jar", p.version); - spec( - purl.clone(), - uuid.clone(), - &format!("package/{a}.class"), - json!({ - "status": "granted", - "url": url, - "purl": purl, - "artifacts": [{ "kind": "tarball", "url": url, "integrity": { - "sha256": gen::sha256_hex(&format!("patched-jar:{}", p.name)), - "sha1": gen::sha1_hex(&format!("patched-jar:{}", p.name)), - } }], - "registryOverride": { - "kind": "maven2", - "indexUrl": format!("{PATCH_HOST}/patch-registry/maven/{token}/{uuid}/maven2"), - "identifiers": { - "name": format!("{g}/{a}"), - "version": p.version, - "mavenGroupId": g, - "mavenArtifactId": a, - "mavenSuffixedVersion": suffixed, - "mavenPomSha256": gen::sha256_hex(&format!("patched-pom:{}", p.name)), - }, - }, - }), + jvm_patch( + p, + |token, uuid| format!("{PATCH_HOST}/patch-registry/maven/{token}/{uuid}/maven2"), + false, ) }) .collect(); @@ -822,3 +809,139 @@ pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { &[], )) } + +/// The mock's patch for one Maven coordinate: a suffixed-version maven2 +/// registry override. `index_url` builds the override's repository URL +/// from the grant token and patch uuid. +fn jvm_patch(p: &Pkg, index_url: impl Fn(&str, &str) -> String, module_sha: bool) -> PatchSpec { + let (g, a) = ga(p); + let purl = format!("pkg:maven/{g}/{a}@{}", p.version); + let (uuid, token) = grant(&purl); + let suffixed = format!("{}-socket.{}", p.version, &uuid[..8]); + let url = format!( + "{PATCH_HOST}/patch/maven/{g}/{a}/{}/{token}/{uuid}/{a}-{suffixed}.jar", + p.version + ); + let mut identifiers = json!({ + "name": format!("{g}/{a}"), + "version": p.version, + "mavenGroupId": g, + "mavenArtifactId": a, + "mavenSuffixedVersion": suffixed, + "mavenPomSha256": gen::sha256_hex(&format!("patched-pom:{}", p.name)), + }); + if module_sha { + identifiers["mavenModuleSha256"] = + json!(gen::sha256_hex(&format!("patched-module:{}", p.name))); + } + spec( + purl.clone(), + uuid.clone(), + &format!("package/{a}.class"), + json!({ + "status": "granted", + "url": url, + "purl": purl, + "artifacts": [{ "kind": "tarball", "url": url, "integrity": { + "sha256": gen::sha256_hex(&format!("patched-jar:{}", p.name)), + "sha1": gen::sha1_hex(&format!("patched-jar:{}", p.name)), + } }], + "registryOverride": { + "kind": "maven2", + "indexUrl": index_url(&token, &uuid), + "identifiers": identifiers, + }, + }), + ) +} + +// ── Gradle ───────────────────────────────────────────────────────────── + +/// A single-project Groovy-DSL build with dependency locking, its +/// dependencies in Gradle's own cache (`modules-2/files-2.1`, jar and pom +/// in separate sha1 dirs). Hosted mode wires the build through an owned +/// settings script and index under `.socket/gradle/` and pins the +/// suffixed versions in `gradle.lockfile`. +pub fn build_gradle(t: &mut Tree, size: Size) -> std::io::Result { + let arts = jvm_universe("gradle", size); + t.write( + "project/settings.gradle", + "rootProject.name = 'bench-app'\n", + )?; + let mut build = String::from( + "plugins {\n id 'java'\n}\n\nrepositories {\n mavenCentral()\n}\n\ndependencyLocking {\n lockAllConfigurations()\n}\n\ndependencies {\n", + ); + for p in arts.iter().filter(|p| p.direct) { + let _ = writeln!(build, " implementation '{}:{}'", p.name, p.version); + } + build.push_str("}\n"); + t.write("project/build.gradle", build)?; + let mut sorted: Vec<&Pkg> = arts.iter().collect(); + sorted.sort_by(|a, b| a.name.cmp(&b.name)); + let mut lock = String::from( + "# This is a Gradle generated file for dependency locking.\n# Manual edits can break the build and are not advised.\n# This file is expected to be part of source control.\n", + ); + for p in &sorted { + let _ = writeln!( + lock, + "{}:{}=compileClasspath,runtimeClasspath", + p.name, p.version + ); + } + lock.push_str("empty=annotationProcessor,testAnnotationProcessor\n"); + t.write("project/gradle.lockfile", lock)?; + t.write( + "project/gradle/wrapper/gradle-wrapper.properties", + "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.10.2-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n", + )?; + t.write( + "project/src/main/java/App.java", + "public class App { public static void main(String[] a) {} }\n", + )?; + for p in &arts { + let (g, a) = ga(p); + let dir = format!( + "home/.gradle/caches/modules-2/files-2.1/{g}/{a}/{}", + p.version + ); + let pom = jvm_pom(&arts, p); + let jar = format!("PK synthetic {}", p.name); + t.write( + &format!("{dir}/{}/{a}-{}.pom", gen::sha1_hex(&pom), p.version), + pom, + )?; + t.write( + &format!("{dir}/{}/{a}-{}.jar", gen::sha1_hex(&jar), p.version), + jar, + )?; + } + // Gradle's planner only takes https repositories; the CLI never + // fetches the index during a scan, so it need not be the mock. + let patches = arts + .iter() + .filter(|p| p.patched) + .map(|p| { + jvm_patch( + p, + |token, uuid| { + format!("https://patch.socket.dev/patch-registry/maven/{token}/{uuid}/maven2") + }, + true, + ) + }) + .collect(); + let mut f = fixture( + arts.len(), + patches, + &[ + ".socket/gradle/.gitattributes", + ".socket/gradle/hosted-index.tsv", + ".socket/gradle/socket-patch.hosted.settings.gradle", + "gradle.lockfile", + "settings.gradle", + ], + &["redirect_gradle_detached_configs_unguarded"], + ); + f.env_paths = vec![("GRADLE_USER_HOME", "home/.gradle")]; + Ok(f) +} diff --git a/crates/socket-patch-bench/src/fixtures/pypi.rs b/crates/socket-patch-bench/src/fixtures/pypi.rs index a785f6c3a..c293fd92a 100644 --- a/crates/socket-patch-bench/src/fixtures/pypi.rs +++ b/crates/socket-patch-bench/src/fixtures/pypi.rs @@ -483,3 +483,32 @@ pub fn build_pdm(t: &mut Tree, size: Size) -> std::io::Result { t.mkdir("home")?; Ok(fixture(&ds, &["pdm.lock"], &[])) } + +// ── hatch ────────────────────────────────────────────────────────────── + +/// A lockless Hatch app: hatchling backend, the direct deps in +/// `[project]`, and a `hatch.toml` default environment (in-project +/// `.venv`) that pins every patched transitive too. Hosted Hatch only +/// redirects deps a Hatch table declares, so the pins are what a real +/// project patching transitive deps writes. +pub fn build_hatch(t: &mut Tree, size: Size) -> std::io::Result { + let ds = dists("hatch", size); + t.write( + "project/pyproject.toml", + pyproject( + &ds, + "\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n", + ), + )?; + let mut s = String::from("[envs.default]\npath = \".venv\"\ndependencies = [\n"); + for (i, d) in ds.iter().enumerate() { + if d.patched || i % 10 == 0 { + let _ = writeln!(s, " \"{}=={}\",", d.name, d.version); + } + } + s.push_str("]\n\n[envs.default.scripts]\ntest = \"python -m unittest\"\n"); + t.write("project/hatch.toml", s)?; + install_venv(t, &ds)?; + t.mkdir("home")?; + Ok(fixture(&ds, &["hatch.toml", "pyproject.toml"], &[])) +} diff --git a/crates/socket-patch-core/src/crawlers/gradle_cache.rs b/crates/socket-patch-core/src/crawlers/gradle_cache.rs index ef295ee27..afd7c4fba 100644 --- a/crates/socket-patch-core/src/crawlers/gradle_cache.rs +++ b/crates/socket-patch-core/src/crawlers/gradle_cache.rs @@ -70,8 +70,7 @@ pub fn hash_eq(dir_name: &str, sha1_hex: &str) -> bool { /// Whether `bytes` are the pristine download Gradle stored in the hash /// directory `dir_name` (their sha1 names it). pub fn pristine(dir_name: &str, bytes: &[u8]) -> bool { - use sha1::{Digest, Sha1}; - hash_eq(dir_name, &hex::encode(Sha1::digest(bytes))) + hash_eq(dir_name, &crate::utils::digest::sha1_hex_of(bytes)) } /// Whether `path` is a version directory of a `files-2.1` tree @@ -432,8 +431,6 @@ impl DerivedIndex { /// The [`DerivedCopies`] of the jar `jar_leaf` whose pristine bytes /// hash to `pristine_sha1`. pub fn query(&self, jar_leaf: &str, pristine_sha1: &str) -> DerivedCopies { - use sha1::{Digest, Sha1}; - let instrumented = format!("instrumented-{jar_leaf}"); let mut out = DerivedCopies { incomplete: self.incomplete, @@ -460,7 +457,9 @@ impl DerivedIndex { out.stale.push(path.clone()); } else if name == jar_leaf || name == instrumented { match crate::utils::fs::read_regular_to_bytes_sync(path) { - Ok(bytes) if hash_eq(&hex::encode(Sha1::digest(&bytes)), pristine_sha1) => { + Ok(bytes) + if hash_eq(&crate::utils::digest::sha1_hex_of(&bytes), pristine_sha1) => + { out.stale.push(path.clone()) } Ok(_) => out.unknown.push(path.clone()), diff --git a/crates/socket-patch-core/src/patch/jvm_jar.rs b/crates/socket-patch-core/src/patch/jvm_jar.rs index 82d679406..f38a84403 100644 --- a/crates/socket-patch-core/src/patch/jvm_jar.rs +++ b/crates/socket-patch-core/src/patch/jvm_jar.rs @@ -25,8 +25,6 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use sha1::Digest as _; - use crate::crawlers::gradle_cache; use crate::hash::git_sha256::compute_git_sha256_from_bytes; use crate::manifest::schema::PatchFileInfo; @@ -353,12 +351,11 @@ fn unpatched_members( } fn sha256_hex(bytes: &[u8]) -> String { - use sha2::Digest as _; - hex::encode(sha2::Sha256::digest(bytes)) + crate::utils::digest::sha256_hex_of(bytes) } fn sha1_hex(bytes: &[u8]) -> String { - hex::encode(sha1::Sha1::digest(bytes)) + crate::utils::digest::sha1_hex_of(bytes) } /// `/jvm-originals/.jar`. diff --git a/crates/socket-patch-core/src/patch/sidecars/maven.rs b/crates/socket-patch-core/src/patch/sidecars/maven.rs index f2f5a2466..8798bfce6 100644 --- a/crates/socket-patch-core/src/patch/sidecars/maven.rs +++ b/crates/socket-patch-core/src/patch/sidecars/maven.rs @@ -17,8 +17,6 @@ use std::path::{Path, PathBuf}; -use sha1::Digest as _; - use super::{ SidecarAdvisory, SidecarAdvisoryCode, SidecarError, SidecarFile, SidecarFileAction, SidecarPayload, SidecarSeverity, @@ -44,7 +42,7 @@ impl Algo { fn digest(self, bytes: &[u8]) -> String { match self { - Algo::Sha1 => hex::encode(sha1::Sha1::digest(bytes)), + Algo::Sha1 => crate::utils::digest::sha1_hex_of(bytes), Algo::Md5 => hex::encode(md5(bytes)), } }