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