#![doc = include_str!("../README.md")]
pub use semver::Version;
use std::{cmp, collections::BTreeMap, fmt};
use url::Url;
pub mod advisories;
pub mod bans;
pub mod cfg;
pub mod diag;
pub mod licenses;
pub mod root_cfg;
pub mod sources;
#[doc(hidden)]
pub mod test_utils;
pub use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
pub use cfg::UnvalidatedConfig;
use krates::cm;
pub use krates::{DepKind, Kid};
pub use toml_span::{
Deserialize, Error,
span::{Span, Spanned},
};
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default, strum::VariantNames, strum::VariantArray)]
#[cfg_attr(test, derive(serde::Serialize))]
#[cfg_attr(test, serde(rename_all = "kebab-case"))]
#[strum(serialize_all = "kebab-case")]
pub enum LintLevel {
Allow,
#[default]
Warn,
Deny,
}
#[macro_export]
macro_rules! enum_deser {
($enum:ty) => {
impl<'de> toml_span::Deserialize<'de> for $enum {
fn deserialize(
value: &mut toml_span::value::Value<'de>,
) -> Result<Self, toml_span::DeserError> {
let s = value.take_string(Some(stringify!($enum)))?;
use strum::{VariantArray, VariantNames};
let Some(pos) = <$enum as VariantNames>::VARIANTS
.iter()
.position(|v| *v == s.as_ref())
else {
return Err(toml_span::Error::from((
toml_span::ErrorKind::UnexpectedValue {
expected: <$enum as VariantNames>::VARIANTS,
value: None,
},
value.span,
))
.into());
};
Ok(<$enum as VariantArray>::VARIANTS[pos])
}
}
};
}
enum_deser!(LintLevel);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Source {
CratesIo(bool),
Git {
spec: GitSpec,
url: Url,
spec_value: Option<String>,
},
Registry(Url),
Sparse(Url),
}
fn crates_io_sparse_dir() -> &'static str {
static mut CRATES_IO_SPARSE_DIR: String = String::new();
static CRATES_IO_INIT: parking_lot::Once = parking_lot::Once::new();
#[allow(unsafe_code)]
unsafe {
CRATES_IO_INIT.call_once(|| {
let Ok(version) = tame_index::utils::cargo_version(None) else {
return;
};
let Ok(url_dir) = tame_index::utils::url_to_local_dir(
tame_index::CRATES_IO_HTTP_INDEX,
version >= semver::Version::new(1, 85, 0),
) else {
return;
};
CRATES_IO_SPARSE_DIR = url_dir.dir_name;
});
#[allow(static_mut_refs)]
&CRATES_IO_SPARSE_DIR
}
}
impl Source {
pub fn crates_io(is_sparse: bool) -> Self {
Self::CratesIo(is_sparse)
}
fn from_metadata(urls: String, manifest_path: &Path) -> anyhow::Result<Self> {
use anyhow::Context as _;
let (kind, url_str) = urls
.split_once('+')
.with_context(|| format!("'{urls}' is not a valid crate source"))?;
match kind {
"sparse" => {
if urls == tame_index::CRATES_IO_HTTP_INDEX {
Ok(Self::crates_io(true))
} else {
Url::parse(&urls)
.map(Self::Sparse)
.context("failed to parse url")
}
}
"registry" => {
if url_str == tame_index::CRATES_IO_INDEX {
let is_sparse = manifest_path.ancestors().nth(2).is_some_and(|dir| {
dir.file_name()
.is_some_and(|dir_name| dir_name == crates_io_sparse_dir())
});
Ok(Self::crates_io(is_sparse))
} else {
Url::parse(url_str)
.map(Self::Registry)
.context("failed to parse url")
}
}
"git" => {
let mut url = Url::parse(url_str).context("failed to parse url")?;
let (spec, spec_value) = normalize_git_url(&mut url);
Ok(Self::Git {
url,
spec,
spec_value,
})
}
unknown => anyhow::bail!("unknown source spec '{unknown}' for url {urls}"),
}
}
#[inline]
pub fn is_git(&self) -> bool {
matches!(self, Self::Git { .. })
}
#[inline]
pub fn git_spec(&self) -> Option<GitSpec> {
let Self::Git { spec, .. } = self else {
return None;
};
Some(*spec)
}
#[inline]
pub fn is_registry(&self) -> bool {
!self.is_git()
}
#[inline]
pub fn is_crates_io(&self) -> bool {
matches!(self, Self::CratesIo(_))
}
#[inline]
pub fn to_rustsec(&self) -> rustsec::package::SourceId {
use rustsec::package::SourceId;
match self {
Self::CratesIo(_) => SourceId::default(),
Self::Registry(url) => SourceId::for_registry(url).unwrap(),
Self::Sparse(sparse) => {
SourceId::from_url(sparse.as_str()).unwrap()
}
Self::Git { .. } => unreachable!(),
}
}
#[inline]
pub fn matches_rustsec(&self, sid: Option<&rustsec::package::SourceId>) -> bool {
let Some(sid) = sid else {
return self.is_crates_io();
};
if !sid.is_remote_registry() {
return false;
}
let (Self::Registry(url) | Self::Sparse(url)) = self else {
return false;
};
sid.url() == url
}
}
impl fmt::Display for Source {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CratesIo(_) => {
write!(f, "registry+{}", tame_index::CRATES_IO_INDEX)
}
Self::Git { url, .. } => {
write!(f, "git+{url}")
}
Self::Registry(url) => {
write!(f, "registry+{url}")
}
Self::Sparse(url) => {
write!(f, "{url}")
}
}
}
}
#[derive(Debug)]
pub struct Krate {
pub name: String,
pub id: Kid,
pub version: Version,
pub source: Option<Source>,
pub authors: Vec<String>,
pub repository: Option<String>,
pub description: Option<String>,
pub manifest_path: PathBuf,
pub license: Option<String>,
pub license_file: Option<PathBuf>,
pub deps: Vec<cm::Dependency>,
pub features: BTreeMap<String, Vec<String>>,
pub targets: Vec<cm::Target>,
pub publish: Option<Vec<String>>,
}
#[cfg(test)]
impl Default for Krate {
fn default() -> Self {
Self {
name: "".to_owned(),
version: Version::new(0, 1, 0),
authors: Vec::new(),
id: Kid::default(),
source: None,
description: None,
deps: Vec::new(),
license: None,
license_file: None,
targets: Vec::new(),
features: BTreeMap::new(),
manifest_path: PathBuf::new(),
repository: None,
publish: None,
}
}
}
impl PartialOrd for Krate {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Krate {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.id.cmp(&other.id)
}
}
impl PartialEq for Krate {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Krate {}
impl krates::KrateDetails for Krate {
#[inline]
fn name(&self) -> &str {
&self.name
}
#[inline]
fn version(&self) -> &semver::Version {
&self.version
}
}
impl From<cm::Package> for Krate {
fn from(pkg: cm::Package) -> Self {
let source = pkg.source.and_then(|src| {
let url = src.to_string();
Source::from_metadata(url, &pkg.manifest_path)
.map_err(|err| {
log::warn!(
"unable to parse source url for {}:{}: {err}",
pkg.name,
pkg.version
);
err
})
.ok()
});
Self {
name: pkg.name,
id: pkg.id.into(),
version: pkg.version,
authors: pkg.authors,
repository: pkg.repository,
source,
targets: pkg.targets,
license: pkg.license.map(|lf| {
if lf.contains('/') {
lf.replace('/', " OR ")
} else {
lf
}
}),
license_file: pkg.license_file,
description: pkg.description,
manifest_path: pkg.manifest_path,
deps: pkg.dependencies,
features: pkg.features,
publish: pkg.publish,
}
}
}
impl Krate {
pub(crate) fn is_private(&self, private_registries: &[&str]) -> bool {
self.publish.as_ref().is_some_and(|v| {
if v.is_empty() {
true
} else {
v.iter()
.all(|reg| private_registries.contains(®.as_str()))
}
})
}
#[inline]
pub(crate) fn matches_url(&self, url: &Url, exact: bool) -> bool {
let Some(src) = &self.source else {
return false;
};
let kurl = match src {
Source::CratesIo(_is_sparse) => {
return url
.as_str()
.ends_with(&tame_index::CRATES_IO_HTTP_INDEX[8..])
|| url.as_str().ends_with(&tame_index::CRATES_IO_INDEX[10..]);
}
Source::Sparse(surl) | Source::Registry(surl) | Source::Git { url: surl, .. } => surl,
};
kurl.host() == url.host()
&& ((exact && kurl.path() == url.path())
|| (!exact && kurl.path().starts_with(url.path())))
}
#[inline]
pub(crate) fn is_crates_io(&self) -> bool {
self.source.as_ref().is_some_and(|src| src.is_crates_io())
}
#[inline]
pub(crate) fn is_git_source(&self) -> bool {
self.source.as_ref().is_some_and(|src| src.is_git())
}
#[inline]
pub(crate) fn is_registry(&self) -> bool {
self.source.as_ref().is_some_and(|src| src.is_registry())
}
}
impl fmt::Display for Krate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} = {}", self.name, self.version)
}
}
pub type Krates = krates::Krates<Krate>;
#[inline]
pub fn binary_search<T, Q>(s: &[T], query: &Q) -> Result<usize, usize>
where
T: std::borrow::Borrow<Q>,
Q: Ord + ?Sized,
{
s.binary_search_by(|i| i.borrow().cmp(query))
}
#[inline]
pub fn contains<T, Q>(s: &[T], query: &Q) -> bool
where
T: std::borrow::Borrow<Q>,
Q: Eq + ?Sized,
{
s.iter().any(|i| i.borrow() == query)
}
#[inline]
pub fn hash(data: &[u8]) -> u32 {
use std::hash::Hasher;
let mut xx = twox_hash::XxHash32::default();
xx.write(data);
xx.finish() as u32
}
pub struct CheckCtx<'ctx, T> {
pub cfg: T,
pub krates: &'ctx Krates,
pub krate_spans: &'ctx diag::KrateSpans<'ctx>,
pub serialize_extra: bool,
pub colorize: bool,
pub log_level: log::LevelFilter,
pub files: &'ctx diag::Files,
}
#[inline]
pub fn match_req(version: &Version, req: Option<&semver::VersionReq>) -> bool {
req.is_none_or(|req| req.matches(version))
}
#[inline]
pub fn match_krate(krate: &Krate, pid: &cfg::PackageSpec) -> bool {
krate.name == pid.name.value && match_req(&krate.version, pid.version_req.as_ref())
}
use sources::cfg::GitSpec;
#[inline]
pub(crate) fn normalize_git_url(url: &mut Url) -> (GitSpec, Option<String>) {
const GIT_EXT: &str = ".git";
let needs_chopping = url.path().ends_with(&GIT_EXT);
if needs_chopping {
let last = {
let last = url.path_segments().unwrap().next_back().unwrap();
last[..last.len() - GIT_EXT.len()].to_owned()
};
url.path_segments_mut().unwrap().pop().push(&last);
}
if url.path().ends_with('/') {
url.path_segments_mut().unwrap().pop_if_empty();
}
let mut spec = GitSpec::Any;
let mut spec_value = None;
for (k, v) in url.query_pairs() {
spec = match k.as_ref() {
"branch" | "ref" => GitSpec::Branch,
"tag" => GitSpec::Tag,
"rev" => GitSpec::Rev,
_ => continue,
};
spec_value = Some(v.into_owned());
}
if url
.query_pairs()
.any(|(k, v)| k == "branch" && v == "master")
{
if url.query_pairs().count() == 1 {
url.set_query(None);
} else {
let mut nq = String::new();
for (k, v) in url.query_pairs() {
if k == "branch" && v == "master" {
continue;
}
use std::fmt::Write;
write!(&mut nq, "{k}={v}&").unwrap();
}
nq.pop();
url.set_query(Some(&nq));
}
}
(spec, spec_value)
}
#[inline]
#[allow(clippy::disallowed_types)]
pub fn utf8path(pb: std::path::PathBuf) -> anyhow::Result<PathBuf> {
use anyhow::Context;
PathBuf::try_from(pb).context("non-utf8 path")
}
pub fn krates_with_index(
kb: &mut krates::Builder,
config_root: Option<PathBuf>,
cargo_home: Option<PathBuf>,
) -> anyhow::Result<()> {
use anyhow::Context as _;
let crates_io = tame_index::IndexUrl::crates_io(config_root, cargo_home.as_deref(), None)
.context("unable to determine crates.io url")?;
let index = tame_index::index::ComboIndexCache::new(
tame_index::IndexLocation::new(crates_io).with_root(cargo_home.clone()),
)
.context("unable to open local crates.io index")?;
let lock = tame_index::utils::flock::FileLock::unlocked();
let index_cache_build = move |krates: std::collections::BTreeSet<String>| {
let mut cache = std::collections::BTreeMap::new();
for name in krates {
let read = || -> Option<krates::index::IndexKrate> {
let name = name.as_str().try_into().ok()?;
let krate = index.cached_krate(name, &lock).ok()??;
let versions = krate
.versions
.into_iter()
.filter_map(|kv| {
kv.version.parse::<semver::Version>().ok().map(|version| {
krates::index::IndexKrateVersion {
version,
features: kv
.features()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
}
})
})
.collect();
Some(krates::index::IndexKrate { versions })
};
let krate = read();
cache.insert(name, krate);
}
cache
};
kb.with_crates_io_index(Box::new(index_cache_build));
Ok(())
}
#[cfg(test)]
mod test {
use super::{Krate, PathBuf, Source, Url};
#[test]
fn parses_sources() {
let empty_dir = super::Path::new("");
let crates_io_git = Source::from_metadata(
format!("registry+{}", tame_index::CRATES_IO_INDEX),
empty_dir,
)
.unwrap();
let crates_io_sparse =
Source::from_metadata(tame_index::CRATES_IO_HTTP_INDEX.to_owned(), empty_dir).unwrap();
let crates_io_sparse_but_git = Source::from_metadata(
format!("registry+{}", tame_index::CRATES_IO_INDEX),
super::Path::new(&format!(
"registry/src/{}/cargo-deny-0.69.0/Cargo.toml",
super::crates_io_sparse_dir(),
)),
)
.unwrap();
assert!(
crates_io_git.is_registry()
&& crates_io_sparse.is_registry()
&& crates_io_sparse_but_git.is_registry()
);
assert!(
crates_io_git.is_crates_io()
&& crates_io_sparse.is_crates_io()
&& crates_io_sparse_but_git.is_crates_io()
);
assert!(
Source::from_metadata(
"registry+https://my-own-my-precious.com/".to_owned(),
empty_dir
)
.unwrap()
.is_registry()
);
assert!(
Source::from_metadata("sparse+https://my-registry.rs/".to_owned(), empty_dir)
.unwrap()
.is_registry()
);
let src = Source::from_metadata("git+https://github.com/EmbarkStudios/wasmtime?branch=v6.0.1-profiler#84b8cacceacb585ef53774c3790b2372ba080067".to_owned(), empty_dir).unwrap();
assert!(src.is_git());
}
#[test]
fn validate_crates_io_sparse_dir_name() {
let stable =
tame_index::utils::cargo_version(None).unwrap() >= tame_index::Version::new(1, 85, 0);
assert_eq!(
tame_index::utils::url_to_local_dir(tame_index::CRATES_IO_HTTP_INDEX, stable)
.unwrap()
.dir_name,
super::crates_io_sparse_dir(),
);
}
#[test]
fn inexact_match_fails_for_different_hosts() {
let krate = Krate {
source: Some(
Source::from_metadata(
"git+ssh://git@repo1.test.org/path/test.git".to_owned(),
&PathBuf::new(),
)
.unwrap(),
),
..Krate::default()
};
let url = Url::parse("ssh://git@repo2.test.org:8000").unwrap();
assert!(!krate.matches_url(&url, false));
}
#[test]
fn inexact_match_passes_for_same_hosts() {
let krate = Krate {
source: Some(
Source::from_metadata(
"git+ssh://git@repo1.test.org/path/test.git".to_owned(),
&PathBuf::new(),
)
.unwrap(),
),
..Krate::default()
};
let url = Url::parse("ssh://git@repo1.test.org:8000").unwrap();
assert!(krate.matches_url(&url, false));
}
}