1#![type_length_limit = "10709970"]
2#![allow(clippy::implicit_hasher)]
3#![allow(clippy::items_after_statements)]
4#![allow(clippy::manual_range_contains)]
5#![allow(clippy::missing_errors_doc)]
6#![allow(clippy::missing_panics_doc)]
7#![allow(clippy::redundant_closure_for_method_calls)]
8
9pub mod activity;
10pub mod id;
11pub mod local;
12pub mod proof;
13pub mod repo;
14pub mod staging;
15pub mod util;
16pub use crate::local::Local;
17pub use activity::{ReviewActivity, ReviewMode};
18use crev_data::{
19 self,
20 id::IdError,
21 proof::{
22 review::{self, Rating},
23 trust::TrustLevel,
24 CommonOps,
25 },
26 Digest, Id, RegistrySource, Version,
27};
28use crev_wot::PkgVersionReviewId;
29pub use crev_wot::TrustDistanceParams;
30use log::warn;
31use std::error::Error as _;
32use std::{
33 collections::{HashMap, HashSet},
34 fmt,
35 path::{Path, PathBuf},
36};
37
38#[derive(Debug, thiserror::Error)]
40pub enum Error {
41 #[error("`{}` already exists", _0.display())]
43 PathAlreadyExists(Box<Path>),
44
45 #[error("Git repository is not in a clean state")]
47 GitRepositoryIsNotInACleanState,
48
49 #[error("Unsupported version {}", _0)]
51 UnsupportedVersion(i64),
52
53 #[error("PubKey mismatch")]
55 PubKeyMismatch,
56
57 #[error("User config not-initialized. Use `crev id new` to generate CrevID.")]
59 UserConfigNotInitialized,
60
61 #[error("User config already exists")]
63 UserConfigAlreadyExists,
64
65 #[error("User config loading error '{}': {}", _0.0.display(), _0.1)]
67 UserConfigLoadError(Box<(PathBuf, std::io::Error)>),
68
69 #[error("No valid home directory path could be retrieved from the operating system")]
71 NoHomeDirectory,
72
73 #[error("Id loading error '{}': {}", _0.0.display(), _0.1)]
75 IdLoadError(Box<(PathBuf, std::io::Error)>),
76
77 #[error("Id file not found.")]
79 IDFileNotFound,
80
81 #[error("Couldn't clone {}: {}", _0.0, _0.1)]
83 CouldNotCloneGitHttpsURL(Box<(String, String)>),
84
85 #[error("No ids given.")]
87 NoIdsGiven,
88
89 #[error("Incorrect passphrase")]
91 IncorrectPassphrase,
92
93 #[error("Current Id not set")]
95 CurrentIDNotSet,
96
97 #[error("Id not specified and current id not set")]
99 IDNotSpecifiedAndCurrentIDNotSet,
100
101 #[error("origin has no url at {}", _0.display())]
103 OriginHasNoURL(Box<Path>),
104
105 #[error("current Id has been created without a git URL")]
107 GitUrlNotConfigured,
108
109 #[error("Error iterating local ProofStore at {}: {}", _0.0.display(), _0.1)]
111 ErrorIteratingLocalProofStore(Box<(PathBuf, String)>),
112
113 #[error("File {} not current. Review again use `crev add` to update.", _0.display())]
115 FileNotCurrent(Box<Path>),
116
117 #[error("Package config not-initialized. Use `crev package init` to generate it.")]
119 PackageConfigNotInitialized,
120
121 #[error("Can't stage path from outside of the staging root")]
123 PathNotInStageRootPath,
124
125 #[error("Git entry without a path")]
127 GitEntryWithoutAPath,
128
129 #[error(transparent)]
131 YAML(#[from] serde_yaml::Error),
132
133 #[error(transparent)]
135 CBOR(#[from] serde_cbor::Error),
136
137 #[error(transparent)]
139 PackageDirNotFound(#[from] repo::PackageDirNotFound),
140
141 #[error(transparent)]
143 Cancelled(#[from] crev_common::CancelledError),
144
145 #[error(transparent)]
147 Data(#[from] crev_data::Error),
148
149 #[error("Passphrase: {}", _0)]
151 Passphrase(#[from] argon2::Error),
152
153 #[error("Review activity parse error: {}", _0)]
155 ReviewActivity(#[source] Box<crev_common::YAMLIOError>),
156
157 #[error("Error parsing user config: {}", _0)]
159 UserConfigParse(#[source] serde_yaml::Error),
160
161 #[error(transparent)]
163 Digest(#[from] crev_recursive_digest::DigestError),
164
165 #[error(transparent)]
167 Git(#[from] git2::Error),
168
169 #[error("I/O: {}", _0)]
171 IO(#[from] std::io::Error),
172
173 #[error("Error while copying crate sources: {}", _0)]
175 CrateSourceSanitizationError(std::io::Error),
176
177 #[error("Error writing to {}: {}", _1.display(), _0)]
179 FileWrite(std::io::Error, PathBuf),
180
181 #[error(transparent)]
183 Id(#[from] IdError),
184}
185
186type Result<T, E = Error> = std::result::Result<T, E>;
188
189#[doc(hidden)]
195pub trait ProofStore {
196 fn insert(&self, proof: &crev_data::proof::Proof) -> Result<()>;
197 fn proofs_iter(&self) -> Result<Box<dyn Iterator<Item = crev_data::proof::Proof>>>;
198}
199
200#[derive(Copy, Clone, PartialEq, Eq)]
202pub enum TrustProofType {
203 Trust,
205 Untrust,
207 Distrust,
209}
210
211impl fmt::Display for TrustProofType {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 match self {
214 TrustProofType::Trust => f.write_str("trust"),
215 TrustProofType::Distrust => f.write_str("distrust"),
216 TrustProofType::Untrust => f.write_str("untrust"),
217 }
218 }
219}
220
221impl TrustProofType {
222 #[must_use]
224 pub fn is_trust(self) -> bool {
225 if let TrustProofType::Trust = self {
226 return true;
227 }
228 false
229 }
230
231 #[must_use]
233 pub fn to_review(self) -> crev_data::Review {
234 use TrustProofType::{Distrust, Trust, Untrust};
235 match self {
236 Trust => crev_data::Review::new_positive(),
237 Distrust => crev_data::Review::new_negative(),
238 Untrust => crev_data::Review::new_none(),
239 }
240 }
241}
242
243#[derive(Clone, Debug)]
247pub struct VerificationRequirements {
248 pub trust_level: crev_data::Level,
250 pub understanding: crev_data::Level,
252 pub thoroughness: crev_data::Level,
254 pub redundancy: u64,
256}
257
258impl Default for VerificationRequirements {
259 fn default() -> Self {
260 VerificationRequirements {
261 trust_level: Default::default(),
262 understanding: Default::default(),
263 thoroughness: Default::default(),
264 redundancy: 1,
265 }
266 }
267}
268
269#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord)]
273pub enum VerificationStatus {
274 Negative,
276 Insufficient,
278 Verified,
280 Local,
282}
283
284impl VerificationStatus {
285 #[must_use]
287 pub fn is_verified(self) -> bool {
288 self == VerificationStatus::Verified
289 }
290
291 #[must_use]
293 pub fn min(self, other: Self) -> Self {
294 if self < other {
295 self
296 } else if other < self {
297 other
298 } else {
299 self
300 }
301 }
302}
303
304impl fmt::Display for VerificationStatus {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 match self {
307 VerificationStatus::Local => f.pad("locl"),
308 VerificationStatus::Verified => f.pad("pass"),
309 VerificationStatus::Insufficient => f.pad("none"),
310 VerificationStatus::Negative => f.pad("warn"),
311 }
312 }
313}
314
315pub fn verify_package_digest(
318 digest: &Digest,
319 trust_set: &crev_wot::TrustSet,
320 requirements: &VerificationRequirements,
321 db: &crev_wot::ProofDB,
322) -> VerificationStatus {
323 let reviews: HashMap<Id, review::Package> = db
324 .get_package_reviews_by_digest(digest)
325 .filter(|review| {
326 match trust_set
327 .package_review_ignore_override
328 .get(&PkgVersionReviewId::from(review))
329 {
330 Some(reporters) => {
331 reporters.max_level().unwrap_or(TrustLevel::None)
332 <= trust_set.get_effective_trust_level(&review.common.from.id)
333 }
334 None => true,
335 }
336 })
337 .map(|review| (review.from().id.clone(), review))
338 .collect();
339 let reviews_by: HashSet<Id, _> = reviews.keys().cloned().collect();
341 let trusted_ids: HashSet<_> = trust_set.get_trusted_ids();
342 let matching_reviewers = trusted_ids.intersection(&reviews_by);
343 let mut trust_count = 0;
344 let mut negative_count = 0;
345 for matching_reviewer in matching_reviewers {
346 let review = &reviews[matching_reviewer].review_possibly_none();
347 if !review.is_none()
348 && Rating::Neutral <= review.rating
349 && requirements.thoroughness <= review.thoroughness
350 && requirements.understanding <= review.understanding
351 {
352 if TrustLevel::from(requirements.trust_level)
353 <= trust_set.get_effective_trust_level(matching_reviewer)
354 {
355 trust_count += 1;
356 }
357 } else if review.rating <= Rating::Negative {
358 negative_count += 1;
359 }
360 }
361
362 if negative_count > 0 {
363 VerificationStatus::Negative
364 } else if trust_count >= requirements.redundancy {
365 VerificationStatus::Verified
366 } else {
367 VerificationStatus::Insufficient
368 }
369}
370
371#[derive(Debug, thiserror::Error)]
373pub enum Warning {
374 #[error(transparent)]
375 Error(#[from] Error),
376
377 #[error("Repo checkout without origin URL at {}", _0.display())]
378 NoRepoUrlAtPath(PathBuf, #[source] Error),
379
380 #[error("URL for {0} is not known yet")]
381 IdUrlNotKnonw(Id),
382
383 #[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.")]
384 GitPushUrl(String),
385
386 #[error("Failed to fetch {0} into {path}", path = _2.display())]
387 FetchError(String, #[source] Error, PathBuf),
388}
389
390impl Warning {
391 #[must_use]
392 pub fn auto_log() -> LogOnDrop {
393 LogOnDrop(Vec::new())
394 }
395
396 pub fn log_all(warnings: &[Warning]) {
397 warnings.iter().for_each(|w| w.log());
398 }
399
400 pub fn log(&self) {
401 warn!("{}", self);
402 let mut s = self.source();
403 while let Some(w) = s {
404 warn!(" - {}", w);
405 s = w.source();
406 }
407 }
408}
409
410pub struct LogOnDrop(pub Vec<Warning>);
411impl Drop for LogOnDrop {
412 fn drop(&mut self) {
413 Warning::log_all(&self.0);
414 }
415}
416
417impl std::ops::Deref for LogOnDrop {
418 type Target = Vec<Warning>;
419 fn deref(&self) -> &Vec<Warning> {
420 &self.0
421 }
422}
423impl std::ops::DerefMut for LogOnDrop {
424 fn deref_mut(&mut self) -> &mut Vec<Warning> {
425 &mut self.0
426 }
427}
428
429pub fn find_latest_trusted_version(
434 trust_set: &crev_wot::TrustSet,
435 source: RegistrySource<'_>,
436 name: &str,
437 requirements: &crate::VerificationRequirements,
438 db: &crev_wot::ProofDB,
439) -> Option<Version> {
440 db.get_pkg_reviews_for_name(source, name)
441 .filter(|review| {
442 verify_package_digest(
443 &Digest::from_bytes(&review.package.digest).unwrap(),
444 trust_set,
445 requirements,
446 db,
447 )
448 .is_verified()
449 })
450 .max_by(|a, b| a.package.id.version.cmp(&b.package.id.version))
451 .map(|review| review.package.id.version.clone())
452}
453
454pub fn dir_or_git_repo_verify(
458 path: &Path,
459 ignore_list: &fnv::FnvHashSet<PathBuf>,
460 db: &crev_wot::ProofDB,
461 trusted_set: &crev_wot::TrustSet,
462 requirements: &VerificationRequirements,
463) -> Result<crate::VerificationStatus> {
464 let digest = if path.join(".git").exists() {
465 get_recursive_digest_for_git_dir(path, ignore_list)?
466 } else {
467 Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap()
468 };
469
470 Ok(verify_package_digest(
471 &digest,
472 trusted_set,
473 requirements,
474 db,
475 ))
476}
477
478pub fn dir_verify(
482 path: &Path,
483 ignore_list: &fnv::FnvHashSet<PathBuf>,
484 db: &crev_wot::ProofDB,
485 trusted_set: &crev_wot::TrustSet,
486 requirements: &VerificationRequirements,
487) -> Result<crate::VerificationStatus> {
488 let digest = Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap();
489 Ok(verify_package_digest(
490 &digest,
491 trusted_set,
492 requirements,
493 db,
494 ))
495}
496
497pub fn get_dir_digest(path: &Path, ignore_list: &fnv::FnvHashSet<PathBuf>) -> Result<Digest> {
499 Ok(Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap())
500}
501
502pub fn get_recursive_digest_for_git_dir(
504 root_path: &Path,
505 ignore_list: &fnv::FnvHashSet<PathBuf>,
506) -> Result<Digest> {
507 let git_repo = git2::Repository::open(root_path)?;
508
509 let mut status_opts = git2::StatusOptions::new();
510 let mut paths = HashSet::default();
511
512 status_opts.include_unmodified(true);
513 status_opts.include_untracked(false);
514 for entry in git_repo.statuses(Some(&mut status_opts))?.iter() {
515 let entry_path = PathBuf::from(entry.path().ok_or(Error::GitEntryWithoutAPath)?);
516 if ignore_list.contains(&entry_path) {
517 continue;
518 };
519
520 paths.insert(entry_path);
521 }
522
523 Ok(util::get_recursive_digest_for_paths(root_path, paths)?)
524}
525
526pub fn get_recursive_digest_for_paths(
528 root_path: &Path,
529 paths: fnv::FnvHashSet<PathBuf>,
530) -> Result<crev_data::Digest> {
531 Ok(util::get_recursive_digest_for_paths(root_path, paths)?)
532}
533
534pub fn get_recursive_digest_for_dir(
536 root_path: &Path,
537 rel_path_ignore_list: &fnv::FnvHashSet<PathBuf>,
538) -> Result<Digest> {
539 Ok(Digest::from_bytes(&util::get_recursive_digest_for_dir(
540 root_path,
541 rel_path_ignore_list,
542 )?)
543 .unwrap())
544}
545
546#[cfg(test)]
547mod tests;