Skip to main content

crev_lib/
lib.rs

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/// Failures that can happen in this library
38#[derive(Debug, thiserror::Error)]
39pub enum Error {
40    /// Trying to init a directory that is already there
41    #[error("`{}` already exists", _0.display())]
42    PathAlreadyExists(Box<Path>),
43
44    /// There are manual modifications in the git repo. Commit or reset them?
45    #[error("Git repository is not in a clean state")]
46    GitRepositoryIsNotInACleanState,
47
48    /// Found data from the future. Your version of crev is too old.
49    #[error("Unsupported version {}", _0)]
50    UnsupportedVersion(i64),
51
52    /// Your crev-id changed unexpectedly
53    #[error("PubKey mismatch")]
54    PubKeyMismatch,
55
56    /// You need to make a crev Id to perform most operations
57    #[error("User config not-initialized. Use `crev id new` to generate CrevID.")]
58    UserConfigNotInitialized,
59
60    /// Use `auto_create_or_open` or fix potentially messed up config directory
61    #[error("User config already exists")]
62    UserConfigAlreadyExists,
63
64    /// User config loading error
65    #[error("User config loading error '{}': {}", _0.0.display(), _0.1)]
66    UserConfigLoadError(Box<(PathBuf, std::io::Error)>),
67
68    /// You've sandboxed too hard? We need to run Cargo
69    #[error("No valid home directory path could be retrieved from the operating system")]
70    NoHomeDirectory,
71
72    /// This stores your private key
73    #[error("Id loading error '{}': {}", _0.0.display(), _0.1)]
74    IdLoadError(Box<(PathBuf, std::io::Error)>),
75
76    /// Create a new Id
77    #[error("Id file not found.")]
78    IDFileNotFound,
79
80    /// Crev repos must be public
81    #[error("Couldn't clone {}: {}", _0.0, _0.1)]
82    CouldNotCloneGitHttpsURL(Box<(String, String)>),
83
84    /// We don't support anonymous reviews
85    #[error("No ids given.")]
86    NoIdsGiven,
87
88    /// There's no password reset. If you don't remember it, start over!
89    #[error("Incorrect passphrase")]
90    IncorrectPassphrase,
91
92    /// crev has a concept of a default/current Id
93    #[error("Current Id not set")]
94    CurrentIDNotSet,
95
96    /// crev has a concept of a default/current Id
97    #[error("Id not specified and current id not set")]
98    IDNotSpecifiedAndCurrentIDNotSet,
99
100    /// crev uses git checkouts, and needs to know their URLs. Delete the repo and try again.
101    #[error("origin has no url at {}", _0.display())]
102    OriginHasNoURL(Box<Path>),
103
104    /// crev created a dummy Id for you, but you still need to configure it
105    #[error("current Id has been created without a git URL")]
106    GitUrlNotConfigured,
107
108    /// Error iterating local db
109    #[error("Error iterating local ProofStore at {}: {}", _0.0.display(), _0.1)]
110    ErrorIteratingLocalProofStore(Box<(PathBuf, String)>),
111
112    /// blake_hash mismatch
113    #[error("File {} not current. Review again use `crev add` to update.", _0.display())]
114    FileNotCurrent(Box<Path>),
115
116    /// Needs config.yaml
117    #[error("Package config not-initialized. Use `crev package init` to generate it.")]
118    PackageConfigNotInitialized,
119
120    /// Wrong path given to git
121    #[error("Can't stage path from outside of the staging root")]
122    PathNotInStageRootPath,
123
124    /// Git is cursed
125    #[error("Git entry without a path")]
126    GitEntryWithoutAPath,
127
128    /// Sorry about YAML syntax
129    #[error(transparent)]
130    YAML(#[from] serde_yaml::Error),
131
132    /// Used for staging temp file
133    #[error(transparent)]
134    CBOR(#[from] serde_cbor::Error),
135
136    /// See [`repo::PackageDirNotFound`]
137    #[error(transparent)]
138    PackageDirNotFound(#[from] repo::PackageDirNotFound),
139
140    /// See [`crev_common::CancelledError`]
141    #[error(transparent)]
142    Cancelled(#[from] crev_common::CancelledError),
143
144    /// See [`crev_data::Error`]
145    #[error(transparent)]
146    Data(#[from] crev_data::Error),
147
148    /// See [`argon2::Error`]
149    #[error("Passphrase: {}", _0)]
150    Passphrase(#[from] argon2::Error),
151
152    /// YAML ;(
153    #[error("Review activity parse error: {}", _0)]
154    ReviewActivity(#[source] Box<crev_common::YAMLIOError>),
155
156    /// YAML ;(
157    #[error("Error parsing user config: {}", _0)]
158    UserConfigParse(#[source] serde_yaml::Error),
159
160    /// See [`crev_recursive_digest::DigestError`]
161    #[error(transparent)]
162    Digest(#[from] crev_recursive_digest::DigestError),
163
164    /// Misc problems with git repos
165    #[error(transparent)]
166    Git(#[from] git2::Error),
167
168    /// Misc problems with file I/O
169    #[error("I/O: {}", _0)]
170    IO(#[from] std::io::Error),
171
172    /// crev open makes cargo projects that don't run the code
173    #[error("Error while copying crate sources: {}", _0)]
174    CrateSourceSanitizationError(std::io::Error),
175
176    /// Misc problems with file I/O
177    #[error("Error writing to {}: {}", _1.display(), _0)]
178    FileWrite(std::io::Error, PathBuf),
179
180    /// See [`IdError`]
181    #[error(transparent)]
182    Id(#[from] IdError),
183}
184
185/// [`crate::Error`]
186type Result<T, E = Error> = std::result::Result<T, E>;
187
188/// Trait representing a place that can keep proofs (all reviews and trust proofs)
189///
190/// See [`::crev_wot::ProofDb`] and [`crate::Local`].
191///
192/// Typically serialized and persisted.
193#[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/// Your relationship to the person
200#[derive(Copy, Clone, PartialEq, Eq)]
201pub enum TrustProofType {
202    /// Positive
203    Trust,
204    /// Neutral (undo Trust)
205    Untrust,
206    /// Very negative. This is an attacker. Block everything by them.
207    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    /// Is this person trusted at all? (regardless of level of trust)
222    #[must_use]
223    pub fn is_trust(self) -> bool {
224        if let TrustProofType::Trust = self {
225            return true;
226        }
227        false
228    }
229
230    /// Make review template. See [`crev_data::Review`]
231    #[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/// Verification requirements for filtering out low quality reviews
243///
244/// See [`crev_wot::TrustDistanceParams`]
245#[derive(Clone, Debug)]
246pub struct VerificationRequirements {
247    /// How much the reviewer must be trusted
248    pub trust_level: crev_data::Level,
249    /// How much code understanding reviewer has reported
250    pub understanding: crev_data::Level,
251    /// How much thoroughness reviewer has reported
252    pub thoroughness: crev_data::Level,
253    /// How many different reviews are required
254    pub redundancy: u64,
255    /// Whether to ignore reviews performed by LLM agents
256    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/// Result of verification
272///
273/// Not named `Result` to avoid confusion with `Result` type.
274#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord)]
275pub enum VerificationStatus {
276    /// That's bad!
277    Negative,
278    /// VerificationRequirements set too high
279    Insufficient,
280    /// Okay
281    Verified,
282    /// This is your package, trust yourself.
283    Local,
284}
285
286impl VerificationStatus {
287    /// Is it `VerificationStatus::Verified`?
288    #[must_use]
289    pub fn is_verified(self) -> bool {
290        self == VerificationStatus::Verified
291    }
292
293    /// Pick worse of both
294    #[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
317/// Find reviews matching `Digest` (exact data of the crate)
318/// and see if there are enough positive reviews for it.
319pub 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    // Faster somehow maybe?
342    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/// Warnings gathered during operation, errors downgraded to warnings.
378#[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
437/// Scan through known reviews of the crate (source is `"https://crates.io"`)
438/// and report semver you can safely use according to `requirements`
439///
440/// See also `verify_package_digest`
441pub 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
462/// Check whether code at this path has reviews, and the reviews meet the requirements.
463///
464/// See also `verify_package_digest`
465pub 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
486/// Check whether code at this path has reviews, and the reviews meet the requirements
487///
488/// Same as `dir_or_git_repo_verify`, except it doesn't handle .git dirs
489pub 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
506/// Scan dir and hash everything in it, to get a unique identifier of the package's source code
507pub 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
511/// See `get_dir_digest`
512pub 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
535/// See `get_dir_digest`
536pub 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
543/// See `get_dir_digest`
544pub 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;