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
69 changes: 18 additions & 51 deletions crates/socket-patch-core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,10 @@ pub const DEFAULT_PATCH_API_PROXY_URL: &str = "https://patches-api.socket.dev";
/// Default Socket API URL for authenticated access.
pub const DEFAULT_SOCKET_API_URL: &str = "https://api.socket.dev";

/// User-Agent header value for API requests.
///
/// The version segment is derived from the crate version at compile time so it
/// tracks the published release instead of drifting from a hardcoded literal.
/// Server-side analytics and any minimum-version gating rely on this reporting
/// the real version.
/// User-Agent header value for API requests, carrying the compiled crate
/// version.
pub(crate) const USER_AGENT: &str = concat!("SocketPatchCLI/", env!("CARGO_PKG_VERSION"));

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn user_agent_reports_real_crate_version() {
// USER_AGENT must carry the actual compiled crate version, never a
// hardcoded one.
let expected = format!("SocketPatchCLI/{}", env!("CARGO_PKG_VERSION"));
assert_eq!(USER_AGENT, expected);
assert!(USER_AGENT.starts_with("SocketPatchCLI/"));
assert!(
!USER_AGENT.ends_with("/1.0"),
"USER_AGENT must not be stuck at the stale 1.0 version"
);
// The version segment must be non-empty.
let version = USER_AGENT.trim_start_matches("SocketPatchCLI/");
assert!(!version.is_empty(), "version segment must not be empty");
}

#[test]
fn api_urls_are_https_without_trailing_slash() {
for url in [DEFAULT_PATCH_API_PROXY_URL, DEFAULT_SOCKET_API_URL] {
assert!(url.starts_with("https://"), "{url} must use https");
assert!(
!url.ends_with('/'),
"{url} must not end with a trailing slash"
);
}
// The proxy and authenticated API are distinct hosts; swapping them
// would silently send authed traffic to the public proxy (or vice versa).
assert_ne!(DEFAULT_PATCH_API_PROXY_URL, DEFAULT_SOCKET_API_URL);
assert_eq!(
DEFAULT_PATCH_API_PROXY_URL,
"https://patches-api.socket.dev"
);
assert_eq!(DEFAULT_SOCKET_API_URL, "https://api.socket.dev");
}

#[test]
fn manifest_path_is_under_dot_socket() {
assert_eq!(DEFAULT_PATCH_MANIFEST_PATH, ".socket/manifest.json");
}
}

/// The npm-family package managers' shared file names. Which subsystem
/// accepts which of them (the intentionally divergent subsets: binary locks
/// have a native byte reader, `pnpm-lock.yml` is only a pnpm marker) is
Expand Down Expand Up @@ -106,3 +57,19 @@ pub mod npm_family {
/// Workspace globs of vlt <= 0.0.0-12 (`{"packages": ...}`).
pub const VLT_LEGACY_WORKSPACES: &str = "vlt-workspaces.json";
}

#[cfg(test)]
mod tests {
use super::*;

/// Swapping the two would send authenticated traffic to the public proxy
/// (or the reverse); a trailing slash doubles up when a route is joined.
#[test]
fn api_urls_are_distinct_https_without_trailing_slash() {
for url in [DEFAULT_PATCH_API_PROXY_URL, DEFAULT_SOCKET_API_URL] {
assert!(url.starts_with("https://"), "{url}");
assert!(!url.ends_with('/'), "{url}");
}
assert_ne!(DEFAULT_PATCH_API_PROXY_URL, DEFAULT_SOCKET_API_URL);
}
}
21 changes: 7 additions & 14 deletions crates/socket-patch-core/src/utils/cargo_home.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
//! The cargo home cargo itself would use: `$CARGO_HOME`, else
//! `<home>/.cargo` with `<home>` resolved the way cargo's `home` crate
//! resolves it.
//!
//! [`super::fs::home_dir`] reads only `HOME` / `USERPROFILE`, but cargo
//! falls back to the account's passwd entry on Unix when `HOME` is unset
//! (`env -i`, systemd units, stripped CI and container entrypoints). A
//! crawler that stopped at `home_dir` probed no registry cache there and
//! reported the crate lockfile-only while cargo built the unpatched copy
//! from the passwd home's `~/.cargo` (#1135). The cargo-home readers (the
//! crawler, the `[patch]` config chain, the build-cache cleaner and the
//! self-update channel check) go through [`cargo_home`] so they agree with
//! cargo and with each other.
//! resolves it. Unlike `fs::home_dir`, that falls back to the
//! account's passwd entry on Unix when `HOME` is unset (`env -i`, systemd
//! units, stripped CI and container entrypoints), so every cargo-home reader
//! goes through [`cargo_home`] to probe the registry cache cargo builds from.

use std::ffi::OsString;
use std::path::PathBuf;
Expand Down Expand Up @@ -124,8 +117,8 @@ mod tests {
assert_eq!(got, Some(PathBuf::from("/home/u/.cargo")));
}

/// #1135: with `HOME` and `CARGO_HOME` unset cargo builds from the
/// passwd home's `~/.cargo`, so the crawler must probe it too.
/// With `HOME` and `CARGO_HOME` unset cargo builds from the passwd
/// home's `~/.cargo`, so the crawler must probe it too.
#[test]
fn unset_home_var_falls_back_to_passwd_home() {
let got = resolve(None, None, || Some("/root".into()));
Expand Down Expand Up @@ -169,7 +162,7 @@ mod tests {
assert_eq!(got, want);
}

/// #1135 through the process environment: `HOME`, `USERPROFILE` and
/// Through the process environment: `HOME`, `USERPROFILE` and
/// `CARGO_HOME` all unset, or `HOME` empty, still finds the passwd
/// home's `.cargo`.
#[cfg(unix)]
Expand Down
77 changes: 20 additions & 57 deletions crates/socket-patch-core/src/utils/composer_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
//! One composer release reaches the CLI under several spellings. Lockfiles
//! and `installed.json` carry the pretty tag (`v3.0.2`, `3.0.2`), while a
//! patch's base purl may carry the padded `version_normalized` form
//! (`3.0.2.0`) that Socket's SBOM ingestion stores. Comparing them as strings
//! made a patch for `@3.0.2.0` "not found" in a project that locks `3.0.2`,
//! and made `scan --prune` delete it.
//! (`3.0.2.0`) that Socket's SBOM ingestion stores.
//!
//! [`composer_version_normalize`] is a port of composer/semver 3.4
//! `VersionParser::normalize`, the function Composer and Packagist key a
Expand All @@ -19,11 +17,9 @@
//! parts too, since that padding erases whether the lock said `X`, `X.0` or
//! `X.0.0`. Any other spelling Composer rejects keys as itself minus one
//! leading `v`, and is equivalent only to other rejected spellings with the
//! same key. [`composer_version_identity_key`] keeps that space apart with a
//! `\u{1}` sentinel; [`composer_version_key`] (what `PurlKey` uses, so its
//! string stays printable in reports) relies instead on every accepted key
//! being accepted itself, bare and behind a `v`, which
//! `sentinel_free_key_keeps_rejected_spellings_apart` pins over the vectors.
//! same key. That needs no separate key space because every accepted key is
//! accepted itself, bare and behind a `v`, which
//! `key_equality_is_equivalence_over_every_vector_pair` pins over the vectors.
//!
//! Only comparisons go through this module, and purl comparisons reach it
//! only through [`crate::utils::purl_key::PurlKey`]. Stored spellings
Expand Down Expand Up @@ -251,20 +247,12 @@ pub fn composer_version_key(version: &str) -> String {
identity_normalize(version).unwrap_or_else(|| strip_leading_v(version).to_string())
}

/// A key two spellings share exactly when [`composer_versions_equivalent`]
/// holds, for map and set lookups: [`identity_normalize`], else the spelling
/// minus one leading `v` behind a `\u{1}` so that versions Composer rejects
/// get their own key space (depscan's `composerVersionIdentityKey`).
pub fn composer_version_identity_key(version: &str) -> String {
identity_normalize(version).unwrap_or_else(|| format!("\u{1}{}", strip_leading_v(version)))
}

/// Whether two composer version spellings name the same release
/// (`3.0.2` ≡ `v3.0.2` ≡ `3.0.2.0`, `8.1.0-RC1` ≡ `v8.1.0-rc.1`). A spelling
/// Composer rejects matches only another rejected spelling with the same
/// `v`-stripped text.
pub fn composer_versions_equivalent(a: &str, b: &str) -> bool {
composer_version_identity_key(a) == composer_version_identity_key(b)
composer_version_key(a) == composer_version_key(b)
}

#[cfg(test)]
Expand Down Expand Up @@ -325,44 +313,14 @@ mod tests {
assert!(failures.is_empty(), "{}", failures.join("\n"));
}

/// [`composer_versions_equivalent`] (one key, no separate space for
/// spellings Composer rejects) against the spec: two accepted spellings
/// are equivalent when they normalize alike, two rejected ones when their
/// `v`-stripped text is equal, and never one of each. Sound only while
/// every accepted key `N` is itself accepted, bare and behind a `v` (the
/// only text a rejected spelling `N` / `vN` would key as).
#[test]
fn identity_key_equality_is_equivalence_over_every_vector_pair() {
let v = vectors();
let mut inputs: Vec<&str> = v["vectors"]
.as_array()
.unwrap()
.iter()
.map(|case| case["input"].as_str().unwrap())
.collect();
for case in v["equivalence"].as_array().unwrap() {
inputs.push(case["left"].as_str().unwrap());
inputs.push(case["right"].as_str().unwrap());
}
let mut failures = Vec::new();
for a in &inputs {
for b in &inputs {
let keyed = composer_version_identity_key(a) == composer_version_identity_key(b);
let rejected = |x: &str| identity_normalize(x).is_none();
let spec = match (rejected(a), rejected(b)) {
(false, false) => composer_version_key(a) == composer_version_key(b),
(true, true) => strip_leading_v(a) == strip_leading_v(b),
_ => false,
};
if keyed != spec || keyed != composer_versions_equivalent(a, b) {
failures.push(format!("{a:?} vs {b:?}: key {keyed}, spec {spec}"));
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}

/// [`PurlKey`](crate::utils::purl_key::PurlKey) keys on the sentinel-free
/// [`composer_version_key`], which is only sound while a rejected
/// spelling's key can never equal an accepted one's: every accepted key
/// `N` must itself be accepted, bare and behind a `v` (the only text a
/// rejected spelling `N` / `vN` would key as; a `v` only before a digit).
#[test]
fn sentinel_free_key_keeps_rejected_spellings_apart() {
fn key_equality_is_equivalence_over_every_vector_pair() {
let v = vectors();
let mut inputs: Vec<&str> = v["vectors"]
.as_array()
Expand Down Expand Up @@ -392,11 +350,16 @@ mod tests {
}
}
}
let rejected = |x: &str| identity_normalize(x).is_none();
for a in &inputs {
for b in &inputs {
let plain = composer_version_key(a) == composer_version_key(b);
if plain != composer_versions_equivalent(a, b) {
failures.push(format!("{a:?} vs {b:?}: plain key {plain}"));
let spec = match (rejected(a), rejected(b)) {
(false, false) => identity_normalize(a) == identity_normalize(b),
(true, true) => strip_leading_v(a) == strip_leading_v(b),
_ => false,
};
if composer_versions_equivalent(a, b) != spec {
failures.push(format!("{a:?} vs {b:?}: spec {spec}"));
}
}
}
Expand Down
Loading
Loading