use chrono::NaiveDateTime;
use log::error;
use semver::{Version, VersionReq};
use serde_derive::{Deserialize, Serialize};
use super::cargo::{DependencyKind, IndexCrateDependency, IndexCrateMetadata};
use super::osv::SimpleAdvisory;
use super::CrateVersion;
use crate::utils::apierror::ApiError;
use crate::utils::push_if_not_present;
pub const BUILTIN_CRATES_REGISTRY_URI: &str = "<builtin>";
pub const BUILTIN_CRATES_LIST: &[&str] = &["core", "alloc", "std"];
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DepsAnalysisJobSpec {
pub package: String,
pub version: String,
pub targets: Vec<String>,
}
impl From<DepsAnalysisState> for DepsAnalysisJobSpec {
fn from(state: DepsAnalysisState) -> Self {
Self {
package: state.package,
version: state.version,
targets: state.targets,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DepsAnalysisState {
pub package: String,
pub version: String,
#[serde(rename = "isDeprecated")]
pub is_deprecated: bool,
#[serde(rename = "depsHasOutdated")]
pub deps_has_outdated: bool,
#[serde(rename = "depsLastCheck")]
pub deps_last_check: NaiveDateTime,
pub targets: Vec<String>,
}
impl From<DepsAnalysisState> for CrateVersion {
fn from(state: DepsAnalysisState) -> Self {
Self {
package: state.package,
version: state.version,
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct DepsAnalysis {
#[serde(rename = "directDependencies")]
pub direct_dependencies: Vec<DirectDepInfo>,
pub advisories: Vec<DepAdvisory>,
}
impl DepsAnalysis {
#[must_use]
pub fn new(graph: &DepsGraph, deps: &[IndexCrateDependency], advisories: Vec<DepAdvisory>) -> Self {
Self {
direct_dependencies: deps
.iter()
.zip(&graph.crates)
.map(|(dep, data)| {
let resolved = data
.resolutions
.iter()
.find(|r| r.origins.contains(&DepsGraphCrateOrigin::Direct(dep.kind)));
let is_outdated = resolved.is_some_and(|res| data.versions[res.version_index].is_outdated);
DirectDepInfo {
registry: dep.registry.clone(),
package: dep.get_name().to_string(),
required: dep.req.clone(),
kind: dep.kind,
last_version: data.last_version.to_string(),
is_outdated,
}
})
.collect(),
advisories,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectDepInfo {
pub registry: Option<String>,
pub package: String,
pub required: String,
pub kind: DependencyKind,
#[serde(rename = "lastVersion")]
pub last_version: String,
#[serde(rename = "isOutdated")]
pub is_outdated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepAdvisory {
pub package: String,
pub version: Version,
pub content: SimpleAdvisory,
}
impl IndexCrateMetadata {
#[must_use]
pub fn rewrite_builtin_deps(mut self, parent_registry: &Option<String>) -> Self {
for d in &mut self.deps {
if d.registry.is_none() {
if BUILTIN_CRATES_LIST.contains(&d.get_name()) {
d.registry = Some(String::from(BUILTIN_CRATES_REGISTRY_URI));
} else {
d.registry.clone_from(parent_registry);
}
}
}
self
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct DepsGraph {
pub targets: Vec<String>,
pub crates: Vec<DepsGraphCrate>,
pub unknowns: Vec<(Option<String>, String)>,
pub dirty: Vec<(usize, usize)>,
}
impl DepsGraph {
#[must_use]
pub fn new(targets: &[String]) -> Self {
Self {
targets: targets.to_vec(),
..Default::default()
}
}
pub fn get_crate(&mut self, registry: Option<&str>, name: &str) -> Option<(usize, &mut DepsGraphCrate)> {
self.crates
.iter_mut()
.enumerate()
.find(|(_, c)| c.registry.as_deref() == registry && c.name == name)
}
#[must_use]
pub fn is_unknown(&self, registry: Option<&str>, name: &str) -> bool {
self.unknowns.iter().any(|(r, n)| r.as_deref() == registry && n == name)
}
pub async fn resolve<F, FUT>(
&mut self,
dep: &IndexCrateDependency,
features: &[String],
origins: &[DepsGraphCrateOrigin],
get_versions: &F,
) -> Result<(), ApiError>
where
F: Fn(Option<String>, String) -> FUT,
FUT: std::future::Future<Output = Result<Vec<IndexCrateMetadata>, ApiError>>,
{
if let Some((crate_index, c)) = self.get_crate(dep.registry.as_deref(), dep.get_name()) {
if let Some(resolution_index) = c.resolve(dep, features, origins) {
push_if_not_present(&mut self.dirty, (crate_index, resolution_index));
}
} else if !self.is_unknown(dep.registry.as_deref(), dep.get_name()) {
let all_versions = match get_versions(dep.registry.clone(), dep.get_name().to_string()).await {
Ok(d) => d,
Err(e) => {
error!("deps: FAILED TO GET {:?} / {} => {}", dep.registry, dep.get_name(), e);
self.unknowns.push((dep.registry.clone(), dep.get_name().to_string()));
return Ok(());
}
};
self.crates.push(DepsGraphCrate::new(dep, all_versions)?);
let crate_index = self.crates.len() - 1;
if let Some(resolution_index) = self.crates.last_mut().unwrap().resolve(dep, features, origins) {
push_if_not_present(&mut self.dirty, (crate_index, resolution_index));
}
}
Ok(())
}
pub async fn close<F, FUT>(&mut self, get_versions: &F) -> Result<(), ApiError>
where
F: Fn(Option<String>, String) -> FUT,
FUT: std::future::Future<Output = Result<Vec<IndexCrateMetadata>, ApiError>>,
{
while let Some((crate_index, resolution_index)) = self.dirty.pop() {
let dependencies = self.crates[crate_index]
.get_active_deps_in(resolution_index, &self.targets)
.map(|(dep, features)| (dep.clone(), features.into_iter().map(str::to_string).collect::<Vec<_>>()))
.collect::<Vec<_>>();
for (dep, features) in dependencies {
let origins = self.crates[crate_index].resolutions[resolution_index]
.origins
.iter()
.filter_map(|&origin| origin.child_of_kind(dep.kind))
.collect::<Vec<_>>();
if !origins.is_empty() {
self.resolve(&dep, &features, &origins, get_versions).await?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DepsGraphCrateOrigin {
Direct(DependencyKind),
NormalIndirect,
BuildIndirect,
DevNormalIndirect,
DevBuildIndirect,
}
impl DepsGraphCrateOrigin {
#[allow(clippy::match_same_arms)]
#[must_use]
pub fn child_of_kind(self, kind: DependencyKind) -> Option<DepsGraphCrateOrigin> {
match (self, kind) {
(_, DependencyKind::Dev) => None, (Self::Direct(DependencyKind::Normal), DependencyKind::Normal) => Some(Self::NormalIndirect),
(Self::Direct(DependencyKind::Normal), DependencyKind::Build) => Some(Self::BuildIndirect),
(Self::Direct(DependencyKind::Build), DependencyKind::Normal) => Some(Self::BuildIndirect),
(Self::Direct(DependencyKind::Build), DependencyKind::Build) => Some(Self::BuildIndirect),
(Self::Direct(DependencyKind::Dev), DependencyKind::Normal) => Some(Self::DevNormalIndirect),
(Self::Direct(DependencyKind::Dev), DependencyKind::Build) => Some(Self::DevBuildIndirect),
(Self::NormalIndirect, DependencyKind::Normal) => Some(Self::NormalIndirect),
(Self::BuildIndirect, DependencyKind::Normal) => Some(Self::BuildIndirect),
(Self::NormalIndirect, DependencyKind::Build) => Some(Self::BuildIndirect),
(Self::BuildIndirect, DependencyKind::Build) => Some(Self::BuildIndirect),
(Self::DevNormalIndirect, DependencyKind::Normal) => Some(Self::DevNormalIndirect),
(Self::DevNormalIndirect, DependencyKind::Build) => Some(Self::DevBuildIndirect),
(Self::DevBuildIndirect, DependencyKind::Normal) => Some(Self::DevBuildIndirect),
(Self::DevBuildIndirect, DependencyKind::Build) => Some(Self::DevBuildIndirect),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepsGraphCrateVersion {
pub semver: Version,
pub metadata: IndexCrateMetadata,
#[serde(rename = "isOutdated")]
pub is_outdated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepsGraphCrateResolution {
#[serde(rename = "versionIndex")]
pub version_index: usize,
#[serde(rename = "defaultFeatures")]
pub default_features: bool,
pub features: Vec<String>,
pub origins: Vec<DepsGraphCrateOrigin>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepsGraphCrate {
pub registry: Option<String>,
pub name: String,
pub versions: Vec<DepsGraphCrateVersion>,
#[serde(rename = "lastVersion")]
pub last_version: Version,
pub resolutions: Vec<DepsGraphCrateResolution>,
pub unresolved: Vec<VersionReq>,
}
impl DepsGraphCrate {
pub fn new(package: &IndexCrateDependency, versions: Vec<IndexCrateMetadata>) -> Result<Self, semver::Error> {
let last_version = versions
.iter()
.filter_map(|metadata| {
if metadata.yanked {
None
} else if let Ok(vers) = metadata.vers.parse::<Version>() {
if vers.pre.is_empty() {
Some(vers)
} else {
None
}
} else {
None
}
})
.max()
.unwrap();
let versions = versions
.into_iter()
.map(|metadata| {
let semver = metadata.vers.parse::<Version>()?;
Ok(DepsGraphCrateVersion {
is_outdated: semver < last_version,
semver,
metadata: metadata.rewrite_builtin_deps(&package.registry),
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
registry: package.registry.clone(),
name: package.get_name().to_string(),
versions,
last_version,
resolutions: Vec::new(),
unresolved: Vec::new(),
})
}
pub fn resolve(
&mut self,
dep: &IndexCrateDependency,
features: &[String],
origins: &[DepsGraphCrateOrigin],
) -> Option<usize> {
let semver = dep.req.parse::<VersionReq>().unwrap();
let version_index = self
.versions
.iter()
.enumerate()
.filter(|(_, version)| semver.matches(&version.semver))
.max_by(|(_, v1), (_, v2)| v1.semver.cmp(&v2.semver))
.map(|(i, _)| i);
let Some(version_index) = version_index else {
self.unresolved.push(semver);
return None;
};
if let Some((resolution_index, resolution)) = self
.resolutions
.iter_mut()
.enumerate()
.find(|(_, res)| res.version_index == version_index)
{
let mut modified = false;
for feature in features {
modified |= push_if_not_present(&mut resolution.features, feature.clone());
}
for &origin in origins {
modified |= push_if_not_present(&mut resolution.origins, origin);
}
if modified {
Some(resolution_index)
} else {
None
}
} else {
let mut resolution = DepsGraphCrateResolution {
version_index,
default_features: dep.default_features,
features: dep.features.clone(),
origins: origins.to_vec(),
};
for feature in features {
push_if_not_present(&mut resolution.features, feature.clone());
}
self.resolutions.push(resolution);
Some(self.resolutions.len() - 1)
}
}
pub fn get_active_deps_in<'this: 'targets, 'targets>(
&'this self,
resolution_index: usize,
targets: &'targets [String],
) -> impl Iterator<Item = (&'this IndexCrateDependency, Vec<&'this str>)> + 'targets {
let resolution = &self.resolutions[resolution_index];
let version = &self.versions[resolution.version_index];
let active_features = Self::get_active_features(resolution, version);
version.metadata.deps.iter().filter_map(move |dep| {
let is_active = dep.is_active_for(targets, &active_features);
if is_active {
let sub_features = active_features
.iter()
.filter_map(|feature| {
let index = feature.find('/')?;
if &feature[..index] == dep.get_name()
|| (feature[..index].ends_with('?') && &feature[..(index - 1)] == dep.get_name())
{
Some(&feature[(index + 1)..])
} else {
None
}
})
.collect();
Some((dep, sub_features))
} else {
None
}
})
}
fn get_active_features<'a>(resolution: &'a DepsGraphCrateResolution, version: &'a DepsGraphCrateVersion) -> Vec<&'a str> {
let mut active_features = Vec::new();
if resolution.default_features {
if let Some(children) = version.metadata.get_feature("default") {
active_features.push("default");
for f in children {
push_if_not_present(&mut active_features, f.as_str());
}
}
}
for feature in &resolution.features {
push_if_not_present(&mut active_features, feature.as_str());
if let Some(children) = version.metadata.get_feature(feature) {
for f in children {
push_if_not_present(&mut active_features, f.as_str());
}
}
}
let mut index = 0;
while index < active_features.len() {
if let Some(children) = version.metadata.get_feature(active_features[index]) {
for f in children {
push_if_not_present(&mut active_features, f.as_str());
}
}
index += 1;
}
active_features
}
}