Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions crates/socket-patch-cli/CLI_CONTRACT.md

Large diffs are not rendered by default.

83 changes: 83 additions & 0 deletions crates/socket-patch-cli/tests/in_process_redirect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4744,6 +4744,89 @@ async fn yarn_berry_rollback_keeps_a_bare_locator_for_conventional_urls() {
assert!(!restored.contains("__archiveUrl"), "{restored}");
}

/// #908: the restore reads `dist.tarball` from the registry the project
/// resolves against (`.yarnrc.yml` `npmRegistryServer`), not from the
/// default registry. A mirror whose tarball URLs are off the conventional
/// path keeps its `::__archiveUrl=` binding, even though the default
/// registry (`SOCKET_NPM_REGISTRY` here, npmjs normally) serves the
/// conventional URL yarn would derive — and the mirror would 404.
#[tokio::test]
#[serial]
async fn yarn_berry_rollback_reads_the_tarball_from_the_project_registry() {
let server = MockServer::start().await;
mock_discovery(&server).await;
let hosted_url = HOSTED_URL.replace("http://patch.test", &server.uri());
mock_reference_with_berry_url(&server, &hosted_url).await;
mock_view(&server).await;
let integrity = vlt_hosted_common::sha512_sri(&upstream_tarball());
// The default registry: conventional URLs, as npmjs serves them.
mock_npm_registry_advertising(
&server,
&integrity,
&format!(
"{}/npm-registry/{NAME}/-/{NAME}-{VERSION}.tgz",
server.uri()
),
)
.await;
// The project's mirror: Artifactory/CDN-style tarball URLs.
let mirror = format!("{}/mirror", server.uri());
let advertised = format!("{}/cdn/files/{NAME}-{VERSION}.tgz", server.uri());
Mock::given(method("GET"))
.and(path(format!("/mirror/{NAME}/{VERSION}")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"name": NAME,
"version": VERSION,
"dist": { "tarball": advertised, "integrity": integrity },
})))
.mount(&server)
.await;
let binding = |t: &str| {
t.replace(
&format!("resolution: \"{NAME}@npm:{VERSION}\""),
&format!(
"resolution: \"{NAME}@npm:{VERSION}::__archiveUrl={}\"",
socket_patch_core::utils::uri::encode_uri_component(&advertised)
),
)
};

let tmp = tempfile::tempdir().unwrap();
write_berry_project_spelled(tmp.path(), binding);
std::fs::write(
tmp.path().join(".yarnrc.yml"),
format!("nodeLinker: node-modules\nnpmRegistryServer: \"{mirror}\"\n"),
)
.unwrap();
let lock_path = tmp.path().join("yarn.lock");
let pristine = std::fs::read_to_string(&lock_path).unwrap();

let env = run_redirect_subprocess_with(
tmp.path(),
&server.uri(),
&["--patch-server-url", &server.uri()],
);
assert_eq!(env["redirect"]["redirected"], 1, "{env:#}");

let (code, env) = rollback_json_with_origin(tmp.path(), &server, &server.uri());
assert_eq!(code, Some(0), "rollback: {env:#}");
assert_eq!(
env["hosted"]["reverted"],
serde_json::json!([PURL]),
"{env:#}"
);
let restored = std::fs::read_to_string(&lock_path).unwrap();
let checksum = berry_checksum_of(&restored);
assert_eq!(
restored,
pristine.replace(
&format!("10c0/{}", "3".repeat(128)),
&format!("10c0/{checksum}")
),
"rollback keeps the mirror's __archiveUrl binding"
);
}

/// A pnpm project whose lock records the patched entry as
/// `{integrity, tarball: <tarball>}` — what pnpm writes under
/// `lockfile-include-tarball-url`, or for a tarball URL the registry
Expand Down
9 changes: 4 additions & 5 deletions crates/socket-patch-core/src/crawlers/gradle_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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()),
Expand Down
7 changes: 2 additions & 5 deletions crates/socket-patch-core/src/patch/jvm_jar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}

/// `<socket_dir>/jvm-originals/<sha256>.jar`.
Expand Down
42 changes: 33 additions & 9 deletions crates/socket-patch-core/src/patch/redirect/upstream/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,15 @@ pub(crate) const OFFLINE: &str =

type Cache<T> = Mutex<HashMap<(String, String), Result<T, String>>>;

/// [`Cache`] keyed by (registry base, name, version).
type RegistryCache<T> = Mutex<HashMap<(String, String, String), Result<T, String>>>;

/// One client per restore run; every lookup is cached (success and
/// failure alike) so a pin wired in several files costs one request.
pub(crate) struct UpstreamClient {
http: RegistryClient,
offline: bool,
npm: Cache<NpmDist>,
npm: RegistryCache<NpmDist>,
npm_berry: Cache<String>,
cargo: Cache<String>,
go: Cache<GoSums>,
Expand Down Expand Up @@ -205,25 +208,42 @@ impl UpstreamClient {
String::from_utf8(bytes).map_err(|_| format!("{url} is not UTF-8"))
}

/// `dist` of `name@version` from the npm registry's version document.
/// `dist` of `name@version` from the default npm registry's version
/// document.
pub(crate) async fn npm_dist(&self, name: &str, version: &str) -> Result<NpmDist, String> {
let key = (name.to_string(), version.to_string());
self.npm_dist_on(&npm_registry_base(), name, version).await
}

/// `dist` of `name@version` from the version document of the npm
/// registry at `base` (a project's configured registry or mirror).
pub(crate) async fn npm_dist_on(
&self,
base: &str,
name: &str,
version: &str,
) -> Result<NpmDist, String> {
let base = base.trim_end_matches('/');
let key = (base.to_string(), name.to_string(), version.to_string());
if let Some(hit) = self.npm.lock().await.get(&key) {
return hit.clone();
}
let result = self.fetch_npm_dist(name, version).await;
let result = self.fetch_npm_dist(base, name, version).await;
self.npm.lock().await.insert(key, result.clone());
result
}

async fn fetch_npm_dist(&self, name: &str, version: &str) -> Result<NpmDist, String> {
async fn fetch_npm_dist(
&self,
base: &str,
name: &str,
version: &str,
) -> Result<NpmDist, String> {
if self.offline {
return Err(OFFLINE.to_string());
}
let encoded_name = name.replace('/', "%2f");
let url = format!(
"{}/{encoded_name}/{}",
npm_registry_base(),
"{base}/{encoded_name}/{}",
crate::utils::uri::encode_uri_component(version)
);
let doc = self.get_json(&url).await?;
Expand Down Expand Up @@ -747,7 +767,9 @@ mod tests {
let h1 = go_mod_h1(b"module example.com/m\n");
assert!(h1.starts_with("h1:") && h1.ends_with('='), "{h1}");
}
// The npm cache key reads `SOCKET_NPM_REGISTRY`, which serial tests set.
#[tokio::test]
#[serial_test::serial]
async fn berry_metadata_is_registry_anchored_without_repacking() {
use base64::Engine as _;
use sha2::Digest as _;
Expand Down Expand Up @@ -775,7 +797,7 @@ mod tests {
.await;
let client = UpstreamClient::new(false);
client.npm.lock().await.insert(
("left-pad".into(), "1.3.0".into()),
(npm_registry_base(), "left-pad".into(), "1.3.0".into()),
Ok(NpmDist {
tarball: format!("{}/archive.tgz", server.uri()),
integrity: registry_sri,
Expand All @@ -794,7 +816,9 @@ mod tests {
}
}

// The npm cache key reads `SOCKET_NPM_REGISTRY`, which serial tests set.
#[tokio::test]
#[serial_test::serial]
async fn berry_metadata_refuses_wrong_identity_integrity_and_unavailable_service() {
use base64::Engine as _;
use sha2::Digest as _;
Expand Down Expand Up @@ -833,7 +857,7 @@ mod tests {
.await;
let client = UpstreamClient::new(false);
client.npm.lock().await.insert(
("left-pad".into(), "1.3.0".into()),
(npm_registry_base(), "left-pad".into(), "1.3.0".into()),
Ok(NpmDist {
tarball: format!("{}/archive.tgz", server.uri()),
integrity: Some("sha512-other".into()),
Expand Down
Loading
Loading