use crate::{
deps::scan::{self, RequiredDetails},
edit, opts,
opts::CrateSelector,
prelude::*,
repo::*,
};
use anyhow::{format_err, Context, Result};
use crev_data::{proof, review::Package};
use crev_lib::{self, local::Local, ProofStore, ReviewMode};
use resiter::FlatMap;
use serde::Deserialize;
use std::{
collections::HashSet,
env,
ffi::{OsStr, OsString},
io,
path::{Path, PathBuf},
process,
};
pub const GOTO_ORIGINAL_DIR_ENV: &str = "CARGO_CREV_GOTO_ORIGINAL_DIR";
pub const GOTO_CRATE_NAME_ENV: &str = "CARGO_CREV_GOTO_ORIGINAL_NAME";
pub const GOTO_CRATE_VERSION_ENV: &str = "CARGO_CREV_GOTO_ORIGINAL_VERSION";
pub const KNOWN_CARGO_OWNERS_FILE: &str = "known_cargo_owners.txt";
pub const PROJECT_SOURCE_CRATES_IO: &str = "https://crates.io";
pub const VCS_INFO_JSON_FILE: &str = ".cargo_vcs_info.json";
#[derive(Debug, Clone, Deserialize)]
pub struct VcsInfoJson {
git: VcsInfoJsonGit,
}
pub fn vcs_info_to_revision_string(vcs: Option<VcsInfoJson>) -> String {
vcs.and_then(|vcs| vcs.get_git_revision())
.unwrap_or_else(|| "".into())
}
#[derive(Debug, Clone, Deserialize)]
pub enum VcsInfoJsonGit {
#[serde(rename = "sha1")]
Sha1(String),
}
impl VcsInfoJson {
fn read_from_crate_dir(pkg_dir: &Path) -> Result<Option<Self>> {
let path = pkg_dir.join(VCS_INFO_JSON_FILE);
if path.exists() {
let txt = std::fs::read_to_string(&path)?;
let info: VcsInfoJson = serde_json::from_str(&txt)?;
Ok(Some(info))
} else {
Ok(None)
}
}
fn get_git_revision(&self) -> Option<String> {
let VcsInfoJsonGit::Sha1(ref s) = self.git;
Some(s.to_string())
}
}
pub fn cargo_full_ignore_list(ignore_cargo_lock: bool) -> fnv::FnvHashSet<PathBuf> {
let mut ignore_list = HashSet::default();
ignore_list.insert(PathBuf::from(".cargo-ok"));
ignore_list.insert(PathBuf::from("target"));
if ignore_cargo_lock {
ignore_list.insert(PathBuf::from("Cargo.lock"));
}
ignore_list
}
pub fn cargo_min_ignore_list() -> fnv::FnvHashSet<PathBuf> {
let mut ignore_list = HashSet::default();
ignore_list.insert(PathBuf::from(".cargo-ok"));
ignore_list
}
#[cfg(target_family = "unix")]
pub fn exec_into(mut command: process::Command) -> Result<()> {
use std::os::unix::process::CommandExt;
bail!(command.exec());
}
#[cfg(target_family = "windows")]
pub fn exec_into(mut command: process::Command) -> Result<()> {
let status = command.status()?;
if !status.success() {
bail!("Shell returned {}", status);
}
Ok(())
}
pub fn goto_crate_src(selector: &opts::CrateSelector) -> Result<()> {
if env::var(GOTO_ORIGINAL_DIR_ENV).is_ok() {
bail!("You're already in a `cargo crev goto` shell");
};
let repo = Repo::auto_open_cwd_default()?;
selector.ensure_name_given()?;
let crate_id = repo.find_pkgid_by_crate_selector(selector)?;
let crate_ = repo.get_crate(&crate_id)?;
let crate_dir = crate_.root();
let crate_name = crate_.name();
let crate_version = crate_.version();
let local = crev_lib::Local::auto_create_or_open()?;
local.record_review_activity(
PROJECT_SOURCE_CRATES_IO,
&crate_.name(),
crate_version,
&crev_lib::ReviewActivity::new_full(),
)?;
let shell = env::var_os("SHELL").ok_or_else(|| format_err!("$SHELL not set"))?;
let cwd = env::current_dir()?;
eprintln!("Opening shell in: {}", crate_dir.display());
eprintln!("Use `exit` or Ctrl-D to return to the original project.",);
eprintln!("Use `cargo crev review` without any arguments to review this crate.");
let mut command = process::Command::new(shell);
command
.current_dir(crate_dir)
.env("PWD", crate_dir)
.env(GOTO_ORIGINAL_DIR_ENV, cwd)
.env(GOTO_CRATE_NAME_ENV, crate_name)
.env(GOTO_CRATE_VERSION_ENV, &crate_version.to_string());
exec_into(command)
}
pub fn expand_crate_src(selector: &opts::CrateSelector) -> Result<()> {
use quote::ToTokens;
let repo = Repo::auto_open_cwd_default()?;
selector.ensure_name_given()?;
let crate_id = repo.find_pkgid_by_crate_selector(selector)?;
let crate_ = repo.get_crate(&crate_id)?;
let crate_dir = crate_.root();
println!(
"{}",
syn_inline_mod::parse_and_inline_modules(&crate_dir.join("src/lib.rs")).into_token_stream()
);
Ok(())
}
pub fn ensure_known_owners_list_exists(local: &crev_lib::Local) -> Result<()> {
let path = local.get_proofs_dir_path()?.join(KNOWN_CARGO_OWNERS_FILE);
if !path.exists() {
crev_common::store_str_to_file(&path, include_str!("known_cargo_owners_defaults.txt"))?;
local.proof_dir_git_add_path(&PathBuf::from(KNOWN_CARGO_OWNERS_FILE))?;
}
Ok(())
}
pub fn read_known_owners_list() -> Result<HashSet<String>> {
let local = Local::auto_create_or_open()?;
let content = if let Some(path) = local.get_proofs_dir_path_opt()? {
let path = path.join(KNOWN_CARGO_OWNERS_FILE);
std::fs::read_to_string(&path)?
} else {
include_str!("known_cargo_owners_defaults.txt").to_string()
};
Ok(content
.lines()
.map(str::trim)
.filter(|s| !s.starts_with('#'))
.map(ToString::to_string)
.collect())
}
pub fn edit_known_owners_list() -> Result<()> {
let local = Local::auto_create_or_open()?;
let path = local.get_proofs_dir_path()?.join(KNOWN_CARGO_OWNERS_FILE);
ensure_known_owners_list_exists(&local)?;
edit::edit_file(&path)?;
Ok(())
}
pub fn clean_all_crates_with_digest_mismatch() -> Result<()> {
let scanner = scan::Scanner::new(CrateSelector::default(), &opts::CrateVerify::default())?;
let events = scanner.run(&RequiredDetails::none());
for stats in events {
if stats.details.accumulative_own.is_local_source_code {
continue;
}
if stats.has_digest_mismatch() {
clean_crate(&CrateSelector::new(
Some(stats.info.id.name().to_string()),
Some(stats.info.id.version().to_owned()),
false,
))?;
}
}
Ok(())
}
pub fn clean_crate(selector: &CrateSelector) -> Result<()> {
let repo = Repo::auto_open_cwd_default()?;
let crate_id = repo.find_pkgid_by_crate_selector(selector)?;
let crate_ = repo.get_crate(&crate_id)?;
assert!(crate_.package_id().source_id().is_registry());
let crate_root = crate_.root();
assert!(crate_root.is_absolute());
assert!(!crate_root.starts_with(std::env::current_dir()?));
if crate_root.is_dir() {
std::fs::remove_dir_all(&crate_root)?;
}
let _crate_ = repo.get_crate(&crate_id)?;
Ok(())
}
pub fn get_open_cmd(local: &Local) -> Result<String> {
let config = local
.load_user_config()
.with_context(|| "Can't open user config")?;
if let Some(cmd) = config.open_cmd {
return Ok(cmd);
}
Ok(if cfg!(target_os = "windows") {
"start"
} else if cfg!(target_os = "macos") {
"open"
} else if cfg!(target_os = "linux") {
"xdg-open"
} else {
eprintln!("Unsupported platform. Please submit a PR!");
"xdg-open"
}
.into())
}
pub fn crate_open(crate_sel: &CrateSelector, cmd: Option<String>, cmd_save: bool) -> Result<()> {
let local = Local::auto_create_or_open()?;
let repo = Repo::auto_open_cwd_default()?;
let crate_id = repo.find_pkgid_by_crate_selector(crate_sel)?;
let cargo_crate = repo.get_crate(&crate_id)?;
if cmd_save {
if let Some(cmd) = &cmd {
local.store_config_open_cmd(cmd.clone())?;
} else {
bail!("Can't save cmd without specifying it");
}
}
let name = cargo_crate.name().to_string();
let version = cargo_crate.version();
let src_dir = cargo_crate.root();
let dest_dir = local.sanitized_crate_copy(PROJECT_SOURCE_CRATES_IO, &name, version, src_dir)?;
let open_cmd = match cmd {
Some(cmd) => cmd,
None => get_open_cmd(&local)?,
};
local.record_review_activity(
PROJECT_SOURCE_CRATES_IO,
&name,
version,
&crev_lib::ReviewActivity::new_full(),
)?;
let status = crev_lib::util::run_with_shell_cmd(open_cmd.as_ref(), Some(&dest_dir))?;
if !status.success() {
bail!("Shell returned {}", status);
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum ActivityCheckError {
#[error("No previous review activity to determine base version")]
NoPreviousReview,
#[error("Last review activity record indicates full review")]
UnexpectedFullReview,
#[error("Last review activity record indicates differential review")]
UnexpectedDiffReview,
#[error("Last review activity record is too old")]
Expired,
#[error("No review activity record found")]
NoRecord,
#[error("{}", _0)]
Other(#[from] crev_lib::Error),
}
pub fn crate_review_activity_check(
local: &Local,
name: &str,
version: &Version,
diff: &Option<Option<Version>>,
skip_activity_check: bool,
) -> Result<Option<Version>, ActivityCheckError> {
let activity = local.read_review_activity(PROJECT_SOURCE_CRATES_IO, name, version)?;
let diff = match diff {
None => None,
Some(None) => Some(
activity
.clone()
.ok_or(ActivityCheckError::NoPreviousReview)?
.diff_base
.ok_or(ActivityCheckError::UnexpectedFullReview)?,
),
Some(o) => o.clone(),
};
if skip_activity_check {
return Ok(diff);
}
if let Some(activity) = activity {
match activity.to_review_mode() {
ReviewMode::Full => {
if diff.is_some() {
return Err(ActivityCheckError::UnexpectedFullReview);
}
}
ReviewMode::Differential => {
if diff.is_none() {
return Err(ActivityCheckError::UnexpectedDiffReview);
}
}
}
if activity.timestamp + chrono::Duration::days(2) < crev_common::now() {
return Err(ActivityCheckError::Expired);
}
} else {
return Err(ActivityCheckError::NoRecord);
}
Ok(diff)
}
pub fn check_package_clean_state(
repo: &Repo,
crate_root: &Path,
name: &str,
version: &Version,
) -> Result<(crev_data::Digest, Option<VcsInfoJson>)> {
let reviewed_pkg_dir: PathBuf =
crev_common::fs::append_to_path(crate_root.to_owned(), ".crev.reviewed");
if reviewed_pkg_dir.is_dir() {
std::fs::remove_dir_all(&reviewed_pkg_dir)?;
}
crev_common::fs::move_dir_content(crate_root, &reviewed_pkg_dir)?;
let crate_second_id = repo.find_pkgid(name, Some(version), true)?;
let crate_second = repo.get_crate(&crate_second_id)?;
let crate_root_second = crate_second.root();
let crate_version_second = crate_second.version();
assert_eq!(crate_root, crate_root_second);
assert_eq!(version, crate_version_second);
let digest_clean =
crev_lib::get_recursive_digest_for_dir(crate_root, &cargo_min_ignore_list())?;
let ignore_cargo_lock = !crate_root.join("Cargo.lock").exists();
let digest_reviewed = crev_lib::get_recursive_digest_for_dir(
&reviewed_pkg_dir,
&cargo_full_ignore_list(ignore_cargo_lock),
)?;
if digest_clean != digest_reviewed {
eprintln!(
r#"The digest of the reviewed source code is different from the digest of a freshly downloaded copy.
This is most probably caused by your actions: creating new or modified files.
Will continue with the digest of a fresh copy.
Fresh copy: {}
Reviewed code: {}."#,
crate_root.display(),
reviewed_pkg_dir.display(),
);
} else {
std::fs::remove_dir_all(&reviewed_pkg_dir)?;
}
let vcs = VcsInfoJson::read_from_crate_dir(crate_root)?;
Ok((digest_clean, vcs))
}
pub fn find_advisories(crate_: &opts::CrateSelector) -> Result<Vec<proof::review::Package>> {
let local = crev_lib::Local::auto_open()?;
let db = local.load_db()?;
Ok(db
.get_advisories(
PROJECT_SOURCE_CRATES_IO,
crate_.name.as_deref(),
crate_.version()?,
)
.cloned()
.collect())
}
pub fn run_diff(args: &opts::Diff) -> Result<std::process::ExitStatus> {
let repo = Repo::auto_open_cwd_default()?;
let name = &args.name;
let dst_version = &args.dst;
let dst_crate_id = repo.find_pkgid(name, dst_version.to_owned().as_ref(), args.unrelated)?;
let dst_crate = repo.get_crate(&dst_crate_id)?;
let requirements = crev_lib::VerificationRequirements::from(args.requirements.clone());
let trust_distance_params = &args.trust_params.clone().into();
let local = crev_lib::Local::auto_create_or_open()?;
let current_id = local.get_current_userid()?;
let db = local.load_db()?;
let trust_set = db.calculate_trust_set(¤t_id, trust_distance_params);
let src_version = args
.src
.clone()
.or_else(|| {
crev_lib::find_latest_trusted_version(
&trust_set,
PROJECT_SOURCE_CRATES_IO,
name,
&requirements,
&db,
)
})
.ok_or_else(|| format_err!("No previously reviewed version found"))?;
let src_crate_id = repo.find_pkgid(name, Some(&src_version), true)?;
let src_crate = repo.get_crate(&src_crate_id)?;
local.record_review_activity(
PROJECT_SOURCE_CRATES_IO,
name,
dst_crate.version(),
&crev_lib::ReviewActivity::new_diff(&src_version),
)?;
use std::process::Command;
let diff = |exe| {
let mut command = Command::new(exe);
command
.arg("-r")
.arg("-N")
.arg(src_crate.root())
.arg(dst_crate.root())
.args(&args.args);
command
};
let mut command = diff(OsStr::new("diff"));
match command.status() {
Err(ref err) if err.kind() == io::ErrorKind::NotFound && cfg!(windows) => {
let program_files =
env::var("ProgramFiles").unwrap_or_else(|_| r"C:\Program Files".to_owned());
let mut diff_exe = PathBuf::from(program_files);
diff_exe.push(r"Git\usr\bin\diff.exe");
let mut command = diff(diff_exe.as_os_str());
command
.status()
.or_else(|err| panic!("Failed to execute {:?}\n{:?}", command, err))
}
Err(ref err) => panic!("Failed to execute {:?}\n{:?}", command, err),
Ok(status) => Ok(status),
}
}
pub fn show_dir(sel: &opts::CrateSelector) -> Result<()> {
let repo = Repo::auto_open_cwd_default()?;
let _ = sel.ensure_name_given()?;
let crate_id = repo.find_pkgid_by_crate_selector(sel)?;
let crate_ = repo.get_crate(&crate_id)?;
println!("{}", crate_.root().display());
Ok(())
}
pub fn list_advisories(crate_: &opts::CrateSelector) -> Result<()> {
for review in find_advisories(crate_)? {
println!("---\n{}", review);
}
Ok(())
}
pub fn list_issues(args: &opts::RepoQueryIssue) -> Result<()> {
let trust_distance_params = args.trust_params.clone().into();
let local = crev_lib::Local::auto_open()?;
let current_id = local.get_current_userid()?;
let db = local.load_db()?;
let trust_set = db.calculate_trust_set(¤t_id, &trust_distance_params);
for review in db.get_pkg_reviews_with_issues_for(
PROJECT_SOURCE_CRATES_IO,
args.crate_.name.as_deref(),
args.crate_.version()?,
&trust_set,
args.trust_level.into(),
) {
println!("---\n{}", review);
}
Ok(())
}
pub fn are_we_called_from_goto_shell() -> Option<OsString> {
env::var_os(GOTO_ORIGINAL_DIR_ENV)
}
pub fn handle_goto_mode_command<F>(args: &opts::ReviewOrGotoCommon, f: F) -> Result<()>
where
F: FnOnce(&CrateSelector) -> Result<()>,
{
if let Some(org_dir) = are_we_called_from_goto_shell() {
if args.crate_.name.is_some() {
bail!("In `crev goto` mode no arguments can be given");
} else {
let name = env::var(GOTO_CRATE_NAME_ENV)
.map_err(|_| format_err!("crate name env var not found"))?;
let version = env::var(GOTO_CRATE_VERSION_ENV)
.map_err(|_| format_err!("crate version env var not found"))?;
env::set_current_dir(org_dir)?;
f(&CrateSelector::new(
Some(name),
Some(Version::parse(&version)?),
true,
))?;
}
} else {
args.crate_.ensure_name_given()?;
f(&args.crate_)?;
}
Ok(())
}
pub fn is_file_with_ext(entry: &walkdir::DirEntry, file_ext: &str) -> bool {
if !entry.file_type().is_file() {
return false;
}
entry
.path()
.extension()
.map(|ext| ext.to_string_lossy().as_ref() == file_ext)
.unwrap_or(false)
}
pub fn iter_rs_files_in_dir(dir: &Path) -> impl Iterator<Item = Result<PathBuf>> {
let walker = walkdir::WalkDir::new(dir).into_iter();
walker
.map(|entry| {
let entry = entry?;
if !is_file_with_ext(&entry, "rs") {
return Ok(None);
}
Ok(Some(entry.path().canonicalize()?))
})
.filter_map(|res| res.transpose())
}
pub fn get_geiger_count(path: &Path) -> Result<u64> {
let mut count = 0;
for metrics in iter_rs_files_in_dir(path)
.flat_map_ok(|path| geiger::find::find_unsafe_in_file(&path, geiger::IncludeTests::No))
{
let counters = metrics?.counters;
count += counters.functions.unsafe_
+ counters.exprs.unsafe_
+ counters.item_impls.unsafe_
+ counters.item_traits.unsafe_
+ counters.methods.unsafe_
}
Ok(count)
}
#[must_use]
pub enum CommandExitStatus {
VerificationFailed,
Success,
CommandExitCode(i32),
}
pub fn get_crate_digest_mismatches(
db: &crev_wot::ProofDB,
name: &str,
version: &Version,
digest: &crev_data::Digest,
) -> Vec<Package> {
db.get_package_reviews_for_package(PROJECT_SOURCE_CRATES_IO, Some(name), Some(version))
.filter(|review| review.package.digest != digest.as_slice())
.cloned()
.collect()
}
pub fn maybe_store(
local: &Local,
proof: &crev_data::proof::Proof,
commit_msg: &str,
proof_create_opt: &opts::CommonProofCreate,
) -> Result<()> {
if proof_create_opt.print_unsigned {
print!("{}", proof.body());
}
if proof_create_opt.print_signed {
print!("{}", proof);
}
if !proof_create_opt.no_store {
local.insert(proof)?;
if !proof_create_opt.no_commit {
local
.proof_dir_commit(commit_msg)
.with_context(|| "Could not not automatically commit")?;
}
}
Ok(())
}
pub fn lookup_crates(query: &str, count: usize) -> Result<()> {
struct CrateStats {
name: String,
downloads: u64,
proof_count: usize,
}
use crates_io_api::{CratesQuery, Sort, SyncClient};
let local = crev_lib::Local::auto_create_or_open()?;
let db = local.load_db()?;
let client = SyncClient::new("cargo-crev", std::time::Duration::from_secs(1))?;
let mut stats: Vec<_> = client
.crates(
CratesQuery::builder()
.sort(Sort::Downloads)
.page_size(100)
.search(query.to_string())
.build(),
)?
.crates
.iter()
.map(|crate_| CrateStats {
name: crate_.name.clone(),
downloads: crate_.downloads,
proof_count: db.get_package_review_count(
PROJECT_SOURCE_CRATES_IO,
Some(&crate_.name),
None,
),
})
.collect();
stats.sort_by(|a, b| {
a.proof_count
.cmp(&b.proof_count)
.then(a.downloads.cmp(&b.downloads))
.reverse()
});
for stats in stats.iter().take(count) {
println!("{:8} {}", stats.proof_count, stats.name);
}
Ok(())
}