use git2::{self, RepositoryState};
#[cfg(feature = "chrono")]
use git2::{AutotagOption, FetchOptions};
use std::{env, fs, path::PathBuf, vec};
use error::{Error, ErrorKind};
mod commit;
mod file;
mod signature;
pub use self::commit::Commit;
pub(crate) use self::file::RepoFile;
pub use self::signature::Signature;
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: git2::Repository,
}
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 = git2::Repository::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 {
git2::Repository::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 = git2::Repository::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<Commit, Error> {
Commit::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()))
}
}
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()
}
}