use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use futures::lock::Mutex;
use futures::StreamExt;
use log::error;
use semver::Version;
use tokio_stream::wrappers::ReadDirStream;
use crate::model::config::Configuration;
use crate::model::osv::{Advisory, SimpleAdvisory};
use crate::utils::apierror::ApiError;
use crate::utils::concurrent::n_at_a_time_stream;
use crate::utils::{stale_instant, FaillibleFuture};
pub trait RustSecChecker {
fn check_crate<'a>(&'a self, package: &'a str, version: &'a Version) -> FaillibleFuture<'a, Vec<SimpleAdvisory>>;
}
#[must_use]
pub fn get_service(config: &Configuration) -> Arc<dyn RustSecChecker + Send + Sync> {
Arc::new(RustSecCheckerImpl {
data: Mutex::new(RustSecData::new(config.data_dir.clone(), config.deps_stale_registry)),
})
}
struct RustSecCheckerImpl {
data: Mutex<RustSecData>,
}
impl RustSecChecker for RustSecCheckerImpl {
fn check_crate<'a>(&'a self, package: &'a str, version: &'a Version) -> FaillibleFuture<'a, Vec<SimpleAdvisory>> {
Box::pin(async move {
let mut data = self.data.lock().await;
data.update_data().await?;
let db = data.db.lock().unwrap();
Ok(db
.get(package)
.map(|advisories| {
advisories
.iter()
.filter(|advisory| advisory.affects(version))
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default())
})
}
}
#[derive(Debug, Clone)]
struct RustSecData {
data_dir: String,
stale_registry: u64,
last_touch: Instant,
db: Arc<std::sync::Mutex<HashMap<String, Vec<SimpleAdvisory>>>>,
}
const RUSTSEC_DB_GIT_URI: &str = "https://github.com/rustsec/advisory-db";
const RUSTSEC_DB_GIT_BRANCH: &str = "osv";
const DATA_SUB_DIR: &str = "rustsec";
impl RustSecData {
fn new(data_dir: String, stale_registry: u64) -> Self {
Self {
data_dir,
stale_registry,
last_touch: stale_instant(),
db: Arc::new(std::sync::Mutex::new(HashMap::new())),
}
}
async fn update_data(&mut self) -> Result<(), ApiError> {
let now = Instant::now();
let is_stale = now.duration_since(self.last_touch) > Duration::from_millis(self.stale_registry);
let mut reg_location = PathBuf::from(&self.data_dir);
reg_location.push(DATA_SUB_DIR);
if is_stale {
if tokio::fs::try_exists(®_location).await? {
crate::utils::execute_git(®_location, &["fetch", "origin", RUSTSEC_DB_GIT_BRANCH]).await?;
crate::utils::execute_git(
®_location,
&["reset", "--hard", &format!("origin/{RUSTSEC_DB_GIT_BRANCH}")],
)
.await?;
} else {
tokio::fs::create_dir_all(®_location).await?;
crate::utils::execute_git(
®_location,
&["clone", "--branch", RUSTSEC_DB_GIT_BRANCH, RUSTSEC_DB_GIT_URI, "."],
)
.await?;
}
self.last_touch = Instant::now();
reg_location.push("crates");
self.db.lock().unwrap().clear();
let _results = n_at_a_time_stream(
ReadDirStream::new(tokio::fs::read_dir(®_location).await?).map(|entry| {
let db = self.db.clone();
Box::pin(async move {
let content = tokio::fs::read(&entry?.path()).await?;
let advisory = serde_json::from_slice::<Advisory>(&content)?;
if let Ok(simple) = SimpleAdvisory::try_from(advisory) {
db.lock().unwrap().entry(simple.package.clone()).or_default().push(simple);
}
Ok::<_, ApiError>(())
})
}),
10,
|r| {
if let Err(e) = r {
error!("{e}");
if let Some(backtrace) = &e.backtrace {
error!("{backtrace}");
}
}
false
},
)
.await;
}
Ok(())
}
}