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
29 changes: 13 additions & 16 deletions crates/socket-patch-core/src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::api::types::*;
use crate::api::vendor_prefetch::VendorPrefetch;
pub use crate::api::vendor_prefetch::VendorPrefetchGuard;
use crate::constants::USER_AGENT as USER_AGENT_VALUE;
use crate::utils::digest::is_hex;
use crate::utils::env_compat::{is_debug_enabled, is_offline_env, proxy_url_from_env};
use crate::utils::notice::{notice_once, Notice};
use crate::utils::socket_cli_config;
Expand Down Expand Up @@ -1055,7 +1056,7 @@ impl ApiClient {
/// slug are available, otherwise falls back to the public proxy.
pub async fn fetch_blob(&self, hash: &str) -> Result<Option<BinaryBody>, ApiError> {
// Validate hash format: SHA-256 = 64 hex characters
if !is_valid_sha256_hex(hash) {
if !is_hex(hash, 64) {
return Err(ApiError::InvalidHash(format!(
"Invalid hash format: {}. Expected SHA256 hash (64 hex characters).",
hash
Expand Down Expand Up @@ -2541,11 +2542,6 @@ fn truncate_to_chars(s: &str, max_chars: usize) -> String {
format!("{}...", truncated)
}

/// Validate that a string is a 64-character hex string (SHA-256).
fn is_valid_sha256_hex(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
}

/// Validate the standard 8-4-4-4-12 UUID hex grouping.
fn is_valid_uuid(s: &str) -> bool {
let parts: Vec<&str> = s.split('-').collect();
Expand Down Expand Up @@ -3120,13 +3116,14 @@ mod tests {
#[test]
fn test_is_valid_sha256_hex() {
let valid = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
assert!(is_valid_sha256_hex(valid));
assert!(is_hex(valid, 64));

// Too short
assert!(!is_valid_sha256_hex("abcdef"));
assert!(!is_hex("abcdef", 64));
// Non-hex
assert!(!is_valid_sha256_hex(
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
assert!(!is_hex(
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
64
));
}

Expand Down Expand Up @@ -3232,7 +3229,7 @@ mod tests {

/// `fetch_blob` must reject a malformed hash *before* any network I/O:
/// the client points at a closed port, so a regression that bypasses the
/// `is_valid_sha256_hex` guard surfaces as `ApiError::Network` instead
/// `is_hex(hash, 64)` guard surfaces as `ApiError::Network` instead
/// of `InvalidHash` (mirrors `invalid_uuid_is_failed_without_network`;
/// `fetch_diff`'s twin guard is already covered).
#[tokio::test]
Expand Down Expand Up @@ -3635,31 +3632,31 @@ mod tests {
#[test]
fn test_sha256_uppercase_valid() {
let upper = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
assert!(is_valid_sha256_hex(upper));
assert!(is_hex(upper, 64));
}

#[test]
fn test_sha256_65_chars_invalid() {
let too_long = "a".repeat(65);
assert!(!is_valid_sha256_hex(&too_long));
assert!(!is_hex(&too_long, 64));
}

#[test]
fn test_sha256_63_chars_invalid() {
let too_short = "a".repeat(63);
assert!(!is_valid_sha256_hex(&too_short));
assert!(!is_hex(&too_short, 64));
}

#[test]
fn test_sha256_empty_invalid() {
assert!(!is_valid_sha256_hex(""));
assert!(!is_hex("", 64));
}

#[test]
fn test_sha256_mixed_case_valid() {
let mixed = "aAbBcCdDeEfF0123456789aAbBcCdDeEfF0123456789aAbBcCdDeEfF01234567";
assert_eq!(mixed.len(), 64);
assert!(is_valid_sha256_hex(mixed));
assert!(is_hex(mixed, 64));
}

// ── UUID validation tests ───────────────────────────────────────
Expand Down
6 changes: 3 additions & 3 deletions crates/socket-patch-core/src/hosted/npm_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub async fn fetch_hosted_npm_manifest(
#[cfg(test)]
mod tests {
use super::*;
use crate::patch::redirect::vlt_preflight::sha512_sri;
use crate::utils::digest::sha512_sri_of;

fn tgz(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut builder = tar::Builder::new(flate2::write::GzEncoder::new(
Expand All @@ -67,7 +67,7 @@ mod tests {
fn decodes_the_manifest_and_checks_the_sha512() {
let manifest = br#"{"name":"uuid","bin":{"uuid":"./dist/bin/uuid"}}"#;
let bytes = tgz(&[("package/package.json", manifest)]);
let sri = sha512_sri(&bytes);
let sri = sha512_sri_of(&bytes);
assert_eq!(
decode_hosted_npm_manifest(&bytes, Some(&sri))
.unwrap()
Expand All @@ -78,7 +78,7 @@ mod tests {
decode_hosted_npm_manifest(&bytes, None).unwrap().as_bytes(),
manifest
);
let other = sha512_sri(b"other");
let other = sha512_sri_of(b"other");
assert!(decode_hosted_npm_manifest(&bytes, Some(&other))
.unwrap_err()
.contains("sha512"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,7 @@ impl UpstreamClient {
if let Some(sri) = dist.integrity.as_deref() {
crate::vendor::registry_fetch::verify_sri(&bytes, sri)?;
} else if let Some(sha1) = dist.shasum.as_deref() {
use sha1::Digest as _;
if hex::encode(sha1::Sha1::digest(&bytes)) != sha1 {
if crate::utils::digest::sha1_hex_of(&bytes) != sha1 {
return Err("registry archive checksum mismatch".into());
}
} else {
Expand Down Expand Up @@ -659,7 +658,7 @@ fn gosumdb_base(module: &str) -> Option<String> {
pub(crate) fn go_mod_h1(go_mod: &[u8]) -> String {
use base64::Engine as _;
use sha2::{Digest, Sha256};
let file_sum = hex::encode(Sha256::digest(go_mod));
let file_sum = crate::utils::digest::sha256_hex_of(go_mod);
let summary = format!("{file_sum} go.mod\n");
format!(
"h1:{}",
Expand Down
27 changes: 10 additions & 17 deletions crates/socket-patch-core/src/patch/redirect/vlt_preflight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::time::Duration;

use base64::Engine as _;
use sha2::{Digest, Sha512};
use crate::utils::digest::sha512_sri_of;

use super::{vlt, DepOverride};
use crate::api::client::{ApiClient, MAX_VENDOR_PACKAGE_BYTES};
Expand Down Expand Up @@ -66,14 +65,6 @@ impl ArtifactProbe {
}
}

/// The SRI form of `bytes`' sha512.
pub fn sha512_sri(bytes: &[u8]) -> String {
format!(
"sha512-{}",
base64::engine::general_purpose::STANDARD.encode(Sha512::digest(bytes))
)
}

fn fetch_error(error: impl Into<String>) -> ArtifactProbe {
ArtifactProbe {
error: Some(error.into()),
Expand Down Expand Up @@ -131,7 +122,7 @@ async fn fetch_capped(client: &reqwest::Client, url: &str, max: u64) -> Artifact
Ok(bytes) => ArtifactProbe {
status: Some(status),
content_encoding,
sha512: Some(sha512_sri(&bytes)),
sha512: Some(sha512_sri_of(&bytes)),
body: Some(bytes),
error: None,
},
Expand Down Expand Up @@ -270,7 +261,7 @@ mod tests {
.mount(&server)
.await;
let probe = probe_of(&server, "/a.tgz").await;
assert_eq!(probe.failure(&sha512_sri(BODY)), None);
assert_eq!(probe.failure(&sha512_sri_of(BODY)), None);
assert_eq!(probe.body.as_deref(), Some(BODY));
}

Expand All @@ -286,7 +277,9 @@ mod tests {
.mount(&server)
.await;
assert_eq!(
probe_of(&server, "/a.tgz").await.failure(&sha512_sri(BODY)),
probe_of(&server, "/a.tgz")
.await
.failure(&sha512_sri_of(BODY)),
None
);
}
Expand Down Expand Up @@ -319,7 +312,7 @@ mod tests {
.respond_with(ResponseTemplate::new(500))
.mount(&server)
.await;
let expected = sha512_sri(BODY);
let expected = sha512_sri_of(BODY);

let gz_probe = probe_of(&server, "/gz.tgz").await;
assert_eq!(
Expand All @@ -328,7 +321,7 @@ mod tests {
);
assert_eq!(
gz_probe.sha512,
Some(sha512_sri(&gz)),
Some(sha512_sri_of(&gz)),
"the body is hashed as received, never decoded"
);
assert_eq!(
Expand Down Expand Up @@ -411,7 +404,7 @@ mod tests {
.respond_with(ResponseTemplate::new(200).set_body_bytes(BODY))
.mount(&server)
.await;
let expected = sha512_sri(BODY);
let expected = sha512_sri_of(BODY);
assert_eq!(probe_of(&server, "/r1").await.failure(&expected), None);
assert!(probe_of(&server, "/r0")
.await
Expand All @@ -430,7 +423,7 @@ mod tests {
let probe = fetch_capped(api(None).plain_http(), &url, 4).await;
assert!(
probe
.failure(&sha512_sri(BODY))
.failure(&sha512_sri_of(BODY))
.is_some_and(|r| r.starts_with("fetch error ") && r.contains("too large")),
"{probe:?}"
);
Expand Down
4 changes: 1 addition & 3 deletions crates/socket-patch-core/src/policy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ use std::collections::BTreeMap;
use std::io::Read;
use std::path::{Path, PathBuf};

use sha2::{Digest, Sha256};

use crate::api::ranking::max_severity_order;
use crate::api::types::PatchSearchResult;
use crate::crawlers::Ecosystem;
Expand Down Expand Up @@ -579,7 +577,7 @@ impl SelectionPolicy {
Some((name, bytes, parsed)) if !parsed.empty => {
let source = PolicySource::File {
path: name.to_string(),
sha256: hex::encode(Sha256::digest(&bytes)),
sha256: crate::utils::digest::sha256_hex_of(&bytes),
};
let block = parsed.patches.clone().unwrap_or_default();
Self::from_parts(name, source, &parsed.project_ignore_paths, &block)?
Expand Down
1 change: 1 addition & 0 deletions crates/socket-patch-core/src/policy/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use sha2::{Digest, Sha256};

fn mem(files: &[(&str, &str)]) -> MemoryPolicyFs {
let mut fs = MemoryPolicyFs::default();
Expand Down
4 changes: 1 addition & 3 deletions crates/socket-patch-core/src/update/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
use std::io::Read;
use std::path::{Path, PathBuf};

use sha2::{Digest, Sha256};

use super::release::{UpdateEndpoints, UpdateTimeouts};
use super::UpdateError;
use crate::utils::http::read_capped;
Expand Down Expand Up @@ -345,7 +343,7 @@ pub async fn download_and_stage(
let archive = fetch_archive(endpoints, timeouts, version, asset).await?;

// 3. Checksum BEFORE extraction.
let actual_sha = hex::encode(Sha256::digest(&archive));
let actual_sha = crate::utils::digest::sha256_hex_of(&archive);
if actual_sha != expected_sha {
return Err(UpdateError::ChecksumMismatch {
asset: asset.to_string(),
Expand Down
Loading
Loading