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
10 changes: 7 additions & 3 deletions crates/socket-patch-cli/src/commands/bun_preflight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::collections::{HashMap, HashSet};
use std::path::Path;

use socket_patch_core::api::types::PatchSearchResult;
use socket_patch_core::crawlers::Ecosystem;
use socket_patch_core::vendor::load_state;
use socket_patch_core::vendor::state::VendorEntry;

Expand Down Expand Up @@ -67,7 +68,7 @@ impl BunVendorRefusal {
/// ecosystem's backend consults `bun.lock`), minus the already-vendored
/// exemption.
pub(crate) fn applies_to(&self, purl: &str) -> bool {
purl.starts_with("pkg:npm/") && !self.exempt.contains(purl)
Ecosystem::from_purl(purl) == Some(Ecosystem::Npm) && !self.exempt.contains(purl)
}
}

Expand Down Expand Up @@ -113,7 +114,10 @@ async fn preflight_pairs(
pairs: &[(&str, &str)],
ledger: Option<LedgerLoad<'_>>,
) -> Option<BunVendorRefusal> {
if !pairs.iter().any(|(purl, _)| purl.starts_with("pkg:npm/")) {
if !pairs
.iter()
.any(|(purl, _)| Ecosystem::from_purl(purl) == Some(Ecosystem::Npm))
{
return None;
}
// vlt wins the router (DESIGN D2): a Bun lock beside `vlt-lock.json`
Expand Down Expand Up @@ -169,7 +173,7 @@ async fn refusal_with_exemptions(
let lock_parsed = code == "vendor_bun_workspace_unsupported";
let mut exempt = HashSet::new();
for (purl, _) in pairs {
if !purl.starts_with("pkg:npm/") {
if Ecosystem::from_purl(purl) != Some(Ecosystem::Npm) {
continue;
}
// A preserved ledger can outlive its wiring (rollback --preserve-state).
Expand Down
3 changes: 2 additions & 1 deletion crates/socket-patch-cli/src/commands/vlt_preflight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use std::path::Path;

use socket_patch_core::api::types::PatchSearchResult;
use socket_patch_core::crawlers::Ecosystem;
use socket_patch_core::vendor::npm_flavor::{vlt_flavor_change_refusal, vlt_routes};
use socket_patch_core::vendor::vlt_lock::vlt_vendor_preflight;

Expand Down Expand Up @@ -47,7 +48,7 @@ pub(crate) async fn vlt_vendor_preflight_pairs(
) -> Vec<(String, VltVendorRefusal)> {
let npm: Vec<&(&str, &str)> = pairs
.iter()
.filter(|(purl, _)| purl.starts_with("pkg:npm/"))
.filter(|(purl, _)| Ecosystem::from_purl(purl) == Some(Ecosystem::Npm))
.collect();
if npm.is_empty() {
return Vec::new();
Expand Down
4 changes: 2 additions & 2 deletions crates/socket-patch-core/src/crawlers/fuzzy_match.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::crawlers::python_crawler::canonicalize_pypi_name;
use crate::crawlers::types::CrawledPackage;
use crate::crawlers::types::{CrawledPackage, Ecosystem};

/// Match type for sorting results by relevance; declaration order is the
/// ranking (earlier = better). Internal to this module — `fuzzy_match_packages`
Expand Down Expand Up @@ -31,7 +31,7 @@ fn get_full_name(pkg: &CrawledPackage) -> String {

/// Whether `pkg` is a PyPI distribution, whose name is PEP 503-insensitive.
fn is_pypi(pkg: &CrawledPackage) -> bool {
pkg.purl.starts_with("pkg:pypi/")
Ecosystem::from_purl(&pkg.purl) == Some(Ecosystem::Pypi)
}

/// Determine the match type for a package against a query, or `None` if there
Expand Down
161 changes: 161 additions & 0 deletions crates/socket-patch-core/src/crawlers/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,164 @@ mod tests {
assert!(opts.global_prefix.is_none());
}
}

/// One map from purl type to ecosystem (#747): production code asks
/// [`Ecosystem::from_purl`] instead of spelling `starts_with("pkg:<type>/")`.
#[cfg(test)]
mod purl_type_tests {
use super::Ecosystem;
use std::path::{Path, PathBuf};

/// Each former inline prefix and the ecosystem its caller now matches.
const PREFIXES: [(&str, Ecosystem); 9] = [
("pkg:npm/", Ecosystem::Npm),
("pkg:pypi/", Ecosystem::Pypi),
("pkg:cargo/", Ecosystem::Cargo),
("pkg:gem/", Ecosystem::Gem),
("pkg:golang/", Ecosystem::Golang),
("pkg:maven/", Ecosystem::Maven),
("pkg:composer/", Ecosystem::Composer),
("pkg:nuget/", Ecosystem::Nuget),
("pkg:jsr/", Ecosystem::Deno),
];

/// `from_purl(p) == Some(eco)` holds exactly when `p` starts with that
/// ecosystem's prefix, so every migrated check keeps its answer,
/// including on the near misses an inline prefix test also rejects.
#[test]
fn from_purl_matches_each_former_inline_prefix() {
let inputs = [
"pkg:npm/lodash@4.17.21",
"pkg:npm/@types/node@20.0.0",
"pkg:npm/",
"pkg:npm",
"pkg:NPM/lodash@1.0.0",
"pkg:npmx/a@1",
"npm/lodash@1",
" pkg:npm/a@1",
"pkg:pypi/requests@2.31.0?artifact_id=x.whl",
"pkg:pypi",
"pkg:PyPI/requests@2.31.0",
"pkg:cargo/serde@1.0.0",
"pkg:gem/rails@7.1.0?platform=x86_64-linux",
"pkg:golang/github.com/a/b@v1.0.0",
"pkg:golang",
"pkg:maven/org.a/b@1.0?classifier=c&ext=jar",
"pkg:maven:org.a/b@1.0",
"pkg:composer/vendor/pkg@1.0.0",
"pkg:nuget/Newtonsoft.Json@13.0.1",
"pkg:jsr/@std/path@1.0.0",
"pkg:deno/x@1",
"pkg:generic/x@1",
"",
];
for purl in inputs {
for (prefix, eco) in PREFIXES {
assert_eq!(
Ecosystem::from_purl(purl) == Some(eco),
purl.starts_with(prefix),
"{purl:?} against {prefix:?}"
);
}
}
}

/// Files that still spell a purl-type prefix inline, waiting on #747's
/// next slices. The guard is one-sided: it fails
/// only on a file outside this list, so a PR that migrates one of
/// these can't turn `main` red. Drop the entry when you migrate it.
const PENDING_INLINE_PREFIXES: &[&str] = &[
Comment thread
mikolalysenko marked this conversation as resolved.
"cli/src/commands/agent_download.rs",
"cli/src/commands/get.rs",
"cli/src/commands/hosted_unwind.rs",
"cli/src/commands/rollback.rs",
"cli/src/commands/scan/hosted.rs",
"cli/src/commands/scan/hosted/python.rs",
"cli/src/commands/scan/hosted/takeover.rs",
"cli/src/commands/scan/mod.rs",
"cli/src/commands/scan/policy.rs",
"cli/src/commands/scan/vendor_flow.rs",
"cli/src/commands/vendor.rs",
"cli/src/commands/vex.rs",
"cli/src/ecosystem_dispatch.rs",
"core/src/api/client.rs",
"core/src/hosted/engine.rs",
"core/src/hosted/memory/stages.rs",
"core/src/hosted/takeover.rs",
"core/src/patch/apply.rs",
"core/src/patch/jvm_jar.rs",
"core/src/patch/rollback.rs",
"core/src/patch/store_copies.rs",
"core/src/utils/target.rs",
];

/// The production part of a source file: everything before its first
/// in-file test module (a one-line `#[cfg(test)] mod x;` doesn't count).
fn production(text: &str) -> &str {
let marker =
regex::Regex::new(r"(?m)^#\[cfg\(test\)\]\n(?:pub(?:\(crate\))? )?mod \w+ \{").unwrap();
marker.find(text).map_or(text, |m| &text[..m.start()])
}

fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}

#[test]
fn production_code_classifies_purls_through_from_purl() {
let inline = regex::Regex::new(r#"starts_with\("pkg:[A-Za-z0-9.+-]+/"\)"#).unwrap();
let crates = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let mut offenders = Vec::new();
for (krate, label) in [("socket-patch-core", "core"), ("socket-patch-cli", "cli")] {
let src = crates.join(krate).join("src");
// The CLI crate is absent from a packaged core crate.
if !src.is_dir() {
continue;
}
let mut files = Vec::new();
walk(&src, &mut files);
for path in files {
let rel = format!(
"{label}/src/{}",
path.strip_prefix(&src)
.unwrap()
.to_string_lossy()
.replace('\\', "/")
);
// The type map itself, the purl parsers, and test-only files.
if rel == "core/src/crawlers/types.rs"
|| rel == "core/src/utils/purl.rs"
|| rel.ends_with("tests.rs")
|| rel.contains("test_support")
|| rel.contains("oracle")
{
continue;
}
// Windows CI checks out with CRLF.
let text = std::fs::read_to_string(&path)
.unwrap()
.replace("\r\n", "\n");
if inline.is_match(production(&text))
&& !PENDING_INLINE_PREFIXES.contains(&rel.as_str())
{
offenders.push(rel);
}
}
}
offenders.sort();
assert!(
offenders.is_empty(),
"these files test a purl type with an inline \
`starts_with(\"pkg:<type>/\")`: {offenders:?}. Use \
`Ecosystem::from_purl(purl) == Some(Ecosystem::X)` \
(crates/socket-patch-core/src/crawlers/types.rs) instead."
);
}
}
3 changes: 2 additions & 1 deletion crates/socket-patch-core/src/patch/sidecars/coursier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use std::path::{Path, PathBuf};
use sha1::Digest as _;

use super::{SidecarError, SidecarFile, SidecarFileAction, SidecarPayload};
use crate::crawlers::Ecosystem;
use crate::patch::apply::{is_safe_relative_subpath, normalize_file_path};

/// The checksum algorithms Coursier may keep a sidecar for, in the order
Expand Down Expand Up @@ -203,7 +204,7 @@ pub(crate) fn retry_record(
keys: &[String],
failed: &str,
) -> Option<super::SidecarRecord> {
if !package_key.starts_with("pkg:maven/") {
if Ecosystem::from_purl(package_key) != Some(Ecosystem::Maven) {
return None;
}
match fixup_with(pkg_path, keys, resync_if_stale) {
Expand Down
5 changes: 3 additions & 2 deletions crates/socket-patch-core/src/vex/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::crawlers::Ecosystem;
use crate::manifest::schema::{PatchManifest, PatchRecord};
use crate::patch::apply::{verify_file_patch, VerifyStatus};
use crate::vendor::state::{lookup_entry, VendorEntry};
Expand Down Expand Up @@ -193,7 +194,7 @@ pub async fn applied_patches_with_copies(
} else if let Some(copies) = vendor.and_then(|ctx| ctx.hosted.get(purl)) {
verify_hosted_copies(purl, copies, record).await
} else if let Some(paths) = package_copies.get(purl).filter(|p| !p.is_empty()) {
if purl.starts_with("pkg:maven/") {
if Ecosystem::from_purl(purl) == Some(Ecosystem::Maven) {
let mut first_failure = None;
for copy in paths {
if let Err(reason) = verify_patch_record_for(purl, copy, record).await {
Expand Down Expand Up @@ -234,7 +235,7 @@ pub async fn applied_patches_with_copies(
// build resolves the GAV from the committed repository
// only (exclusiveContent), so the cache copy is a
// pristine sibling the build never reads.
let go_cache_copy = purl.starts_with("pkg:golang/");
let go_cache_copy = Ecosystem::from_purl(purl) == Some(Ecosystem::Golang);
let installed = package_copies
.get(purl)
.filter(|_| !go_cache_copy)
Expand Down
Loading