#[cfg(feature = "chrono")]
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
#[cfg(feature = "chrono")]
use git2::{AutotagOption, FetchOptions, Oid, ResetType};
use git2::{ObjectType, Repository as GitRepository, RepositoryState};
use std::{
env,
fs::{self, File},
io::Read,
path::{Path, PathBuf},
vec,
};
use error::{Error, ErrorKind};
pub const ADVISORY_DB_REPO_URL: &str = "https://github.com/RustSec/advisory-db.git";
pub const DAYS_UNTIL_STALE: usize = 90;
const ADVISORY_DB_DIRECTORY: &str = "advisory-db";
const CRATE_ADVISORY_DIRECTORY: &str = "crates";
#[cfg(feature = "chrono")]
const LOCAL_MASTER_REF: &str = "refs/heads/master";
#[cfg(feature = "chrono")]
const REMOTE_MASTER_REF: &str = "refs/remotes/origin/master";
pub struct Repository {
path: PathBuf,
repo: GitRepository,
}
impl Repository {
pub fn default_path() -> PathBuf {
if let Some(path) = env::var_os("CARGO_HOME") {
PathBuf::from(path).join(ADVISORY_DB_DIRECTORY)
} else {
panic!("Can't locate CARGO_HOME!");
}
}
#[cfg(feature = "chrono")]
pub fn fetch_default_repo() -> Result<Self, Error> {
Self::fetch(ADVISORY_DB_REPO_URL, Repository::default_path(), true)
}
#[cfg(feature = "chrono")]
pub fn fetch<P: Into<PathBuf>>(
url: &str,
into_path: P,
ensure_fresh: bool,
) -> Result<Self, Error> {
if !url.starts_with("https://") {
fail!(
ErrorKind::BadParam,
"expected {} to start with https://",
url
);
}
let path = into_path.into();
if let Some(parent) = path.parent() {
if !parent.is_dir() {
fail!(ErrorKind::BadParam, "not a directory: {}", parent.display());
}
} else {
fail!(ErrorKind::BadParam, "invalid directory: {}", path.display())
}
if path.exists() {
let repo = GitRepository::open(&path)?;
let refspec = LOCAL_MASTER_REF.to_owned() + ":" + REMOTE_MASTER_REF;
let mut fetch_opts = FetchOptions::new();
fetch_opts.download_tags(AutotagOption::All);
let mut remote = repo.remote_anonymous(url)?;
remote.fetch(&[refspec.as_str()], Some(&mut fetch_opts), None)?;
let remote_master_ref = repo.find_reference(REMOTE_MASTER_REF)?;
let remote_target = remote_master_ref.target().unwrap();
let mut local_master_ref = repo.find_reference(LOCAL_MASTER_REF)?;
local_master_ref.set_target(
remote_target,
&format!(
"rustsec: moving master to {}: {}",
REMOTE_MASTER_REF, &remote_target
),
)?;
} else {
GitRepository::clone(url, &path)?;
}
let repo = Self::open(path)?;
let latest_commit = repo.latest_commit()?;
latest_commit.reset(&repo)?;
if latest_commit.signature.is_none() {
fail!(
ErrorKind::Repo,
"no signature on commit {}: {} ({})",
latest_commit.commit_id,
latest_commit.summary,
latest_commit.author
);
}
if ensure_fresh {
latest_commit.ensure_fresh()?;
}
Ok(repo)
}
pub fn open<P: Into<PathBuf>>(into_path: P) -> Result<Self, Error> {
let path = into_path.into();
let repo = GitRepository::open(&path)?;
match repo.state() {
RepositoryState::Clean => Ok(Repository { path, repo }),
state => fail!(ErrorKind::Repo, "bad repository state: {:?}", state),
}
}
pub fn latest_commit(&self) -> Result<CommitInfo, Error> {
CommitInfo::from_repo_head(self)
}
pub(crate) fn crate_advisories(&self) -> Result<Iter, Error> {
let mut advisory_files = vec![];
for crate_entry in fs::read_dir(self.path.join(CRATE_ADVISORY_DIRECTORY))? {
for advisory_entry in fs::read_dir(crate_entry?.path())? {
advisory_files.push(RepoFile::new(advisory_entry?.path())?);
}
}
Ok(Iter(advisory_files.into_iter()))
}
}
#[derive(Debug)]
pub struct CommitInfo {
pub commit_id: String,
pub author: String,
pub summary: String,
#[cfg(feature = "chrono")]
pub time: DateTime<Utc>,
pub signature: Option<Signature>,
signed_data: Option<String>,
}
impl CommitInfo {
pub fn from_repo_head(repo: &Repository) -> Result<Self, Error> {
let head = repo.repo.head()?;
let oid = head.target().ok_or_else(|| {
err!(
ErrorKind::Repo,
"no ref target for: {}",
repo.path.display()
)
})?;
let commit_id = oid.to_string();
let commit_object = repo.repo.find_object(oid, Some(ObjectType::Commit))?;
let commit = commit_object.as_commit().unwrap();
let author = commit.author().to_string();
let summary = commit
.summary()
.ok_or_else(|| err!(ErrorKind::Repo, "no commit summary for {}", commit_id))?
.to_owned();
let (signature, signed_data) = match repo.repo.extract_signature(&oid, None) {
Ok((sig, data)) => (
sig.as_str().and_then(|s| Signature::new(s).ok()),
data.as_str().map(|s| s.to_owned()),
),
_ => (None, None),
};
#[cfg(feature = "chrono")]
let time = DateTime::from_utc(
NaiveDateTime::from_timestamp(commit.time().seconds(), 0),
Utc,
);
Ok(CommitInfo {
commit_id,
author,
summary,
#[cfg(feature = "chrono")]
time,
signature,
signed_data,
})
}
pub fn raw_signed_bytes(&self) -> &[u8] {
match self.signed_data {
Some(ref s) => s.as_bytes(),
None => b"",
}
}
#[cfg(feature = "chrono")]
fn reset(&self, repo: &Repository) -> Result<(), Error> {
let commit_object = repo.repo.find_object(
Oid::from_str(&self.commit_id).unwrap(),
Some(ObjectType::Commit),
)?;
repo.repo.reset(&commit_object, ResetType::Hard, None)?;
Ok(())
}
#[cfg(feature = "chrono")]
fn ensure_fresh(&self) -> Result<(), Error> {
let fresh_after_date = Utc::now()
.checked_sub_signed(Duration::days(DAYS_UNTIL_STALE as i64))
.unwrap();
if self.time > fresh_after_date {
Ok(())
} else {
fail!(
ErrorKind::Repo,
"stale repo: not updated for {} days (last commit: {:?})",
DAYS_UNTIL_STALE,
self.time
)
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Signature(String);
impl Signature {
pub fn new<T: Into<String>>(into_string: T) -> Result<Self, Error> {
Ok(Signature(into_string.into()))
}
}
impl AsRef<[u8]> for Signature {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[derive(Debug)]
pub(crate) struct RepoFile(PathBuf);
impl RepoFile {
pub fn new<P: Into<PathBuf>>(path: P) -> Result<RepoFile, Error> {
Ok(RepoFile(path.into()))
}
pub fn path(&self) -> &Path {
self.0.as_ref()
}
pub fn read_to_string(&self) -> Result<String, Error> {
let mut file = File::open(&self.0)?;
let mut string = String::new();
file.read_to_string(&mut string)?;
Ok(string)
}
}
pub(crate) struct Iter(vec::IntoIter<RepoFile>);
impl Iterator for Iter {
type Item = RepoFile;
fn next(&mut self) -> Option<RepoFile> {
self.0.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl ExactSizeIterator for Iter {
fn len(&self) -> usize {
self.0.len()
}
}