use aube_lockfile::{LocalSource, LockedPackage, LockfileGraph};
use smallvec::SmallVec;
use std::collections::BTreeMap;
use crate::FxHashMap;
use crate::resolve::vulnerable::is_vulnerable;
use crate::semver_util::version_satisfies;
pub struct LockedIndex<'a> {
by_name: FxHashMap<&'a str, SmallVec<[&'a LockedPackage; 1]>>,
}
impl<'a> LockedIndex<'a> {
pub fn new(existing: Option<&'a LockfileGraph>) -> Self {
let mut by_name: FxHashMap<&'a str, SmallVec<[&'a LockedPackage; 1]>> =
FxHashMap::default();
if let Some(graph) = existing {
for pkg in graph.packages.values() {
by_name.entry(pkg.name.as_str()).or_default().push(pkg);
}
}
Self { by_name }
}
fn bucket(&self, name: &str) -> &[&'a LockedPackage] {
self.by_name
.get(name)
.map(SmallVec::as_slice)
.unwrap_or(&[])
}
pub fn find_satisfying(
&self,
name: &str,
range: &str,
registry_name: &str,
vulnerable_ranges: &BTreeMap<String, Vec<String>>,
) -> Option<&'a LockedPackage> {
self.bucket(name).iter().copied().find(|p| {
version_satisfies(&p.version, range)
&& !is_vulnerable(registry_name, &p.version, vulnerable_ranges)
})
}
pub fn find_first_in_range(&self, name: &str, range: &str) -> Option<&'a LockedPackage> {
self.bucket(name)
.iter()
.copied()
.find(|p| version_satisfies(&p.version, range))
}
pub fn find_local_source_integrity(
&self,
name: &str,
version: &str,
local: &LocalSource,
) -> Option<String> {
self.bucket(name)
.iter()
.copied()
.find(|pkg| {
pkg.local_source.as_ref().is_some_and(|old| {
local_sources_match_for_integrity(old, local)
&& (pkg.version == version
|| matches!((old, local), (LocalSource::Git(_), LocalSource::Git(_)))
&& pkg.version == "0.0.0")
})
})
.and_then(|pkg| pkg.integrity.clone())
}
}
fn local_sources_match_for_integrity(old: &LocalSource, new: &LocalSource) -> bool {
match (old, new) {
(LocalSource::Git(old), LocalSource::Git(new)) => {
aube_lockfile::git_commits_match(&old.resolved, &new.resolved)
&& old.subpath == new.subpath
}
_ => old == new,
}
}
#[cfg(test)]
mod tests {
use super::*;
use aube_lockfile::GitSource;
fn locked(name: &str, version: &str) -> LockedPackage {
LockedPackage {
name: name.to_string(),
version: version.to_string(),
..Default::default()
}
}
#[test]
fn index_selects_same_package_as_linear_scan() {
let graph = LockfileGraph {
packages: BTreeMap::from([
("react@17.0.2".to_string(), locked("react", "17.0.2")),
("react@18.2.0".to_string(), locked("react", "18.2.0")),
("lodash@4.17.21".to_string(), locked("lodash", "4.17.21")),
]),
..Default::default()
};
let vuln = BTreeMap::new();
let index = LockedIndex::new(Some(&graph));
let linear = |name: &str, range: &str| {
graph
.packages
.values()
.find(|p| p.name == name && version_satisfies(&p.version, range))
};
for (name, range) in [("react", ">=16"), ("react", "^18"), ("lodash", "^4")] {
let want = linear(name, range);
assert_eq!(
index.find_first_in_range(name, range).map(|p| &p.version),
want.map(|p| &p.version),
"find_first_in_range diverged from linear scan for {name}@{range}",
);
assert_eq!(
index
.find_satisfying(name, range, name, &vuln)
.map(|p| &p.version),
want.map(|p| &p.version),
"find_satisfying diverged from linear scan for {name}@{range}",
);
}
}
#[test]
fn vulnerable_first_match_skipped_only_by_find_satisfying() {
let graph = LockfileGraph {
packages: BTreeMap::from([
("pkg@1.0.0".to_string(), locked("pkg", "1.0.0")),
("pkg@1.5.0".to_string(), locked("pkg", "1.5.0")),
]),
..Default::default()
};
let mut vuln = BTreeMap::new();
vuln.insert("pkg".to_string(), vec!["1.0.0".to_string()]);
let index = LockedIndex::new(Some(&graph));
assert_eq!(
index
.find_satisfying("pkg", "^1", "pkg", &vuln)
.map(|p| p.version.as_str()),
Some("1.5.0"),
);
assert_eq!(
index
.find_first_in_range("pkg", "^1")
.map(|p| p.version.as_str()),
Some("1.0.0"),
);
}
#[test]
fn empty_index_when_no_existing_graph() {
let index = LockedIndex::new(None);
assert!(index.find_first_in_range("anything", "*").is_none());
let vuln = BTreeMap::new();
assert!(
index
.find_satisfying("anything", "*", "anything", &vuln)
.is_none()
);
}
#[test]
fn find_local_source_integrity_matches_resolved_git_commit() {
let source = LocalSource::Git(GitSource {
url: "git+https://github.com/acme/dep.git".to_string(),
committish: Some("main".to_string()),
resolved: "abcdef0123456789abcdef0123456789abcdef01".to_string(),
integrity: None,
subpath: None,
});
let graph = LockfileGraph {
packages: BTreeMap::from([(
"dep@git+https://github.com/acme/dep.git#abcdef0123456789abcdef0123456789abcdef01"
.to_string(),
LockedPackage {
name: "dep".to_string(),
version: "1.0.0".to_string(),
integrity: Some("sha512-old".to_string()),
local_source: Some(source.clone()),
..Default::default()
},
)]),
..Default::default()
};
let index = LockedIndex::new(Some(&graph));
assert_eq!(
index
.find_local_source_integrity("dep", "1.0.0", &source)
.as_deref(),
Some("sha512-old")
);
let changed_commit = LocalSource::Git(GitSource {
resolved: "1111111111111111111111111111111111111111".to_string(),
..match source {
LocalSource::Git(g) => g,
_ => unreachable!(),
}
});
assert!(
index
.find_local_source_integrity("dep", "1.0.0", &changed_commit)
.is_none()
);
}
#[test]
fn find_local_source_integrity_matches_git_by_resolved_commit() {
let old_source = LocalSource::Git(GitSource {
url: "git+ssh://git@github.com/acme/dep.git".to_string(),
committish: None,
resolved: "abcdef0123456789abcdef0123456789abcdef01".to_string(),
integrity: None,
subpath: Some("packages/dep".to_string()),
});
let graph = LockfileGraph {
packages: BTreeMap::from([(
"dep@git+ssh://git@github.com/acme/dep.git#abcdef0123456789abcdef0123456789abcdef01"
.to_string(),
LockedPackage {
name: "dep".to_string(),
version: "1.0.0".to_string(),
integrity: Some("sha512-old".to_string()),
local_source: Some(old_source),
..Default::default()
},
)]),
..Default::default()
};
let resolved_source = LocalSource::Git(GitSource {
url: "https://github.com/acme/dep.git".to_string(),
committish: Some("main".to_string()),
resolved: "abcdef0123456789abcdef0123456789abcdef01".to_string(),
integrity: None,
subpath: Some("packages/dep".to_string()),
});
let index = LockedIndex::new(Some(&graph));
assert_eq!(
index
.find_local_source_integrity("dep", "1.0.0", &resolved_source)
.as_deref(),
Some("sha512-old")
);
}
#[test]
fn find_local_source_integrity_matches_git_abbrev_and_placeholder_version() {
let old_source = LocalSource::Git(GitSource {
url: "git+ssh://git@github.com/acme/dep.git".to_string(),
committish: None,
resolved: "abcdef0".to_string(),
integrity: None,
subpath: None,
});
let graph = LockfileGraph {
packages: BTreeMap::from([(
"dep@git+ssh://git@github.com/acme/dep.git#abcdef0".to_string(),
LockedPackage {
name: "dep".to_string(),
version: "0.0.0".to_string(),
integrity: Some("sha512-old".to_string()),
local_source: Some(old_source),
..Default::default()
},
)]),
..Default::default()
};
let resolved_source = LocalSource::Git(GitSource {
url: "https://github.com/acme/dep.git".to_string(),
committish: Some("main".to_string()),
resolved: "abcdef0123456789abcdef0123456789abcdef01".to_string(),
integrity: None,
subpath: None,
});
let index = LockedIndex::new(Some(&graph));
assert_eq!(
index
.find_local_source_integrity("dep", "1.0.0", &resolved_source)
.as_deref(),
Some("sha512-old")
);
}
}