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
46 changes: 39 additions & 7 deletions crates/socket-patch-cli/src/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ use socket_patch_core::utils::group_commit::{CommittedFile, GroupCommit};
use socket_patch_core::utils::purl::{canonical_purl, normalize_purl, strip_purl_qualifiers};
use socket_patch_core::utils::socket_dir::remove_tree_and_prune;
use socket_patch_core::vendor::{
self, ecosystem_dir_for_purl, load_state, lock_inventory, lookup_entry, save_state,
save_state_shared, PackageSource, RevertOpts, RevertOutcome, VendorEntry, VendorOutcome,
VendorServiceConfig, VendorState, VendorWarning,
self, ecosystem_dir_for_purl, load_state, lock_inventory, lookup_entry, lookup_entry_kv,
save_state, save_state_shared, PackageSource, RevertOpts, RevertOutcome, VendorEntry,
VendorOutcome, VendorServiceConfig, VendorState, VendorWarning,
};
use socket_patch_core::vex::time::now_rfc3339;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -2010,6 +2010,24 @@ impl StagedSource {
}
}

/// Whether an installed copy that fails `candidate`'s variant probe is
/// still `candidate` itself, superseded: the ledger vendored exactly this
/// package at an OLDER patch uuid (#769), so the venv most likely holds
/// that patch's bytes (`pipenv sync` from the vendored wheel), which are
/// neither the pristine release nor this patch's output. The re-vendor
/// then takes the pristine artifact from the lock / registry / service, as
/// a lock-only checkout does, instead of reporting it not installed.
fn superseded_install(
ledger: &VendorState,
candidate: &str,
record: &PatchRecord,
sole_candidate: bool,
) -> bool {
lookup_entry_kv(&ledger.entries, candidate).is_some_and(|(key, entry)| {
entry.uuid != record.uuid && (sole_candidate || key == candidate)
})
}

#[allow(clippy::too_many_arguments)]
async fn plan_service_downloads(
cwd: &Path,
Expand All @@ -2026,6 +2044,8 @@ async fn plan_service_downloads(
&vendor::pypi::InstalledSiteListings,
),
) -> Vec<socket_patch_core::api::client::PlannedDownload> {
// The loop's stand-in for a superseded install (see there).
let uninstalled = cwd.join(".socket/vendor/.uninstalled");
// Each loop candidate that reaches its backend, in loop order.
let mut reaching: Vec<(&str, &PatchRecord, &Path)> = Vec::new();
let mut handled_bases: HashSet<String> = HashSet::new();
Expand Down Expand Up @@ -2057,14 +2077,19 @@ async fn plan_service_downloads(
&& !matches!(Ecosystem::from_purl(candidate), Some(Ecosystem::Maven));
let ledger_answers_probe =
lookup_entry(&ledger.entries, candidate).is_some_and(|e| e.uuid == record.uuid);
let mut source_path = source.path();
if probe_applicable && !force && !ledger_answers_probe {
if matches!(staged, StagedSource::Installed(_)) {
if let Some((file, info)) = representative_file(&record.files) {
let dir = source.path();
if !variant_matches_installed(Some(
&verify_file_patch(dir, file, info).await.status,
)) {
continue;
if !superseded_install(ledger, candidate, record, candidates.len() == 1)
{
continue;
}
source_path = &uninstalled;
}
}
} else if candidates.len() > 1 && lookup_entry(&ledger.entries, candidate).is_none()
Expand Down Expand Up @@ -2102,7 +2127,7 @@ async fn plan_service_downloads(
if lookup_entry(&ledger.entries, candidate).is_some_and(|e| e.uuid == record.uuid) {
continue;
}
reaching.push((candidate.as_str(), record, source.path()));
reaching.push((candidate.as_str(), record, source_path));
}
}

Expand Down Expand Up @@ -2459,6 +2484,8 @@ pub(crate) async fn vendor_records_reusing(
&& !socket_patch_core::utils::failpoint::switched_off("group_commit"))
.then(|| GroupCommit::begin(&common.cwd));
let mut stale_artifacts: Vec<StaleArtifact> = Vec::new();
// The source of a superseded install (see [`superseded_install`]).
let uninstalled = common.cwd.join(".socket/vendor/.uninstalled");
for (index, (purl, staged)) in all_packages.iter().enumerate() {
let pkg_source = staged.as_source();
let is_variant_eco =
Expand Down Expand Up @@ -2500,6 +2527,7 @@ pub(crate) async fn vendor_records_reusing(
// without downloading the pristine tree just to read one file.
let ledger_answers_probe =
lookup_entry(&state.entries, candidate).is_some_and(|e| e.uuid == record.uuid);
let mut candidate_source = pkg_source;
if probe_applicable && !force && !ledger_answers_probe {
if matches!(staged, StagedSource::Installed(_)) {
if let Some((file, info)) = representative_file(&record.files) {
Expand All @@ -2508,7 +2536,11 @@ pub(crate) async fn vendor_records_reusing(
.await
.status,
)) {
continue;
if !superseded_install(&state, candidate, record, candidates.len() == 1)
{
continue;
}
candidate_source = PackageSource::Installed(&uninstalled);
}
}
} else if candidates.len() > 1 && lookup_entry(&state.entries, candidate).is_none()
Expand Down Expand Up @@ -2815,7 +2847,7 @@ pub(crate) async fn vendor_records_reusing(
));
let outcome = dispatch_vendor_one(
candidate,
pkg_source,
candidate_source,
&common.cwd,
record,
sources,
Expand Down
88 changes: 88 additions & 0 deletions crates/socket-patch-cli/tests/mode_migration_pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,94 @@ async fn pipenv_vendored_to_hosted() {
assert_vendored_to_hosted(&root, files).await;
}

/// A superseding patch for the same release (a fixed patch, or one
/// covering more CVEs).
const UUID_B: &str = "6d4f2b3c-8e5a-4f7b-9c9d-2e3f4a5b6c7d";
const PATCHED_B: &[u8] = b"# six\nVERSION = '1.16.0'\nSOCKET_PATCHED = 2\n";

/// A virtualenv whose six was installed from the vendored wheel of patch
/// A (`pipenv sync` after the first vendor): its files are A's patched
/// bytes, not the pristine release patch B is diffed against.
fn venv_installed_from_patch_a(venv: &Path) {
let site = venv.join("lib/python3.11/site-packages");
let dist = site.join("six-1.16.0.dist-info");
std::fs::create_dir_all(&dist).unwrap();
std::fs::write(site.join("six.py"), PATCHED).unwrap();
std::fs::write(
dist.join("METADATA"),
"Metadata-Version: 2.1\nName: six\nVersion: 1.16.0\n\n",
)
.unwrap();
std::fs::write(
dist.join("WHEEL"),
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py2-none-any\nTag: py3-none-any\n",
)
.unwrap();
std::fs::write(dist.join("INSTALLER"), "pip\n").unwrap();
std::fs::write(
dist.join("RECORD"),
"six.py,,\nsix-1.16.0.dist-info/METADATA,,\nsix-1.16.0.dist-info/WHEEL,,\nsix-1.16.0.dist-info/INSTALLER,,\nsix-1.16.0.dist-info/RECORD,,\n",
)
.unwrap();
}

/// #769: a Pipenv project vendored at patch A moves to the superseding
/// patch B when the manifest offers it, as the `would_revendor` preview
/// and the CLI contract promise, whether the checkout is lock-only or its
/// venv was installed from A's vendored wheel. Pipfile.lock is rewired to
/// B, A's artifact is swept, and reverting B restores the registry pin.
#[tokio::test]
async fn pipenv_revendors_to_a_superseding_patch() {
for venv_present in [false, true] {
let lane = if venv_present {
"venv from A"
} else {
"lock-only"
};
let (_tmp, root) = project();
stage_pipenv(&root);
let registry = std::fs::read_to_string(root.join("Pipfile.lock")).unwrap();
vendor_project(&root, &["Pipfile.lock"]);

let venv = root.join("../patched-venv");
let mut extra: Vec<(&str, &str)> = Vec::new();
if venv_present {
venv_installed_from_patch_a(&venv);
extra.push(("VIRTUAL_ENV", venv.to_str().unwrap()));
}
stage_manifest_with(&root, UUID_B, PATCHED_B);
let (code, env) = run_cli(&root, &["vendor"], &extra);
assert_eq!(code, 0, "{lane}: re-vendor to B: {env:#}");
assert!(
env.to_string().contains("vendor_stale_artifact_removed"),
"{lane}: A's artifact is swept: {env:#}"
);
let lock = std::fs::read_to_string(root.join("Pipfile.lock")).unwrap();
assert!(
lock.contains(&format!(".socket/vendor/pypi/{UUID_B}/")) && !lock.contains(UUID),
"{lane}: Pipfile.lock is rewired to B:\n{lock}"
);
assert!(!root.join(format!(".socket/vendor/pypi/{UUID}")).exists());
let wheels: Vec<_> = std::fs::read_dir(root.join(format!(".socket/vendor/pypi/{UUID_B}")))
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().ends_with(".whl"))
.collect();
assert_eq!(wheels.len(), 1, "{lane}: B's wheel is vendored");

let (code, env) = run_cli(&root, &["vendor", "--revert"], &extra);
assert_eq!(code, 0, "{lane}: revert B: {env:#}");
let reverted: Value =
serde_json::from_str(&std::fs::read_to_string(root.join("Pipfile.lock")).unwrap())
.unwrap();
let registry: Value = serde_json::from_str(&registry).unwrap();
assert_eq!(
reverted, registry,
"{lane}: revert restores the registry pin"
);
}
}

const UV_LOCK: &str = r#"version = 1
revision = 2
requires-python = ">=3.9"
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
4 changes: 2 additions & 2 deletions crates/socket-patch-core/src/vendor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ pub use source::PackageSource;
pub(crate) use npm_common::is_safe_npm_name;
pub use pypi_requirements::requirements_include_names;
pub use state::{
carry_forward_wiring, load_state, lookup_entry, purl_keys_cover, save_state, save_state_shared,
VendorEntry, VendorState, VENDOR_STATE_REL,
carry_forward_wiring, load_state, lookup_entry, lookup_entry_kv, purl_keys_cover, save_state,
save_state_shared, VendorEntry, VendorState, VENDOR_STATE_REL,
};
pub use verify::{
artifact_is_file_shaped, check_vendored_artifact, compute_dir_inventory,
Expand Down
Loading
Loading