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
37 changes: 37 additions & 0 deletions crates/socket-patch-cli/tests/e2e_vex_lockfile/pip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,40 @@ fn vendored_extras_pin_is_refused_and_nothing_is_attested() {
assert_ne!(out.code, Some(0), "{out}");
assert_no_statement(&out, "vendored extras");
}

/// #1549: a Pipfile.lock whose entry installs the same release from a
/// non-Socket `file` url or `path` wheel contests the requirements wiring,
/// as a registry `version: "==1.2.3"` entry there already does.
#[test]
fn pipfile_lock_direct_reference_contests_the_requirements_wiring() {
let lock = |entry: serde_json::Value| {
serde_json::json!({
"_meta": { "pipfile-spec": 6, "hash": { "sha256": "x" }, "requires": {}, "sources": [] },
"default": { NAME: entry },
"develop": {},
})
.to_string()
};
let hashes = serde_json::json!([format!("sha256:{}", "c".repeat(64))]);
contested_by_a_non_socket_direct_reference(
&flavors()[..1],
"Pipfile.lock",
&[
(
"file url",
lock(serde_json::json!({ "file": upstream_wheel_url(), "hashes": hashes })),
),
(
"path wheel",
lock(serde_json::json!({
"path": format!("./wheels/{NAME}-{VERSION}-py3-none-any.whl"),
"hashes": hashes,
})),
),
(
"registry pin (control)",
lock(serde_json::json!({ "version": format!("=={VERSION}"), "hashes": hashes })),
),
],
);
}
21 changes: 21 additions & 0 deletions crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,24 @@ fn vendored_legacy_installer_is_refused_and_nothing_is_attested() {
assert_ne!(out.code, Some(0), "{out}");
assert_no_statement(&out, "legacy vendored");
}

/// #1549 (mirror case): a requirements.txt line that installs the same
/// release from a non-Socket url or wheel path contests the Pipfile.lock
/// wiring, as a `vexdemo==1.2.3` registry pin there already does.
#[test]
fn requirements_direct_reference_contests_the_lock_wiring() {
let url = upstream_wheel_url();
contested_by_a_non_socket_direct_reference(
&flavors()[..1],
"requirements.txt",
&[
("name @ url", format!("{NAME} @ {url}\n")),
("bare url", format!("{url}\n")),
(
"bare wheel path",
format!("./wheels/{NAME}-{VERSION}-py3-none-any.whl\n"),
),
("registry pin (control)", format!("{NAME}=={VERSION}\n")),
],
);
}
50 changes: 50 additions & 0 deletions crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1349,3 +1349,53 @@ pub fn embedded_apply_and_vendor_vex_attest_a_manifest_less_checkout(flavors: &[
}
}
}

/// #1549: beside the wired lock, ANOTHER install source (`other`, written
/// to `other_rel`) resolves the same release from a non-Socket artifact (a
/// Pipfile.lock `file` url / `path` wheel, a requirements `name @ url`
/// line; a contesting Pipfile.lock gets its Pipfile). Pipenv / pip install
/// that upstream artifact, so the wiring is
/// contested: never attested, `--no-verify` included, and the contest is
/// explained. Without `other` the same checkout attests.
pub fn contested_by_a_non_socket_direct_reference(
flavors: &[Flavor],
other_rel: &str,
others: &[(&str, String)],
) {
for (flavor, mode) in cells(flavors) {
let wired = wired(&flavor, mode);
for (label, other) in others {
let what = format!("{} + {other_rel} {label}", wired.what());
let (_tmp, cwd) = fresh();
wired.restore(&cwd, NOTHING);
let api = api_with(mode.uuid(), PURL);
assert_ok_attested(&vex(&cwd, &vex_run(Some(&api))), mode, &what);
put(&cwd, other_rel, other.as_bytes());
if other_rel == "Pipfile.lock" {
// Pipenv installs from the lock only with its Pipfile.
put(&cwd, "Pipfile", PIPFILE.as_bytes());
}
for no_verify in [false, true] {
let run = crate::vex_e2e_common::VexRun {
no_verify,
..vex_run(Some(&api))
};
let out = vex(&cwd, &run);
let what = format!("{what} no_verify={no_verify}");
assert_ne!(out.code, Some(0), "{what}: {out}");
assert_no_statement(&out, &what);
let text = out.stdout.clone() + &out.stderr;
assert!(
text.contains("patched_ref_unattributable")
&& text.contains(&format!("but {other_rel} resolves the same version")),
"{what}: the contested wiring must be explained: {out}"
);
}
}
}
}

/// The upstream wheel of the fictitious package on a non-Socket host.
pub fn upstream_wheel_url() -> String {
format!("https://files.pythonhosted.org/packages/ab/cd/{NAME}-{VERSION}-py3-none-any.whl")
}
68 changes: 57 additions & 11 deletions crates/socket-patch-core/src/vex/discover/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,12 +453,36 @@ impl UnlockedPin {
/// [`Discovery::resolved_elsewhere`]).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ResolvedElsewhere {
/// Canonical base purl ([`canonical_base_purl`]).
/// Canonical base purl ([`canonical_base_purl`]). Versionless
/// (`pkg:pypi/six`) for an entry whose version can't be read: it may
/// resolve any version of that package
/// ([`Discovery::resolved_elsewhere_any_version`]).
pub purl: String,
/// Root-relative lock file.
pub file: PathBuf,
}

impl ResolvedElsewhere {
/// Whether this entry resolves the canonical base `purl`: the same
/// `name@version`, or any version of `name` for a versionless entry.
fn covers(&self, purl: &str) -> bool {
self.purl == purl || purl_without_version(purl) == Some(self.purl.as_str())
}

/// Whether the entry names no version, so may resolve any.
fn any_version(&self) -> bool {
purl_without_version(&self.purl).is_none()
}
}

/// `pkg:<type>/<name>` of a versioned `pkg:<type>/<name>@<version>`;
/// `None` for a purl with no version (an npm scope's `@` is part of the
/// name).
fn purl_without_version(purl: &str) -> Option<&str> {
let (head, _) = purl.rsplit_once('@')?;
(head.contains('/') && !head.ends_with('/')).then_some(head)
}

/// A lock entry that installs its OWN copy of a package — a `file:`
/// directory or tarball, a user url, git, a registry block no Socket rewrite
/// reached — beside a Socket wiring of the same `name@version` in the SAME
Expand Down Expand Up @@ -901,6 +925,24 @@ impl Discovery {
});
}

/// Record that lock `file` resolves the package `name_purl`
/// (`pkg:<type>/<name>`, no version; `None` is ignored) from a
/// non-Socket source whose version can't be read: a source directory, a
/// VCS checkout, an artifact named for no release. It may install any
/// version, so it is [`Discovery::resolved_elsewhere`] evidence against
/// every version of that package (#1549).
pub(crate) fn resolved_elsewhere_any_version(&mut self, file: &str, name_purl: Option<String>) {
let Some(name_purl) = name_purl else { return };
let purl = canonical_base_purl(&name_purl);
if purl_without_version(&purl).is_some() {
return;
}
self.elsewhere.push(ResolvedElsewhere {
purl,
file: PathBuf::from(file),
});
}

/// Record that lock `file`'s entry `key` installs its own copy of `purl`
/// (`None` is ignored), `how` saying from where. The cross-lock contest
/// only weighs OTHER locks (a lock that wires a package is never its
Expand Down Expand Up @@ -1051,14 +1093,14 @@ impl Discovery {
let tree = self.tree_of(&r.source_file);
self.elsewhere.iter().find(|e| {
let other = self.tree_of(&e.file);
e.purl == r.purl
e.covers(&r.purl)
&& other != tree
&& !wiring.contains(&(e.purl.clone(), other.to_path_buf()))
&& !wiring.contains(&(r.purl.clone(), other.to_path_buf()))
})
})
.flatten();
match other {
Some(e) => contested.push((r, e.file.clone())),
Some(e) => contested.push((r, e.clone())),
None => self.refs.push(r),
}
}
Expand All @@ -1068,21 +1110,25 @@ impl Discovery {
uuid: r.uuid.clone(),
mode: r.mode,
file: r.source_file.clone(),
other: other.clone(),
other: other.file.clone(),
});
let file = r.source_file.to_string_lossy().into_owned();
let resolves = if other.any_version() {
"resolves the package, at a version it doesn't name, from elsewhere"
} else {
"resolves the same version from elsewhere"
};
self.diag(
DIAG_REF_UNATTRIBUTABLE,
&file,
format!(
"{file}: {} is wired to Socket patch {}, but {} resolves the same version \
from elsewhere (not a Socket patch) — which lock the build installs from \
depends on the package manager that runs, so the patch is not attested; \
rewire both locks (re-run `socket-patch scan` / `vendor`) or delete the \
stale one",
"{file}: {} is wired to Socket patch {}, but {} {resolves} (not a Socket \
patch) — which lock the build installs from depends on the package \
manager that runs, so the patch is not attested; rewire both locks \
(re-run `socket-patch scan` / `vendor`) or delete the stale one",
r.purl,
r.uuid,
other.display()
other.file.display()
),
);
self.withhold_rewirable(r);
Expand Down
Loading
Loading