#![type_length_limit = "10709970"]
#![allow(clippy::implicit_hasher)]
#![allow(clippy::items_after_statements)]
#![allow(clippy::manual_range_contains)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::redundant_closure_for_method_calls)]
pub mod activity;
pub mod id;
pub mod local;
pub mod proof;
pub mod repo;
pub mod staging;
pub mod util;
pub use crate::local::Local;
pub use activity::{ReviewActivity, ReviewMode};
use crev_data::{
self,
id::IdError,
proof::{
review::{self, Rating},
trust::TrustLevel,
CommonOps,
},
Digest, Id, RegistrySource, Version,
};
use crev_wot::PkgVersionReviewId;
pub use crev_wot::TrustDistanceParams;
use log::warn;
use std::error::Error as _;
use std::{
collections::{HashMap, HashSet},
fmt,
path::{Path, PathBuf},
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("`{}` already exists", _0.display())]
PathAlreadyExists(Box<Path>),
#[error("Git repository is not in a clean state")]
GitRepositoryIsNotInACleanState,
#[error("Unsupported version {}", _0)]
UnsupportedVersion(i64),
#[error("PubKey mismatch")]
PubKeyMismatch,
#[error("User config not-initialized. Use `crev id new` to generate CrevID.")]
UserConfigNotInitialized,
#[error("User config already exists")]
UserConfigAlreadyExists,
#[error("User config loading error '{}': {}", _0.0.display(), _0.1)]
UserConfigLoadError(Box<(PathBuf, std::io::Error)>),
#[error("No valid home directory path could be retrieved from the operating system")]
NoHomeDirectory,
#[error("Id loading error '{}': {}", _0.0.display(), _0.1)]
IdLoadError(Box<(PathBuf, std::io::Error)>),
#[error("Id file not found.")]
IDFileNotFound,
#[error("Couldn't clone {}: {}", _0.0, _0.1)]
CouldNotCloneGitHttpsURL(Box<(String, String)>),
#[error("No ids given.")]
NoIdsGiven,
#[error("Incorrect passphrase")]
IncorrectPassphrase,
#[error("Current Id not set")]
CurrentIDNotSet,
#[error("Id not specified and current id not set")]
IDNotSpecifiedAndCurrentIDNotSet,
#[error("origin has no url at {}", _0.display())]
OriginHasNoURL(Box<Path>),
#[error("current Id has been created without a git URL")]
GitUrlNotConfigured,
#[error("Error iterating local ProofStore at {}: {}", _0.0.display(), _0.1)]
ErrorIteratingLocalProofStore(Box<(PathBuf, String)>),
#[error("File {} not current. Review again use `crev add` to update.", _0.display())]
FileNotCurrent(Box<Path>),
#[error("Package config not-initialized. Use `crev package init` to generate it.")]
PackageConfigNotInitialized,
#[error("Can't stage path from outside of the staging root")]
PathNotInStageRootPath,
#[error("Git entry without a path")]
GitEntryWithoutAPath,
#[error(transparent)]
YAML(#[from] serde_yaml::Error),
#[error(transparent)]
CBOR(#[from] serde_cbor::Error),
#[error(transparent)]
PackageDirNotFound(#[from] repo::PackageDirNotFound),
#[error(transparent)]
Cancelled(#[from] crev_common::CancelledError),
#[error(transparent)]
Data(#[from] crev_data::Error),
#[error("Passphrase: {}", _0)]
Passphrase(#[from] argon2::Error),
#[error("Review activity parse error: {}", _0)]
ReviewActivity(#[source] Box<crev_common::YAMLIOError>),
#[error("Error parsing user config: {}", _0)]
UserConfigParse(#[source] serde_yaml::Error),
#[error(transparent)]
Digest(#[from] crev_recursive_digest::DigestError),
#[error(transparent)]
Git(#[from] git2::Error),
#[error("I/O: {}", _0)]
IO(#[from] std::io::Error),
#[error("Error while copying crate sources: {}", _0)]
CrateSourceSanitizationError(std::io::Error),
#[error("Error writing to {}: {}", _1.display(), _0)]
FileWrite(std::io::Error, PathBuf),
#[error(transparent)]
Id(#[from] IdError),
}
type Result<T, E = Error> = std::result::Result<T, E>;
#[doc(hidden)]
pub trait ProofStore {
fn insert(&self, proof: &crev_data::proof::Proof) -> Result<()>;
fn proofs_iter(&self) -> Result<Box<dyn Iterator<Item = crev_data::proof::Proof>>>;
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TrustProofType {
Trust,
Untrust,
Distrust,
}
impl fmt::Display for TrustProofType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TrustProofType::Trust => f.write_str("trust"),
TrustProofType::Distrust => f.write_str("distrust"),
TrustProofType::Untrust => f.write_str("untrust"),
}
}
}
impl TrustProofType {
#[must_use]
pub fn is_trust(self) -> bool {
if let TrustProofType::Trust = self {
return true;
}
false
}
#[must_use]
pub fn to_review(self) -> crev_data::Review {
use TrustProofType::{Distrust, Trust, Untrust};
match self {
Trust => crev_data::Review::new_positive(),
Distrust => crev_data::Review::new_negative(),
Untrust => crev_data::Review::new_none(),
}
}
}
#[derive(Clone, Debug)]
pub struct VerificationRequirements {
pub trust_level: crev_data::Level,
pub understanding: crev_data::Level,
pub thoroughness: crev_data::Level,
pub redundancy: u64,
}
impl Default for VerificationRequirements {
fn default() -> Self {
VerificationRequirements {
trust_level: Default::default(),
understanding: Default::default(),
thoroughness: Default::default(),
redundancy: 1,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord)]
pub enum VerificationStatus {
Negative,
Insufficient,
Verified,
Local,
}
impl VerificationStatus {
#[must_use]
pub fn is_verified(self) -> bool {
self == VerificationStatus::Verified
}
#[must_use]
pub fn min(self, other: Self) -> Self {
if self < other {
self
} else if other < self {
other
} else {
self
}
}
}
impl fmt::Display for VerificationStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VerificationStatus::Local => f.pad("locl"),
VerificationStatus::Verified => f.pad("pass"),
VerificationStatus::Insufficient => f.pad("none"),
VerificationStatus::Negative => f.pad("warn"),
}
}
}
pub fn verify_package_digest(
digest: &Digest,
trust_set: &crev_wot::TrustSet,
requirements: &VerificationRequirements,
db: &crev_wot::ProofDB,
) -> VerificationStatus {
let reviews: HashMap<Id, review::Package> = db
.get_package_reviews_by_digest(digest)
.filter(|review| {
match trust_set
.package_review_ignore_override
.get(&PkgVersionReviewId::from(review))
{
Some(reporters) => {
reporters.max_level().unwrap_or(TrustLevel::None)
<= trust_set.get_effective_trust_level(&review.common.from.id)
}
None => true,
}
})
.map(|review| (review.from().id.clone(), review))
.collect();
let reviews_by: HashSet<Id, _> = reviews.keys().cloned().collect();
let trusted_ids: HashSet<_> = trust_set.get_trusted_ids();
let matching_reviewers = trusted_ids.intersection(&reviews_by);
let mut trust_count = 0;
let mut negative_count = 0;
for matching_reviewer in matching_reviewers {
let review = &reviews[matching_reviewer].review_possibly_none();
if !review.is_none()
&& Rating::Neutral <= review.rating
&& requirements.thoroughness <= review.thoroughness
&& requirements.understanding <= review.understanding
{
if TrustLevel::from(requirements.trust_level)
<= trust_set.get_effective_trust_level(matching_reviewer)
{
trust_count += 1;
}
} else if review.rating <= Rating::Negative {
negative_count += 1;
}
}
if negative_count > 0 {
VerificationStatus::Negative
} else if trust_count >= requirements.redundancy {
VerificationStatus::Verified
} else {
VerificationStatus::Insufficient
}
}
#[derive(Debug, thiserror::Error)]
pub enum Warning {
#[error(transparent)]
Error(#[from] Error),
#[error("Repo checkout without origin URL at {}", _0.display())]
NoRepoUrlAtPath(PathBuf, #[source] Error),
#[error("URL for {0} is not known yet")]
IdUrlNotKnonw(Id),
#[error("Could not deduce `ssh` push url for {0}. Call:\ncargo crev repo git remote set-url --push origin <url>\nmanually after the id is generated.")]
GitPushUrl(String),
#[error("Failed to fetch {0} into {}", _2.display())]
FetchError(String, #[source] Error, PathBuf),
}
impl Warning {
#[must_use]
pub fn auto_log() -> LogOnDrop {
LogOnDrop(Vec::new())
}
pub fn log_all(warnings: &[Warning]) {
warnings.iter().for_each(|w| w.log());
}
pub fn log(&self) {
warn!("{}", self);
let mut s = self.source();
while let Some(w) = s {
warn!(" - {}", w);
s = w.source();
}
}
}
pub struct LogOnDrop(pub Vec<Warning>);
impl Drop for LogOnDrop {
fn drop(&mut self) {
Warning::log_all(&self.0);
}
}
impl std::ops::Deref for LogOnDrop {
type Target = Vec<Warning>;
fn deref(&self) -> &Vec<Warning> {
&self.0
}
}
impl std::ops::DerefMut for LogOnDrop {
fn deref_mut(&mut self) -> &mut Vec<Warning> {
&mut self.0
}
}
pub fn find_latest_trusted_version(
trust_set: &crev_wot::TrustSet,
source: RegistrySource<'_>,
name: &str,
requirements: &crate::VerificationRequirements,
db: &crev_wot::ProofDB,
) -> Option<Version> {
db.get_pkg_reviews_for_name(source, name)
.filter(|review| {
verify_package_digest(
&Digest::from_bytes(&review.package.digest).unwrap(),
trust_set,
requirements,
db,
)
.is_verified()
})
.max_by(|a, b| a.package.id.version.cmp(&b.package.id.version))
.map(|review| review.package.id.version.clone())
}
pub fn dir_or_git_repo_verify(
path: &Path,
ignore_list: &fnv::FnvHashSet<PathBuf>,
db: &crev_wot::ProofDB,
trusted_set: &crev_wot::TrustSet,
requirements: &VerificationRequirements,
) -> Result<crate::VerificationStatus> {
let digest = if path.join(".git").exists() {
get_recursive_digest_for_git_dir(path, ignore_list)?
} else {
Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap()
};
Ok(verify_package_digest(
&digest,
trusted_set,
requirements,
db,
))
}
pub fn dir_verify(
path: &Path,
ignore_list: &fnv::FnvHashSet<PathBuf>,
db: &crev_wot::ProofDB,
trusted_set: &crev_wot::TrustSet,
requirements: &VerificationRequirements,
) -> Result<crate::VerificationStatus> {
let digest = Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap();
Ok(verify_package_digest(
&digest,
trusted_set,
requirements,
db,
))
}
pub fn get_dir_digest(path: &Path, ignore_list: &fnv::FnvHashSet<PathBuf>) -> Result<Digest> {
Ok(Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap())
}
pub fn get_recursive_digest_for_git_dir(
root_path: &Path,
ignore_list: &fnv::FnvHashSet<PathBuf>,
) -> Result<Digest> {
let git_repo = git2::Repository::open(root_path)?;
let mut status_opts = git2::StatusOptions::new();
let mut paths = HashSet::default();
status_opts.include_unmodified(true);
status_opts.include_untracked(false);
for entry in git_repo.statuses(Some(&mut status_opts))?.iter() {
let entry_path = PathBuf::from(entry.path().ok_or(Error::GitEntryWithoutAPath)?);
if ignore_list.contains(&entry_path) {
continue;
};
paths.insert(entry_path);
}
Ok(util::get_recursive_digest_for_paths(root_path, paths)?)
}
pub fn get_recursive_digest_for_paths(
root_path: &Path,
paths: fnv::FnvHashSet<PathBuf>,
) -> Result<crev_data::Digest> {
Ok(util::get_recursive_digest_for_paths(root_path, paths)?)
}
pub fn get_recursive_digest_for_dir(
root_path: &Path,
rel_path_ignore_list: &fnv::FnvHashSet<PathBuf>,
) -> Result<Digest> {
Ok(Digest::from_bytes(&util::get_recursive_digest_for_dir(
root_path,
rel_path_ignore_list,
)?)
.unwrap())
}
#[cfg(test)]
mod tests;