Skip to content
Open
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
157 changes: 157 additions & 0 deletions crates/socket-patch-cli/tests/mode_migration_pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,163 @@ async fn hatch_vendored_to_hosted() {
assert_vendored_to_hosted(&root, files).await;
}

/// Stages one flavor's project files; returns its wiring files.
type StageFn = fn(&Path) -> &'static [&'static str];

/// A uv PEP 723 script with its `.py.lock`; returns its wiring files.
fn stage_script_lock(root: &Path) -> &'static [&'static str] {
std::fs::write(
root.join("job.py"),
"# /// script\n# requires-python = \">=3.9\"\n# dependencies = [\"six==1.16.0\"]\n# ///\nimport six\n",
)
.unwrap();
std::fs::write(
root.join("job.py.lock"),
format!(
"version = 1\nrevision = 3\nrequires-python = \">=3.9\"\n\n[manifest]\nrequirements = [{{name = \"six\", specifier = \"==1.16.0\"}}]\n\n[[package]]\nname = \"six\"\nversion = \"1.16.0\"\nsource = {{registry = \"https://pypi.org/simple\"}}\nwheels = [{{url = \"https://files.pythonhosted.org/six-1.16.0-py2.py3-none-any.whl\", hash = \"sha256:{WHEEL_SHA}\"}}]\n"
),
)
.unwrap();
&["job.py", "job.py.lock"]
}

/// #742 / #650: a vendored uv project, uv script lock and Hatch project pick
/// up a superseding patch. The manifest moves `six` from patch A to patch B
/// (different patched bytes); the next `vendor` must wire B's wheel, remove
/// A's uuid dir (`vendor_stale_artifact_removed`) and exit 0. Before the fix
/// it failed `pypi_uv_source_already_exists`,
/// `pypi_lock_source_already_exists` or `pypi_hatch_unsupported` (exit 1)
/// and the project kept installing patch A. `vendor --revert` afterwards
/// restores the user's original files byte for byte.
#[tokio::test]
async fn pyproject_flavors_vendored_revendor_superseding_patch() {
const UUID_B: &str = "5c3e1a2b-7d4f-4e6a-9b8c-1d2e3f4a5b6d";
const PATCHED_B: &[u8] = b"# six\nVERSION = '1.16.0'\nSOCKET_PATCHED = 2\n";
let stages: [(&str, StageFn); 3] = [
("uv", stage_uv),
("script lock", stage_script_lock),
("hatch", stage_hatch),
];
for (flavor, stage) in stages {
let (_tmp, root) = project();
let files = stage(&root);
let originals: Vec<String> = files
.iter()
.map(|f| std::fs::read_to_string(root.join(f)).unwrap())
.collect();
vendor_project(&root, files);

stage_manifest_with(&root, UUID_B, PATCHED_B);
let (code, env) = run_cli(&root, &["vendor"], &[]);
assert_eq!(
code, 0,
"{flavor}: re-vendor to the superseding patch: {env:#}"
);
let rendered = env.to_string();
assert!(!rendered.contains("already_exists"), "{flavor}: {env:#}");
assert!(
rendered.contains("vendor_stale_artifact_removed"),
"{flavor}: patch A's artifact is reclaimed: {env:#}"
);
let wired_b: Vec<String> = files
.iter()
.map(|f| std::fs::read_to_string(root.join(f)).unwrap())
.collect();
for (f, text) in files.iter().zip(&wired_b) {
assert!(!text.contains(UUID), "{flavor}: {f} kept uuid A:\n{text}");
}
assert!(
wired_b
.iter()
.any(|t| t.contains(&format!(".socket/vendor/pypi/{UUID_B}/"))),
"{flavor}: wired to patch B: {wired_b:#?}"
);
assert!(
!root.join(format!(".socket/vendor/pypi/{UUID}")).exists(),
"{flavor}"
);
assert!(
root.join(format!(".socket/vendor/pypi/{UUID_B}")).is_dir(),
"{flavor}"
);
let ledger = std::fs::read_to_string(root.join(".socket/vendor/state.json")).unwrap();
assert!(
ledger.contains(UUID_B) && !ledger.contains(UUID),
"{flavor}: {ledger}"
);

// Re-running is settled: in sync, nothing rewritten.
let (code, env) = run_cli(&root, &["vendor"], &[]);
assert_eq!(code, 0, "{flavor}: {env:#}");
for (f, text) in files.iter().zip(&wired_b) {
assert_eq!(
&std::fs::read_to_string(root.join(f)).unwrap(),
text,
"{flavor}: {f}"
);
}

let (code, env) = run_cli(&root, &["vendor", "--revert"], &[]);
assert_eq!(code, 0, "{flavor}: revert after the re-vendor: {env:#}");
for (f, text) in files.iter().zip(&originals) {
assert_eq!(
&std::fs::read_to_string(root.join(f)).unwrap(),
text,
"{flavor}: {f} restored to the user's original"
);
}
assert!(
!root.join(format!(".socket/vendor/pypi/{UUID_B}")).exists(),
"{flavor}"
);
}
}

/// A Hatch guard unrelated to the old wiring (here the uv installer, which
/// Hatch reports under the same `pypi_hatch_unsupported` code) refuses the
/// superseding patch BEFORE patch A's wiring is unwound: the project files,
/// the ledger and patch A's artifact are left exactly as they were, and no
/// patch B wheel is built.
#[tokio::test]
async fn hatch_unrelated_guard_refuses_superseding_patch_before_unwinding() {
const UUID_B: &str = "5c3e1a2b-7d4f-4e6a-9b8c-1d2e3f4a5b6d";
const PATCHED_B: &[u8] = b"# six\nVERSION = '1.16.0'\nSOCKET_PATCHED = 2\n";
let (_tmp, root) = project();
let files = stage_hatch(&root);
vendor_project(&root, files);
let wired_a: Vec<String> = files
.iter()
.map(|f| std::fs::read_to_string(root.join(f)).unwrap())
.collect();
let ledger_a = std::fs::read_to_string(root.join(".socket/vendor/state.json")).unwrap();

stage_manifest_with(&root, UUID_B, PATCHED_B);
let (code, env) = run_cli(
&root,
&["vendor"],
&[("HATCH_ENV_TYPE_VIRTUAL_UV_PATH", "/usr/bin/uv")],
);
assert_eq!(code, 1, "{env:#}");
let rendered = env.to_string();
assert!(
rendered.contains("pypi_hatch_unsupported") && rendered.contains("pip installer"),
"the installer guard is the reported refusal: {env:#}"
);
for (f, text) in files.iter().zip(&wired_a) {
assert_eq!(
&std::fs::read_to_string(root.join(f)).unwrap(),
text,
"{f} untouched"
);
}
assert_eq!(
std::fs::read_to_string(root.join(".socket/vendor/state.json")).unwrap(),
ledger_a
);
assert!(root.join(format!(".socket/vendor/pypi/{UUID}")).is_dir());
assert!(!root.join(format!(".socket/vendor/pypi/{UUID_B}")).exists());
}

/// The uv lock rewrite needs the hosted wheel's METADATA, fetched only
/// after the takeover reverted the vendored wiring. When it is unavailable
/// the package is left on the unpatched registry release in both modes, so
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
4 changes: 1 addition & 3 deletions crates/socket-patch-core/src/patch/sidecars/maven.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

use std::path::{Path, PathBuf};

use sha1::Digest as _;

use super::{
SidecarAdvisory, SidecarAdvisoryCode, SidecarError, SidecarFile, SidecarFileAction,
SidecarPayload, SidecarSeverity,
Expand All @@ -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)),
}
}
Expand Down
Loading
Loading