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
14 changes: 7 additions & 7 deletions crates/socket-patch-cli/src/commands/hosted_bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! Stdin: `{"files": {path: text}, "binaryFiles"?: {path: base64},
//! "presentOnly"?: [path], "symlinks"?: [path], "projectRoots"?: [dir],
//! "pipenvMajor"?: n, "batchSize"?: n, "noSocketYml"?: bool,
//! "minSeverity"?: severity, "policyPaths"?: [path],
//! "minSeverity"?: severity, "policyPaths"?: [path], "policySha256"?: hex,
//! "maxNewPatches"?: n | "none", "maxNewPatchesCap"?: n,
//! "inFlightPatches"?: [purl]}`. Stdout: the engine result
//! (`HostedScanResult`, binary contents base64), or
Expand Down Expand Up @@ -122,7 +122,7 @@ pub async fn run(args: HostedBundleArgs) -> i32 {
print_error("invalid_bundle", &format!("cannot read stdin: {e}"));
return 2;
}
let bundle: Bundle = match serde_json::from_str(&raw) {
let mut bundle: Bundle = match serde_json::from_str(&raw) {
Ok(bundle) => bundle,
Err(e) => {
print_error("invalid_bundle", &e.to_string());
Expand All @@ -137,14 +137,14 @@ pub async fn run(args: HostedBundleArgs) -> i32 {
pipenv_major: bundle.pipenv_major,
trust_lockfile_config: Some(!common.no_trust_lockfile_config),
npm_allow_remote_config: Some(!common.no_npm_allow_remote_config),
project_roots: bundle.project_roots.clone(),
project_roots: bundle.project_roots.take(),
no_socket_yml: bundle.no_socket_yml,
min_severity: bundle.min_severity.clone(),
policy_paths: bundle.policy_paths.clone(),
policy_sha256: bundle.policy_sha256.clone(),
min_severity: bundle.min_severity.take(),
policy_paths: bundle.policy_paths.take(),
policy_sha256: bundle.policy_sha256.take(),
max_new_patches: bundle.max_new_patches,
max_new_patches_cap: bundle.max_new_patches_cap,
in_flight_patches: bundle.in_flight_patches.clone(),
in_flight_patches: bundle.in_flight_patches.take(),
..HostedScanOptions::default()
};
let input = match build_input(bundle, options) {
Expand Down
24 changes: 12 additions & 12 deletions crates/socket-patch-cli/src/commands/hosted_unwind.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! The hosted unwind leg: restore in-scope hosted pins to their default
//! upstream registry entries. A helper module, not a command: `rollback`,
//! `remove` and `vendor` (a pre-v5 vendored-over-hosted revert) all run it
//! without importing each other.
//! `remove`, scan's withdrawn-patch restore and `vendor` (a pre-v5
//! vendored-over-hosted revert) all run it without importing each other.

use socket_patch_core::patch::redirect::upstream::HostedPin;

Expand All @@ -12,8 +12,6 @@ use crate::args::GlobalArgs;
pub(crate) struct HostedLegOutcome {
pub(crate) reverted: Vec<String>,
pub(crate) failed: Vec<(String, String)>,
/// Scoped targets whose ecosystem has no per-purl hosted revert.
pub(crate) unsupported: Vec<String>,
pub(crate) warnings: Vec<(String, String)>,
pub(crate) edited_files: std::collections::BTreeSet<String>,
}
Expand Down Expand Up @@ -42,7 +40,7 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
use socket_patch_core::patch::redirect::upstream::{
restore_upstream, PinStatus, RestoreOptions,
};
// The pins are the manifest's project's (#745): restore them there.
// The pins are the manifest's project's: restore them there.
let rooted = common.at_project_root();
let common: &GlobalArgs = &rooted;

Expand Down Expand Up @@ -76,7 +74,11 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
// rebuilt registry record is not byte-exact for every lock.
bun_lockb: false,
};
let live_before = socket_patch_core::vendor::bun_lock::live_user_patch_keys(&common.cwd).await;
let live_before = if common.dry_run {
Default::default()
} else {
socket_patch_core::vendor::bun_lock::live_user_patch_keys(&common.cwd).await
};
let outcome = restore_upstream(&common.cwd, pins, &opts).await;
for pin in &outcome.pins {
match &pin.status {
Expand Down Expand Up @@ -115,7 +117,7 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
out.edited_files
.extend(outcome.reverted_files.iter().cloned());
// Bun's hoisted linker keeps the patched copies of the pins it no
// longer pins (#764): say so, with the install that does reinstall.
// longer pins: say so, with the install that does reinstall.
// A dry run says it too, so the preview's reinstall note names
// `bun install --force` like the real run's.
if outcome.flush_error.is_none() {
Expand All @@ -133,7 +135,7 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
));
}
// A `bun patch` keyed by a hosted URL the restore took out of the
// lock stops applying (#1019).
// lock stops applying.
if !common.dry_run {
let live_after = bun_lock::live_user_patch_keys(&common.cwd).await;
for key in live_before.difference(&live_after) {
Expand All @@ -143,10 +145,8 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
));
}
}
}
// PDM, uv and Pipenv keep a same-version install through a plain sync
// (#477): name the reinstall that restores the upstream bytes.
if outcome.flush_error.is_none() {
// PDM, uv and Pipenv keep a same-version install through a plain
// sync: name the reinstall that restores the upstream bytes.
use crate::commands::pypi_reinstall::{advisory, Tool, HOSTED_CODE};
let unwound: Vec<(String, Tool)> = outcome
.restored()
Expand Down
99 changes: 38 additions & 61 deletions crates/socket-patch-cli/src/commands/list.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::Args;
use socket_patch_core::ledgers::Store;
use socket_patch_core::manifest::schema::{PatchManifest, PatchRecord};
use socket_patch_core::patch::redirect::upstream::HostedPin;
use socket_patch_core::patch::redirect::RedirectState;
Expand All @@ -16,20 +17,12 @@ pub struct ListArgs {
pub common: GlobalArgs,
}

// Where a listed record lives (see `socket_patch_core::ledgers`): the
// manifest (agent mode); the hosted redirect ledger, where `scan --mode
// hosted` recorded its patches; or the vendor ledger, where vendored mode
// keeps every `scan`/`get --mode vendored` patch as a `detached` entry's
// embedded record (a standalone `vendor` entry's fallback copy lists too
// once no manifest entry covers it — the checkout `vex` attests from it).
use socket_patch_core::ledgers::Store as Source;

/// The display order of one purl's copies: manifest, hosted, vendored.
fn display_rank(source: Source) -> u8 {
fn display_rank(source: Store) -> u8 {
match source {
Source::Manifest => 0,
Source::Hosted => 1,
Source::Vendored => 2,
Store::Manifest => 0,
Store::Hosted => 1,
Store::Vendored => 2,
}
}

Expand All @@ -38,18 +31,18 @@ fn display_rank(source: Source) -> u8 {
/// `HOSTED_MODE_LABEL`'s docs) — or `None` for a manifest entry or a hosted
/// pin (which has no ledger; see [`ListEntry::lockfiles`]). Shared by the
/// JSON `details` and the human `Mode:` line.
fn ledger_label(source: Source) -> Option<(&'static str, &'static str)> {
fn ledger_label(source: Store) -> Option<(&'static str, &'static str)> {
match source {
Source::Manifest | Source::Hosted => None,
Source::Vendored => Some((crate::commands::VENDORED_MODE_LABEL, VENDOR_STATE_REL)),
Store::Manifest | Store::Hosted => None,
Store::Vendored => Some((crate::commands::VENDORED_MODE_LABEL, VENDOR_STATE_REL)),
}
}

/// One listable patch record with its provenance.
struct ListEntry<'a> {
purl: &'a str,
record: &'a PatchRecord,
source: Source,
source: Store,
/// The lockfiles wiring a hosted pin (empty for the other sources).
lockfiles: &'a [String],
}
Expand Down Expand Up @@ -78,15 +71,7 @@ impl HostedListing {
.find(|(k, r)| PurlKey::same(k, &pin.purl) && r.uuid == pin.uuid)
.map(|(_, r)| r.clone())
})
.unwrap_or_else(|| PatchRecord {
uuid: pin.uuid.clone(),
exported_at: String::new(),
files: Default::default(),
vulnerabilities: Default::default(),
description: String::new(),
license: String::new(),
tier: String::new(),
});
.unwrap_or_else(|| socket_patch_core::ledgers::uuid_only_record(&pin.uuid));
HostedListing {
purl: pin.purl.clone(),
record,
Expand Down Expand Up @@ -129,7 +114,7 @@ fn combined_entries<'a>(
entries.extend(hosted.iter().map(|h| ListEntry {
purl: &h.purl,
record: &h.record,
source: Source::Hosted,
source: Store::Hosted,
lockfiles: &h.lockfiles,
}));
entries.sort_by(|a, b| {
Expand Down Expand Up @@ -195,7 +180,7 @@ fn build_list_envelope(entries: &[ListEntry<'_>]) -> Envelope {
details["mode"] = serde_json::json!(mode);
details["ledger"] = serde_json::json!(ledger);
}
if entry.source == Source::Hosted {
if entry.source == Store::Hosted {
details["mode"] = serde_json::json!(crate::commands::HOSTED_MODE_LABEL);
details["lockfiles"] = serde_json::json!(entry.lockfiles);
}
Expand Down Expand Up @@ -264,7 +249,7 @@ fn format_entry(entry: &ListEntry<'_>, color: bool) -> String {
// Same labeling rule as the JSON details.
lines.push(format!(" Mode: {mode} (recorded in {ledger})"));
}
if entry.source == Source::Hosted {
if entry.source == Store::Hosted {
lines.push(format!(
" Mode: {} (wired in {})",
crate::commands::HOSTED_MODE_LABEL,
Expand Down Expand Up @@ -334,11 +319,6 @@ pub async fn run(args: ListArgs) -> i32 {
apply_env_toggles(&args.common);
let manifest_path = args.common.resolved_manifest_path();

// `read_manifest` is the single source of truth for the three error
// states: `Ok(None)` (file absent), `Err(InvalidData)` (present but
// unparseable), and any other `Err` (genuine I/O failure). No stat
// pre-check: it would report any stat failure as `manifest_not_found`
// and open a TOCTOU window.
// One load of the three stores, all from the SAME project as the
// manifest (see below); each keeps list's own posture.
let ctx = crate::commands::context::ProjectContext::new(&args.common);
Expand Down Expand Up @@ -370,10 +350,10 @@ pub async fn run(args: ListArgs) -> i32 {
Ok(state) => state.as_ref(),
Err(corrupt) => {
if args.common.json {
warnings.push(RunWarning {
code: "redirect_ledger_corrupt".to_string(),
detail: corrupt.to_string(),
});
warnings.push(RunWarning::new(
"redirect_ledger_corrupt",
corrupt.to_string(),
));
} else if !args.common.silent {
eprintln!("Warning: {corrupt}");
}
Expand All @@ -387,10 +367,7 @@ pub async fn run(args: ListArgs) -> i32 {
let contested = inventory.contested_refusal();
if let Some(detail) = &contested {
if args.common.json {
warnings.push(RunWarning {
code: "hosted_wiring_contested".to_string(),
detail: detail.clone(),
});
warnings.push(RunWarning::new("hosted_wiring_contested", detail.clone()));
} else if !args.common.silent {
eprintln!("Warning: {}", crate::ui::sentence_case(detail));
}
Expand Down Expand Up @@ -426,15 +403,24 @@ pub async fn run(args: ListArgs) -> i32 {
)
};
if args.common.json {
warnings.push(RunWarning {
code: "hosted_resolution_orphaned".to_string(),
detail,
});
warnings.push(RunWarning::new("hosted_resolution_orphaned", detail));
} else if !args.common.silent {
eprintln!("Warning: {}", crate::ui::sentence_case(&detail));
}
}
let vendor_state = crate::commands::vendor_state_lenient(&loaded.vendor, args.common.silent);
// A broken vendor ledger degrades to nothing-to-list here (every path
// that writes or attests from it fails closed instead), but says so.
let vendor_state = match &loaded.vendor {
Ok(state) => Some(state),
Err(e) => {
if !args.common.silent {
eprintln!(
"Warning: unreadable vendor ledger ({e}); its vendored patches are not listed"
);
}
None
}
};

// `combined_entries` folds only real records in (a record-less legacy
// vendor entry asserts no patch), so entry emptiness is the whole exit
Expand Down Expand Up @@ -799,17 +785,17 @@ mod tests {
assert_eq!(v["events"][0]["details"]["mode"], "hosted");
}

/// A vendor-ledger entry: `detached` with the embedded record when
/// `record` is given (the manifest-free vendored posture), a legacy
/// manifest-tracked entry (no record of its own) otherwise. Built from
/// the on-disk JSON shape so the fixture follows the ledger schema.
fn as_state(entries: &HashMap<String, VendorEntry>) -> VendorState {
VendorState {
entries: entries.clone(),
..VendorState::new()
}
}

/// A vendor-ledger entry: `detached` with the embedded record when
/// `record` is given (the manifest-free vendored posture), a legacy
/// manifest-tracked entry (no record of its own) otherwise. Built from
/// the on-disk JSON shape so the fixture follows the ledger schema.
fn vendor_entry(purl: &str, record: Option<PatchRecord>) -> VendorEntry {
serde_json::from_value(serde_json::json!({
"ecosystem": "npm",
Expand Down Expand Up @@ -877,7 +863,7 @@ mod tests {
("pkg:npm/minimist@1.2.2", "vendored"),
("pkg:npm/zzz-vendored@1.0.0", "vendored"),
],
"purl-sorted, manifest < hosted < vendored on a tie, record-less entries skipped: {v}"
"purl-sorted, manifest < hosted < vendored on a tie, record-less entries skipped: {v}"
);
let events = v["events"].as_array().unwrap();
assert_eq!(
Expand Down Expand Up @@ -982,15 +968,6 @@ mod tests {
);
}

#[test]
fn ordering_is_deterministic_across_builds() {
// Two independent builds of the same manifest must be byte-identical.
let manifest = multi_entry_manifest();
let a = manifest_envelope(&manifest).to_pretty_json();
let b = manifest_envelope(&manifest).to_pretty_json();
assert_eq!(a, b);
}

fn record_with(description: &str, license: &str, severity: &str, summary: &str) -> PatchRecord {
let mut rec = sample_manifest().patches["pkg:npm/minimist@1.2.2"].clone();
rec.description = description.to_string();
Expand All @@ -1005,7 +982,7 @@ mod tests {
ListEntry {
purl,
record,
source: Source::Manifest,
source: Store::Manifest,
lockfiles: &[],
}
}
Expand Down
Loading
Loading