cargo-crev 0.26.2

Distibuted Code REView system for verifying security and quality of Cargo dependencies
Documentation
use crate::{
    edit,
    opts::CargoOpts,
    opts::{self, ReviewCrateSelector},
    prelude::*,
    term, url_to_status_str,
};
use anyhow::format_err;
use crev_data::{
    proof::{self, ContentExt},
    Rating, SOURCE_CRATES_IO,
};
use crev_lib::{self, local::Local, TrustProofType};
use std::{default::Default, fmt::Write};

use crate::{repo::Repo, shared::*};

/// Review a crate
///
/// * `unrelated` - the crate might not actually be a dependency
#[allow(clippy::option_option)]
pub fn create_review_proof(
    crate_sel: &ReviewCrateSelector,
    report_severity: Option<crev_data::Level>,
    advise_common: Option<opts::AdviseCommon>,
    trust: TrustProofType,
    proof_create_opt: &opts::CommonProofCreate,
    skip_activity_check: bool,
    show_override_suggestions: bool,
    cargo_opts: CargoOpts,
) -> Result<()> {
    let diff_version = &crate_sel.diff;
    let crate_sel = &crate_sel.crate_;

    let repo = Repo::auto_open_cwd(cargo_opts)?;

    let pkg_id = repo.find_pkgid_by_crate_selector(crate_sel)?;
    let crate_ = repo.get_crate(&pkg_id)?;
    let crate_root = crate_.root();
    let effective_crate_version = crate_.version();

    // We check the working directory because of how check_package_clean_state modifies the
    // contents of the crate root, moving everything out of the directory.
    // Therefore, it’s acceptable to run from the crate root directory or from outside the crate
    // root directory, but not from a subdirectory of the crate root, because it may fail or
    // misbehave on some platforms and is likely to exhibit confusing behaviour elsewhere.
    //
    // (While talking of problematic working directories: the "{crate_root}.crev.reviewed"
    // directory would be bad, but is not trivially (in lines of code) to detect since
    // OsStr::starts_with doesn’t exist; and symlinks can probably wreak havoc. But neither of
    // these are likely to be encountered accidentally, so they’re not worth worrying about.)
    //
    // FIXME: in goto shells, cwd has already been changed to the GOTO_ORIGINAL_DIR,
    // but we should actually be checking the process’s original cwd, either instead of or as well
    // as, as that’s the cwd from the *user’s* perspective. As it stands, you could easily goto a
    // crate, switch to a subdirectory, review, and either have fs errors (Windows, probably) or be
    // returned to a deleted directory (most other OSes).
    let cwd = std::env::current_dir()?;
    assert!(
        !cwd.starts_with(crate_root) || cwd == crate_root,
        "Cannot create review proof while in a subdirectory of {}.",
        crate_root.display(),
    );
    let local = Local::auto_open()?;

    let diff_base_version = match crate_review_activity_check(
        &local,
        &pkg_id.name(),
        effective_crate_version,
        diff_version,
        skip_activity_check,
    ) {
        Ok(res) => res,
        Err(ActivityCheckError::NoPreviousReview) => bail!("No previous review activity to determine base version"),
        Err(ActivityCheckError::UnexpectedFullReview) => bail!(
            "Last review activity record for {}:{} indicates full review. \
             Use `--diff` flag? Use `--skip-activity-check` to override.",
            pkg_id.name(),
            effective_crate_version
        ),
        Err(ActivityCheckError::UnexpectedDiffReview) => bail!(
            "Last review activity record for {}:{} indicates differential review. \
             Use `--diff` flag? Use `--skip-activity-check` to override.",
            pkg_id.name(),
            effective_crate_version
        ),
        Err(ActivityCheckError::Expired) =>  bail!(
            "Last review activity record for {}:{} is too old. \
             Re-review or use `--skip-activity-check` to override.",
            pkg_id.name(),
            effective_crate_version
        ),
        Err(ActivityCheckError::NoRecord) => bail!(
            "No review activity record for {name}:{} found. \
             Make sure you have reviewed the code in this version before creating review proof. \n\
             Use `cargo crev open {name}` or `cargo crev goto {name}` to review the code, or `--skip-activity-check` to override.",
            effective_crate_version,
            name = pkg_id.name(),
        ),
        Err(ActivityCheckError::Other(e)) => return Err(e.into()),
    };

    let (digest_clean, vcs) =
        check_package_clean_state(&repo, crate_root, &crate_.name(), effective_crate_version)?;

    let diff_base = if let Some(ref diff_base_version) = diff_base_version {
        let crate_id = repo.find_pkgid(&crate_.name(), Some(diff_base_version), true)?;
        let crate_ = repo.get_crate(&crate_id)?;
        let crate_root = crate_.root();

        let (digest, vcs) =
            check_package_clean_state(&repo, crate_root, &crate_.name(), diff_base_version)?;

        Some(proof::PackageInfo {
            id: proof::PackageVersionId::new(
                SOURCE_CRATES_IO.to_owned(),
                crate_.name().to_string(),
                diff_base_version.clone(),
            ),
            digest: digest.into_vec(),
            digest_type: proof::default_digest_type(),
            revision: vcs_info_to_revision_string(vcs),
            revision_type: proof::default_revision_type(),
        })
    } else {
        None
    };

    let id = local.read_current_unlocked_id(&term::read_passphrase)?;

    let db = local.load_db()?;

    let default_review_content = if advise_common.is_some() || report_severity.is_some() {
        crev_data::Review::new_none()
    } else {
        trust.to_review()
    };

    let (previous_date, mut review) = if let Some(mut previous_review) = db
        .get_pkg_review(
            SOURCE_CRATES_IO,
            &crate_.name(),
            effective_crate_version,
            &id.id.id,
        )
        .cloned()
    {
        if trust == TrustProofType::Untrust {
            *previous_review.review_possibly_none_mut() = default_review_content;
        }
        (Some(previous_review.common.date), previous_review)
    } else {
        let mut fresh_review = proof::review::PackageBuilder::default()
            .from(id.id.clone())
            .package(proof::PackageInfo {
                id: proof::PackageVersionId::new(
                    SOURCE_CRATES_IO.to_owned(),
                    crate_.name().to_string(),
                    effective_crate_version.clone(),
                ),
                digest: digest_clean.into_vec(),
                digest_type: proof::default_digest_type(),
                revision: vcs_info_to_revision_string(vcs),
                revision_type: proof::default_revision_type(),
            })
            .review(default_review_content)
            .diff_base(diff_base)
            .build()
            .map_err(|e| format_err!("{}", e))?;

        if let Some(diff_base_version) = diff_base_version.clone() {
            if let Some(base_review) = db.get_pkg_review(
                SOURCE_CRATES_IO,
                &crate_.name(),
                &diff_base_version,
                &id.id.id,
            ) {
                fresh_review.comment = base_review.comment.clone();
                *fresh_review.review_possibly_none_mut() =
                    base_review.review_possibly_none().clone();
            }
        }
        (None, fresh_review)
    };

    if let Some(advise_common) = advise_common {
        let mut advisory: proof::review::package::Advisory = advise_common.affected.into();
        advisory.severity = advise_common.severity;
        review.advisories.push(advisory);
    }
    if let Some(severity) = report_severity {
        let mut report = proof::review::package::Issue::new_with_severity(String::new(), severity);
        report.severity = severity;
        review.issues.push(report);
        review.review_possibly_none_mut().rating = Rating::Negative;
    }

    review.flags = db
        .get_pkg_flags_by_author(&id.id.id, &review.package.id.id)
        .cloned()
        .unwrap_or_default();

    review.alternatives = db.get_pkg_alternatives_by_author(&id.id.id, &review.package.id.id);

    // clear "original" reference when overwriting a review
    if previous_date.is_some() {
        review.common.original = None;
    }

    let mut review = edit::edit_proof_content_iteractively(
        &review,
        previous_date.as_ref(),
        diff_base_version.as_ref(),
        None,
        |text| {
            if show_override_suggestions && review.override_.is_empty() {
                writeln!(text, "# override:")?;
            }

            if show_override_suggestions {
                for review in db.get_package_reviews_for_package(
                    SOURCE_CRATES_IO,
                    Some(&pkg_id.name()),
                    Some(pkg_id.version()),
                ) {
                    let id = &review.common.from.id;
                    let (status, url) = url_to_status_str(&db.lookup_url(id));
                    writeln!(text, "# - id-type: crev")?; // TODO: support other ids?
                    writeln!(text, "#   id: {id}")?;
                    writeln!(text, "#   url: {url} # {status}")?;
                    writeln!(text, "#   comment: \"\"")?;
                }
            }

            Ok(())
        },
    )?;

    review.touch_date();
    let proof = review.sign_by(&id)?;

    let commit_msg = format!(
        "{add_or_overwrite} review for {crate} v{version}",
        crate = &crate_.name(),
        version = effective_crate_version,
        add_or_overwrite = if previous_date.is_some() {
            "Overwrite"
        } else {
            "Add"
        },
    );
    maybe_store(&local, &proof, &commit_msg, proof_create_opt)
}

pub fn find_reviews(crate_: &opts::CrateSelector) -> Result<Vec<proof::review::Package>> {
    let local = crev_lib::Local::auto_open()?;
    let db = local.load_db()?;
    Ok(db
        .get_package_reviews_for_package(
            SOURCE_CRATES_IO,
            crate_.name.as_deref(),
            crate_.version()?,
        )
        .cloned()
        .collect())
}

pub fn list_reviews(crate_: &opts::CrateSelector) -> Result<()> {
    for review in find_reviews(crate_)? {
        println!("---\n{review}");
    }

    Ok(())
}