use crate::error::{DepsError, Result};
use crate::parser::DependencySource;
use crate::{ConcreteVersion, EcosystemId, PackageName, VersionReq};
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use std::any::Any;
use std::hash::Hash;
use std::pin::Pin;
type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
pub trait Registry: Send + Sync {
fn get_versions<'a>(
&'a self,
name: &'a PackageName,
) -> BoxFuture<'a, Result<Vec<Box<dyn Version>>>>;
fn get_versions_with<'a>(
&'a self,
name: &'a PackageName,
freshness: crate::freshness::FreshnessSettings,
) -> BoxFuture<'a, Result<Vec<Box<dyn Version>>>> {
let _ = freshness;
self.get_versions(name)
}
fn get_versions_from<'a>(
&'a self,
name: &'a PackageName,
source: &'a DependencySource,
freshness: crate::freshness::FreshnessSettings,
) -> BoxFuture<'a, Result<Vec<Box<dyn Version>>>> {
let _ = source;
self.get_versions_with(name, freshness)
}
fn get_latest_matching<'a>(
&'a self,
name: &'a PackageName,
req: &'a VersionReq,
selection_context: &'a crate::SelectionContext,
) -> BoxFuture<'a, Result<Option<Box<dyn Version>>>>;
fn get_latest_matching_from<'a>(
&'a self,
name: &'a PackageName,
source: &'a DependencySource,
req: &'a VersionReq,
selection_context: &'a crate::SelectionContext,
) -> BoxFuture<'a, Result<Option<Box<dyn Version>>>> {
let _ = source;
self.get_latest_matching(name, req, selection_context)
}
fn search_raw<'a>(
&'a self,
query: &'a str,
limit: usize,
) -> BoxFuture<'a, Result<Vec<Box<dyn Metadata>>>>;
fn search_normalizes_query(&self) -> bool {
false
}
fn select_latest_matching(
&self,
_versions: &[Box<dyn Version>],
_req: &VersionReq,
_selection_context: &crate::SelectionContext,
) -> Option<usize> {
None
}
fn reports_yanked(&self) -> bool {
true
}
fn as_any(&self) -> &dyn Any;
}
impl dyn Registry + '_ {
pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<Box<dyn Metadata>>> {
if crate::redact::is_credential_or_query_bearing(query) {
crate::lsp_helpers::warn_rejected_value("credential_bearing", "registry search", query);
return Ok(vec![]);
}
self.search_raw(query, limit).await
}
}
#[must_use]
pub fn has_default_prerelease_marker(version: &str) -> bool {
let v = version.to_lowercase();
v.contains("-alpha")
|| v.contains("-beta")
|| v.contains("-rc")
|| v.contains("-dev")
|| v.contains("-pre")
|| v.contains("-snapshot")
|| v.contains("-canary")
|| v.contains("-nightly")
}
#[must_use]
pub fn not_found_or(
err: DepsError,
package: &str,
registry: &'static str,
also_not_found: &[u16],
) -> DepsError {
match err {
DepsError::HttpStatus { status, .. }
if status == 404 || also_not_found.contains(&status) =>
{
DepsError::PackageNotFound {
package: package.into(),
registry,
}
}
other => other,
}
}
#[must_use]
pub fn classify_default_registry_url(url: String, default_urls: &[&str]) -> DependencySource {
let normalized = url.trim_end_matches('/');
if default_urls.contains(&normalized) {
DependencySource::Registry
} else {
DependencySource::CustomRegistry { url }
}
}
#[must_use]
pub fn hash_routing_key<'a>(prefix: &str, parts: impl Iterator<Item = &'a str>) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for part in parts {
part.hash(&mut hasher);
}
format!("{prefix}:{:016x}", hasher.finish())
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RemovalStatus {
Available,
AdvisoryDeprecated,
Yanked,
}
impl RemovalStatus {
#[must_use]
pub const fn blocks_resolution(self) -> bool {
matches!(self, Self::Yanked)
}
#[must_use]
pub const fn is_flagged(self) -> bool {
!matches!(self, Self::Available)
}
#[must_use]
pub const fn from_yanked(flag: bool) -> Self {
if flag { Self::Yanked } else { Self::Available }
}
#[must_use]
pub const fn from_advisory(flag: bool) -> Self {
if flag {
Self::AdvisoryDeprecated
} else {
Self::Available
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Deprecation {
pub reason: Option<String>,
pub replacement: Option<String>,
}
pub trait Version: Send + Sync {
fn version_string(&self) -> &ConcreteVersion;
fn removal_status(&self) -> RemovalStatus {
RemovalStatus::Available
}
fn deprecation(&self) -> Option<&Deprecation> {
None
}
fn is_prerelease(&self) -> bool {
has_default_prerelease_marker(self.version_string().as_str())
}
fn features(&self) -> Vec<String> {
vec![]
}
fn as_any(&self) -> &dyn Any;
fn is_stable(&self) -> bool {
!self.removal_status().blocks_resolution() && !self.is_prerelease()
}
fn published_at(&self) -> Option<crate::freshness::PublishTime> {
None
}
fn license(&self) -> &[String] {
&[]
}
}
pub fn find_latest_stable(versions: &[Box<dyn Version>]) -> Option<&dyn Version> {
versions.iter().find(|v| v.is_stable()).map(|v| v.as_ref())
}
#[must_use]
pub fn is_existence_wildcard(req: &crate::VersionReq) -> bool {
is_existence_wildcard_str(req.as_str())
}
#[must_use]
pub fn is_existence_wildcard_str(req: &str) -> bool {
matches!(req.trim(), "" | "*")
}
#[must_use]
pub fn existence_wildcard_req() -> crate::VersionReq {
crate::VersionReq::new("*")
}
#[must_use]
pub fn select_latest_for_existence<T>(
versions: &[T],
as_version: impl Fn(&T) -> &dyn Version,
) -> Option<usize> {
if versions.is_empty() {
return None;
}
Some(
versions
.iter()
.position(|v| {
let v = as_version(v);
!v.removal_status().is_flagged() && !v.is_prerelease()
})
.or_else(|| {
versions
.iter()
.position(|v| !as_version(v).removal_status().blocks_resolution())
})
.unwrap_or(0),
)
}
pub trait Metadata: Send + Sync {
fn name(&self) -> &crate::PackageName;
fn description(&self) -> Option<&str>;
fn repository(&self) -> Option<&str>;
fn documentation(&self) -> Option<&str>;
fn latest_version(&self) -> &ConcreteVersion;
fn as_any(&self) -> &dyn Any;
}
pub const MAX_ALTERNATE_REGISTRIES: usize = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyShape {
Url,
Opaque,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapResult {
Inserted,
AlreadyPresent,
RefusedAtCapacity,
}
pub fn register_capped<K, V>(
map: &DashMap<K, V>,
key: K,
ecosystem: EcosystemId,
key_shape: KeyShape,
make: impl FnOnce() -> V,
) -> CapResult
where
K: Eq + Hash + AsRef<str>,
{
register_capped_with_occupied(map, key, ecosystem, key_shape, make, |_| {})
}
pub fn register_capped_with_occupied<K, V>(
map: &DashMap<K, V>,
key: K,
ecosystem: EcosystemId,
key_shape: KeyShape,
make: impl FnOnce() -> V,
on_occupied: impl FnOnce(&mut V),
) -> CapResult
where
K: Eq + Hash + AsRef<str>,
{
let at_capacity = map.len() >= MAX_ALTERNATE_REGISTRIES;
match map.entry(key) {
Entry::Occupied(mut slot) => {
on_occupied(slot.get_mut());
CapResult::AlreadyPresent
}
Entry::Vacant(slot) => {
if at_capacity {
let key_ref = slot.key().as_ref();
let rendered_key = match key_shape {
KeyShape::Url => crate::redact::RedactedUrl::new(key_ref).into_inner(),
KeyShape::Opaque => key_ref.to_string(),
};
tracing::warn!(
key = %rendered_key,
cap = MAX_ALTERNATE_REGISTRIES,
%ecosystem,
"{ecosystem} alternate registry cap reached; not registering a new entry"
);
return CapResult::RefusedAtCapacity;
}
slot.insert(make());
CapResult::Inserted
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::MockVersion;
struct ProbeRegistry {
search_raw_called: std::sync::atomic::AtomicBool,
}
impl ProbeRegistry {
fn new() -> Self {
Self {
search_raw_called: std::sync::atomic::AtomicBool::new(false),
}
}
fn search_raw_was_called(&self) -> bool {
self.search_raw_called
.load(std::sync::atomic::Ordering::SeqCst)
}
}
impl Registry for ProbeRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a PackageName,
) -> BoxFuture<'a, Result<Vec<Box<dyn Version>>>> {
Box::pin(async move { Ok(vec![]) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a PackageName,
_req: &'a VersionReq,
_selection_context: &'a crate::SelectionContext,
) -> BoxFuture<'a, Result<Option<Box<dyn Version>>>> {
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> BoxFuture<'a, Result<Vec<Box<dyn Metadata>>>> {
self.search_raw_called
.store(true, std::sync::atomic::Ordering::SeqCst);
Box::pin(async move {
let metadata = crate::test_util::MockMetadata::new("probe", "1.0.0");
Ok(vec![Box::new(metadata) as Box<dyn Metadata>])
})
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[tokio::test]
async fn dyn_registry_search_rejects_credential_bearing_query_before_calling_search_raw() {
let probe = ProbeRegistry::new();
let registry: &dyn Registry = &probe;
let results = registry
.search("user:hunter2@registry.example", 10)
.await
.unwrap();
assert!(
results.is_empty(),
"a credential-shaped query must return no results"
);
assert!(
!probe.search_raw_was_called(),
"a credential-shaped query must be rejected before ever calling `search_raw` \
(issue #1215) — redacting the value and searching anyway, then dropping the \
result, would still satisfy the empty-result assertion above but fail this one"
);
}
#[tokio::test]
async fn dyn_registry_search_delegates_to_search_raw_for_a_safe_query() {
let probe = ProbeRegistry::new();
let registry: &dyn Registry = &probe;
let results = registry.search("serde", 10).await.unwrap();
assert!(
!results.is_empty(),
"a safe query must reach `search_raw` and return its result"
);
assert!(probe.search_raw_was_called());
}
#[test]
fn test_version_default_features() {
let version = MockVersion::new("1.0.0").yanked(false);
assert_eq!(version.features(), Vec::<String>::new());
}
#[test]
fn test_version_trait_object() {
let version = MockVersion::new("1.2.3").yanked(false);
let boxed: Box<dyn Version> = Box::new(version);
assert_eq!(boxed.version_string().as_str(), "1.2.3");
assert!(!boxed.removal_status().blocks_resolution());
}
#[test]
fn test_version_downcast() {
let version = MockVersion::new("1.0.0").yanked(true);
let boxed: Box<dyn Version> = Box::new(version);
let any = boxed.as_any();
assert!(any.is::<MockVersion>());
}
struct MockMetadata {
name: crate::PackageName,
latest: ConcreteVersion,
}
impl Metadata for MockMetadata {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn description(&self) -> Option<&str> {
None
}
fn repository(&self) -> Option<&str> {
None
}
fn documentation(&self) -> Option<&str> {
None
}
fn latest_version(&self) -> &ConcreteVersion {
&self.latest
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[test]
fn test_metadata_trait_object() {
let metadata = MockMetadata {
name: crate::PackageName::new("test-package"),
latest: "2.0.0".into(),
};
let boxed: Box<dyn Metadata> = Box::new(metadata);
assert_eq!(boxed.name(), "test-package");
assert_eq!(boxed.latest_version().as_str(), "2.0.0");
assert!(boxed.description().is_none());
assert!(boxed.repository().is_none());
assert!(boxed.documentation().is_none());
}
#[test]
fn test_metadata_with_full_info() {
struct FullMetadata {
name: crate::PackageName,
desc: String,
repo: String,
docs: String,
latest: ConcreteVersion,
}
impl Metadata for FullMetadata {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn description(&self) -> Option<&str> {
Some(&self.desc)
}
fn repository(&self) -> Option<&str> {
Some(&self.repo)
}
fn documentation(&self) -> Option<&str> {
Some(&self.docs)
}
fn latest_version(&self) -> &ConcreteVersion {
&self.latest
}
fn as_any(&self) -> &dyn Any {
self
}
}
let meta = FullMetadata {
name: crate::PackageName::new("serde"),
desc: "Serialization framework".into(),
repo: "https://github.com/serde-rs/serde".into(),
docs: "https://docs.rs/serde".into(),
latest: "1.0.214".into(),
};
assert_eq!(meta.description(), Some("Serialization framework"));
assert_eq!(meta.repository(), Some("https://github.com/serde-rs/serde"));
assert_eq!(meta.documentation(), Some("https://docs.rs/serde"));
}
#[test]
fn test_is_prerelease_alpha() {
let version = MockVersion::new("4.0.0-alpha.13").yanked(false);
assert!(version.is_prerelease());
}
#[test]
fn test_is_prerelease_beta() {
let version = MockVersion::new("2.0.0-beta.1").yanked(false);
assert!(version.is_prerelease());
}
#[test]
fn test_is_prerelease_rc() {
let version = MockVersion::new("1.5.0-rc.2").yanked(false);
assert!(version.is_prerelease());
}
#[test]
fn test_is_prerelease_dev() {
let version = MockVersion::new("3.0.0-dev").yanked(false);
assert!(version.is_prerelease());
}
#[test]
fn test_is_prerelease_canary() {
let version = MockVersion::new("5.0.0-canary").yanked(false);
assert!(version.is_prerelease());
}
#[test]
fn test_is_prerelease_nightly() {
let version = MockVersion::new("6.0.0-nightly").yanked(false);
assert!(version.is_prerelease());
}
#[test]
fn test_is_not_prerelease_stable() {
let version = MockVersion::new("1.2.3").yanked(false);
assert!(!version.is_prerelease());
}
#[test]
fn test_is_not_prerelease_patch() {
let version = MockVersion::new("1.0.214").yanked(false);
assert!(!version.is_prerelease());
}
#[test]
fn test_is_stable_true() {
let version = MockVersion::new("1.0.0").yanked(false);
assert!(version.is_stable());
}
#[test]
fn test_is_stable_false_yanked() {
let version = MockVersion::new("1.0.0").yanked(true);
assert!(!version.is_stable());
}
#[test]
fn test_is_stable_false_prerelease() {
let version = MockVersion::new("1.0.0-alpha.1").yanked(false);
assert!(!version.is_stable());
}
#[test]
fn test_find_latest_stable_skips_prerelease() {
let versions: Vec<Box<dyn Version>> = vec![
Box::new(MockVersion::new("2.0.0-alpha.1").yanked(false)),
Box::new(MockVersion::new("1.5.0").yanked(false)),
];
let latest = super::find_latest_stable(&versions);
assert_eq!(latest.map(|v| v.version_string().as_str()), Some("1.5.0"));
}
#[test]
fn test_find_latest_stable_skips_yanked() {
let versions: Vec<Box<dyn Version>> = vec![
Box::new(MockVersion::new("2.0.0").yanked(true)),
Box::new(MockVersion::new("1.5.0").yanked(false)),
];
let latest = super::find_latest_stable(&versions);
assert_eq!(latest.map(|v| v.version_string().as_str()), Some("1.5.0"));
}
#[test]
fn test_find_latest_stable_returns_first_stable() {
let versions: Vec<Box<dyn Version>> = vec![
Box::new(MockVersion::new("3.0.0-beta.1").yanked(false)),
Box::new(MockVersion::new("2.0.0").yanked(true)),
Box::new(MockVersion::new("1.5.0").yanked(false)),
Box::new(MockVersion::new("1.4.0").yanked(false)),
];
let latest = super::find_latest_stable(&versions);
assert_eq!(latest.map(|v| v.version_string().as_str()), Some("1.5.0"));
}
#[test]
fn test_find_latest_stable_empty_list() {
let versions: Vec<Box<dyn Version>> = vec![];
let latest = super::find_latest_stable(&versions);
assert!(latest.is_none());
}
#[test]
fn test_find_latest_stable_no_stable_versions() {
let versions: Vec<Box<dyn Version>> = vec![
Box::new(MockVersion::new("2.0.0-alpha.1").yanked(false)),
Box::new(MockVersion::new("1.0.0").yanked(true)),
];
let latest = super::find_latest_stable(&versions);
assert!(latest.is_none());
}
struct StatusVersion {
version: ConcreteVersion,
status: RemovalStatus,
}
impl Version for StatusVersion {
fn version_string(&self) -> &ConcreteVersion {
&self.version
}
fn removal_status(&self) -> RemovalStatus {
self.status
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[test]
fn test_is_stable_true_for_advisory_deprecated() {
let version = StatusVersion {
version: "1.0.0".into(),
status: RemovalStatus::AdvisoryDeprecated,
};
assert!(version.is_stable());
}
#[test]
fn test_is_stable_false_for_yanked() {
let version = StatusVersion {
version: "1.0.0".into(),
status: RemovalStatus::Yanked,
};
assert!(!version.is_stable());
}
#[test]
fn test_removal_status_blocks_resolution() {
assert!(!RemovalStatus::Available.blocks_resolution());
assert!(!RemovalStatus::AdvisoryDeprecated.blocks_resolution());
assert!(RemovalStatus::Yanked.blocks_resolution());
}
#[test]
fn test_removal_status_is_flagged() {
assert!(!RemovalStatus::Available.is_flagged());
assert!(RemovalStatus::AdvisoryDeprecated.is_flagged());
assert!(RemovalStatus::Yanked.is_flagged());
}
#[test]
fn test_removal_status_from_yanked() {
assert_eq!(RemovalStatus::from_yanked(true), RemovalStatus::Yanked);
assert_eq!(RemovalStatus::from_yanked(false), RemovalStatus::Available);
}
#[test]
fn test_removal_status_from_advisory() {
assert_eq!(
RemovalStatus::from_advisory(true),
RemovalStatus::AdvisoryDeprecated
);
assert_eq!(
RemovalStatus::from_advisory(false),
RemovalStatus::Available
);
}
#[test]
fn test_hash_routing_key_format() {
let key = hash_routing_key("pypi-chain", ["https://example.test/simple/"].into_iter());
assert!(key.starts_with("pypi-chain:"));
assert_eq!(key.len(), "pypi-chain:".len() + 16);
}
#[test]
fn test_hash_routing_key_deterministic() {
let parts = || ["https://a.test/", "https://b.test/", "true"].into_iter();
assert_eq!(
hash_routing_key("go-proxy", parts()),
hash_routing_key("go-proxy", parts())
);
}
#[test]
fn test_hash_routing_key_distinguishes_prefix() {
let parts = || ["same"].into_iter();
assert_ne!(
hash_routing_key("pypi-chain", parts()),
hash_routing_key("nuget-chain", parts())
);
}
#[test]
fn test_hash_routing_key_distinguishes_parts() {
assert_ne!(
hash_routing_key("chain", ["a", "b"].into_iter()),
hash_routing_key("chain", ["a", "c"].into_iter())
);
}
#[test]
fn test_hash_routing_key_order_sensitive() {
assert_ne!(
hash_routing_key("chain", ["a", "b", "c"].into_iter()),
hash_routing_key("chain", ["c", "b", "a"].into_iter())
);
}
#[test]
fn test_hash_routing_key_only_hashes_what_callers_pass() {
let identity_only =
hash_routing_key("nuget-chain", ["https://feed.test/", "corp"].into_iter());
let with_credential_included = hash_routing_key(
"nuget-chain",
["https://feed.test/", "corp", "hunter2"].into_iter(),
);
assert_ne!(identity_only, with_credential_included);
}
#[cfg(feature = "test-util")]
#[test]
fn test_register_capped_url_shape_redacts_credential_on_cap_reached() {
let map: DashMap<String, usize> = DashMap::new();
for i in 0..MAX_ALTERNATE_REGISTRIES {
assert_eq!(
register_capped(
&map,
format!("https://index{i}.example"),
EcosystemId::Cargo,
KeyShape::Url,
|| i,
),
CapResult::Inserted
);
}
let log = crate::test_util::capture_tracing_output(|| {
register_capped(
&map,
"https://overflow.example/?api_key=SUPERSECRET_TOKEN".to_string(),
EcosystemId::Cargo,
KeyShape::Url,
|| 0,
);
});
assert!(
log.contains("cargo alternate registry cap reached"),
"log: {log}"
);
assert!(!log.contains("SUPERSECRET_TOKEN"), "log: {log}");
assert!(log.contains("overflow.example"), "log: {log}");
}
#[cfg(feature = "test-util")]
#[test]
fn test_register_capped_opaque_shape_is_not_redacted_on_cap_reached() {
let map: DashMap<String, usize> = DashMap::new();
for i in 0..MAX_ALTERNATE_REGISTRIES {
let key = hash_routing_key("test-chain", std::iter::once(i.to_string().as_str()));
assert_eq!(
register_capped(&map, key, EcosystemId::Cargo, KeyShape::Opaque, || i),
CapResult::Inserted
);
}
let overflow_key = hash_routing_key("test-chain", std::iter::once("overflow"));
let log = crate::test_util::capture_tracing_output(|| {
register_capped(
&map,
overflow_key.clone(),
EcosystemId::Cargo,
KeyShape::Opaque,
|| 0,
);
});
assert!(
log.contains("cargo alternate registry cap reached"),
"log: {log}"
);
assert!(
log.contains(&overflow_key),
"opaque key must not be redacted: {log}"
);
assert!(
!log.contains("***"),
"opaque key must not collapse to '***': {log}"
);
}
}