| //! Details of the file formats used by cargo vet |
| |
| use crate::errors::{StoreVersionParseError, VersionParseError}; |
| use crate::resolver::{DiffRecommendation, ViolationConflict}; |
| use crate::serialization::spanned::Spanned; |
| use crate::{flock::Filesystem, serialization}; |
| use core::{cmp, fmt}; |
| use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; |
| use std::path::PathBuf; |
| use std::str::FromStr; |
| |
| use cargo_metadata::{semver, Package}; |
| use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; |
| |
| // Collections based on how we're using, so it's easier to swap them out. |
| pub type FastMap<K, V> = HashMap<K, V>; |
| pub type FastSet<T> = HashSet<T>; |
| pub type SortedMap<K, V> = BTreeMap<K, V>; |
| pub type SortedSet<T> = BTreeSet<T>; |
| |
| pub type CriteriaName = String; |
| pub type CriteriaStr<'a> = &'a str; |
| pub type ForeignCriteriaName = String; |
| pub type PackageName = String; |
| pub type PackageStr<'a> = &'a str; |
| pub type ImportName = String; |
| pub type ImportStr<'a> = &'a str; |
| pub type CratesUserId = u64; |
| |
| // newtype VersionReq so that we can implement PartialOrd on it. |
| #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] |
| pub struct VersionReq(pub semver::VersionReq); |
| impl fmt::Display for VersionReq { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| self.0.fmt(f) |
| } |
| } |
| impl FromStr for VersionReq { |
| type Err = <semver::VersionReq as FromStr>::Err; |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| semver::VersionReq::from_str(s).map(VersionReq) |
| } |
| } |
| impl core::ops::Deref for VersionReq { |
| type Target = semver::VersionReq; |
| fn deref(&self) -> &Self::Target { |
| &self.0 |
| } |
| } |
| impl cmp::PartialOrd for VersionReq { |
| fn partial_cmp(&self, other: &VersionReq) -> Option<cmp::Ordering> { |
| format!("{self}").partial_cmp(&format!("{other}")) |
| } |
| } |
| impl VersionReq { |
| pub fn parse(text: &str) -> Result<Self, <Self as FromStr>::Err> { |
| cargo_metadata::semver::VersionReq::parse(text).map(VersionReq) |
| } |
| pub fn matches(&self, version: &VetVersion) -> bool { |
| self.0.matches(&version.semver) |
| } |
| } |
| |
| #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| pub struct VetVersion { |
| pub semver: semver::Version, |
| pub git_rev: Option<String>, |
| } |
| impl VetVersion { |
| pub fn parse(s: &str) -> Result<Self, VersionParseError> { |
| if let Some((ver, rev)) = s.split_once('@') { |
| if let Some(hash) = rev.trim_start().strip_prefix("git:") { |
| if hash.len() != 40 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) { |
| Err(VersionParseError::InvalidGitHash) |
| } else { |
| Ok(VetVersion { |
| semver: ver.trim_end().parse()?, |
| git_rev: Some(hash.to_owned()), |
| }) |
| } |
| } else { |
| Err(VersionParseError::UnknownRevision) |
| } |
| } else { |
| Ok(VetVersion { |
| semver: s.parse()?, |
| git_rev: None, |
| }) |
| } |
| } |
| |
| /// Check if this VetVersion exactly matches the given semver version with |
| /// no git revision metadata. |
| pub fn equals_semver(&self, semver: &semver::Version) -> bool { |
| self.git_rev.is_none() && &self.semver == semver |
| } |
| } |
| impl fmt::Display for VetVersion { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| match &self.git_rev { |
| Some(hash) => write!(f, "{}@git:{}", self.semver, hash), |
| None => self.semver.fmt(f), |
| } |
| } |
| } |
| impl FromStr for VetVersion { |
| type Err = VersionParseError; |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| Self::parse(s) |
| } |
| } |
| impl Serialize for VetVersion { |
| fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
| where |
| S: Serializer, |
| { |
| self.to_string().serialize(serializer) |
| } |
| } |
| impl<'de> Deserialize<'de> for VetVersion { |
| fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
| where |
| D: Deserializer<'de>, |
| { |
| struct VersionVisitor; |
| |
| impl<'de> Visitor<'de> for VersionVisitor { |
| type Value = VetVersion; |
| |
| fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| f.write_str("semver version") |
| } |
| fn visit_str<E>(self, string: &str) -> Result<Self::Value, E> |
| where |
| E: de::Error, |
| { |
| VetVersion::parse(string).map_err(de::Error::custom) |
| } |
| } |
| |
| deserializer.deserialize_str(VersionVisitor) |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // Metaconfigs (found in Cargo.tomls) // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| /// A `[*.metadata.vet]` table in a Cargo.toml, configuring our behaviour |
| #[derive(serde::Deserialize)] |
| pub struct MetaConfigInstance { |
| // Reserved for future use, if not present version=1 assumed. |
| // (not sure whether this versions the format, or semantics, or... |
| // for now assuming this species global semantics of some kind. |
| pub version: Option<u64>, |
| pub store: Option<StoreInfo>, |
| } |
| #[derive(serde::Deserialize)] |
| pub struct StoreInfo { |
| pub path: Option<PathBuf>, |
| } |
| |
| // FIXME: It's *possible* for someone to have a workspace but not have a |
| // global `vet` instance for the whole workspace. In this case they *could* |
| // have individual `vet` instances for each subcrate they care about. |
| // This is... Weird, and it's unclear what that *means*... but maybe it's valid? |
| // Either way, we definitely don't support it right now! |
| |
| /// All available configuration files, overlaying each other. |
| /// Generally contains: `[Default, Workspace, Package]` |
| pub struct MetaConfig(pub Vec<MetaConfigInstance>); |
| |
| impl MetaConfig { |
| pub fn store_path(&self) -> Filesystem { |
| // Last config gets priority to set this |
| for config in self.0.iter().rev() { |
| if let Some(store) = &config.store { |
| if let Some(path) = &store.path { |
| return Filesystem::new(path.into()); |
| } |
| } |
| } |
| unreachable!("Default config didn't define store.path???"); |
| } |
| pub fn version(&self) -> u64 { |
| // Last config gets priority to set this |
| for config in self.0.iter().rev() { |
| if let Some(ver) = config.version { |
| return ver; |
| } |
| } |
| unreachable!("Default config didn't define version???"); |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // audits.toml // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| pub type WildcardAudits = SortedMap<PackageName, Vec<WildcardEntry>>; |
| |
| pub type AuditedDependencies = SortedMap<PackageName, Vec<AuditEntry>>; |
| |
| pub type TrustedPackages = SortedMap<PackageName, Vec<TrustEntry>>; |
| |
| /// audits.toml |
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, Default)] |
| pub struct AuditsFile { |
| /// A map of criteria_name to details on that criteria. |
| #[serde(skip_serializing_if = "SortedMap::is_empty")] |
| #[serde(default)] |
| pub criteria: SortedMap<CriteriaName, CriteriaEntry>, |
| /// Wildcard audits |
| #[serde(rename = "wildcard-audits")] |
| #[serde(skip_serializing_if = "SortedMap::is_empty")] |
| #[serde(default)] |
| pub wildcard_audits: WildcardAudits, |
| /// Actual audits. |
| pub audits: AuditedDependencies, |
| /// Trusted packages |
| #[serde(skip_serializing_if = "SortedMap::is_empty")] |
| #[serde(default)] |
| pub trusted: TrustedPackages, |
| } |
| |
| /// Foreign audits.toml with unparsed entries and audits. Should have the same |
| /// structure as `AuditsFile`, but with individual audits and criteria unparsed. |
| #[derive(serde::Deserialize, Clone, Debug)] |
| pub struct ForeignAuditsFile { |
| #[serde(default)] |
| pub criteria: SortedMap<CriteriaName, toml::Value>, |
| #[serde(default)] |
| #[serde(rename = "wildcard-audits")] |
| pub wildcard_audits: SortedMap<PackageName, Vec<toml::Value>>, |
| #[serde(default)] |
| pub audits: SortedMap<PackageName, Vec<toml::Value>>, |
| #[serde(default)] |
| pub trusted: SortedMap<PackageName, Vec<toml::Value>>, |
| } |
| |
| /// Information on a Criteria |
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] |
| #[serde(deny_unknown_fields)] |
| pub struct CriteriaEntry { |
| /// Summary of how you evaluate something by this criteria. |
| pub description: Option<String>, |
| /// An alternative to description which locates the criteria text at a publicly-accessible URL. |
| /// This can be useful for sharing criteria descriptions across multiple repositories. |
| #[serde(rename = "description-url")] |
| pub description_url: Option<String>, |
| /// Criteria that this one implies |
| #[serde(skip_serializing_if = "Vec::is_empty")] |
| #[serde(default)] |
| #[serde(with = "serialization::string_or_vec")] |
| pub implies: Vec<Spanned<CriteriaName>>, |
| /// Chain of sources this criteria was aggregated from, most recent last. |
| #[serde(rename = "aggregated-from")] |
| #[serde(skip_serializing_if = "Vec::is_empty")] |
| #[serde(default)] |
| #[serde(with = "serialization::string_or_vec")] |
| pub aggregated_from: Vec<Spanned<String>>, |
| } |
| |
| /// This is conceptually an enum |
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] |
| #[serde(try_from = "serialization::audit::AuditEntryAll")] |
| #[serde(into = "serialization::audit::AuditEntryAll")] |
| pub struct AuditEntry { |
| pub who: Vec<Spanned<String>>, |
| pub criteria: Vec<Spanned<CriteriaName>>, |
| pub kind: AuditKind, |
| pub notes: Option<String>, |
| /// Chain of sources this audit was aggregated from, most recent last. |
| pub aggregated_from: Vec<Spanned<String>>, |
| /// A non-serialized member which indicates whether this audit is a "fresh" |
| /// audit. This will be set for all audits imported found in the remote |
| /// audits file which aren't also found in the local `imports.lock` cache. |
| /// |
| /// This should almost always be `false`, and only set to `true` by the |
| /// import handling code. |
| #[serde(skip)] |
| pub is_fresh_import: bool, |
| } |
| |
| impl AuditEntry { |
| /// Should `self` be considered to be the same audit as `other`, e.g. for |
| /// the purposes of `is_fresh_import` checks? |
| pub fn same_audit_as(&self, other: &AuditEntry) -> bool { |
| // Ignore `who` and `notes` for comparison, as they are not relevant |
| // semantically and might have been updated uneventfully. |
| self.kind == other.kind && self.criteria == other.criteria |
| } |
| } |
| |
| /// Implement PartialOrd manually because the order we want for sorting is |
| /// different than the order we want for serialization. |
| impl cmp::PartialOrd for AuditEntry { |
| fn partial_cmp<'a>(&'a self, other: &'a AuditEntry) -> Option<cmp::Ordering> { |
| let tuple = |x: &'a AuditEntry| (&x.kind, &x.criteria, &x.who, &x.notes); |
| tuple(self).partial_cmp(&tuple(other)) |
| } |
| } |
| |
| impl cmp::Ord for AuditEntry { |
| fn cmp(&self, other: &AuditEntry) -> cmp::Ordering { |
| self.partial_cmp(other).unwrap() |
| } |
| } |
| |
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] |
| pub enum AuditKind { |
| Full { version: VetVersion }, |
| Delta { from: VetVersion, to: VetVersion }, |
| Violation { violation: VersionReq }, |
| } |
| |
| /// A "VERSION" or "VERSION -> VERSION" |
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| pub struct Delta { |
| pub from: Option<VetVersion>, |
| pub to: VetVersion, |
| } |
| |
| impl<'de> Deserialize<'de> for Delta { |
| fn deserialize<D>(deserializer: D) -> Result<Delta, D::Error> |
| where |
| D: Deserializer<'de>, |
| { |
| struct DeltaVisitor; |
| |
| impl<'de> Visitor<'de> for DeltaVisitor { |
| type Value = Delta; |
| |
| fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| f.write_str("version -> version delta") |
| } |
| |
| fn visit_str<E>(self, string: &str) -> Result<Self::Value, E> |
| where |
| E: de::Error, |
| { |
| if let Some((from, to)) = string.split_once("->") { |
| Ok(Delta { |
| from: Some(VetVersion::parse(from.trim()).map_err(de::Error::custom)?), |
| to: VetVersion::parse(to.trim()).map_err(de::Error::custom)?, |
| }) |
| } else { |
| Ok(Delta { |
| from: None, |
| to: VetVersion::parse(string.trim()).map_err(de::Error::custom)?, |
| }) |
| } |
| } |
| } |
| |
| deserializer.deserialize_str(DeltaVisitor) |
| } |
| } |
| |
| impl Serialize for Delta { |
| fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
| where |
| S: Serializer, |
| { |
| match &self.from { |
| Some(from) => format!("{} -> {}", from, self.to).serialize(serializer), |
| None => self.to.serialize(serializer), |
| } |
| } |
| } |
| |
| impl fmt::Display for Delta { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| match &self.from { |
| Some(from) => writeln!(f, "{} -> {}", from, self.to), |
| None => self.to.fmt(f), |
| } |
| } |
| } |
| |
| /// An entry specifying a wildcard audit for a specific crate based on crates.io |
| /// publication time and user-id. |
| /// |
| /// These audits will be reified in the imports.lock file when unlocked. |
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct WildcardEntry { |
| #[serde(default)] |
| #[serde(skip_serializing_if = "Vec::is_empty")] |
| #[serde(with = "serialization::string_or_vec")] |
| pub who: Vec<Spanned<String>>, |
| #[serde(with = "serialization::string_or_vec")] |
| pub criteria: Vec<Spanned<CriteriaName>>, |
| #[serde(rename = "user-id")] |
| pub user_id: CratesUserId, |
| pub start: Spanned<chrono::NaiveDate>, |
| pub end: Spanned<chrono::NaiveDate>, |
| pub renew: Option<bool>, |
| pub notes: Option<String>, |
| #[serde(rename = "aggregated-from")] |
| #[serde(skip_serializing_if = "Vec::is_empty")] |
| #[serde(with = "serialization::string_or_vec")] |
| #[serde(default)] |
| pub aggregated_from: Vec<Spanned<String>>, |
| /// See `AuditEntry::is_fresh_import`. |
| #[serde(skip)] |
| pub is_fresh_import: bool, |
| } |
| |
| impl WildcardEntry { |
| /// Should `self` be considered to be the same audit as `other`, e.g. for |
| /// the purposes of `is_fresh_import` checks? |
| pub fn same_audit_as(&self, other: &WildcardEntry) -> bool { |
| // Ignore `who` and `notes` for comparison, as they are not relevant |
| // semantically and might have been updated uneventfully. |
| self.user_id == other.user_id |
| && self.start == other.start |
| && self.end == other.end |
| && self.criteria == other.criteria |
| } |
| |
| /// Whether a renewal should be suggested for the entry. |
| /// |
| /// If the entry expires before `date` (and `renew` isn't `false`) a renewal will be |
| /// suggested. |
| pub fn should_renew(&self, date: chrono::NaiveDate) -> bool { |
| self.renew.unwrap_or(true) && self.end < date |
| } |
| } |
| |
| /// An entry specifying a trusted publisher for a specific crate based on |
| /// crates.io publication time and user-id. |
| /// |
| /// Trusted crates will be reified in the imports.lock file when unlocked. |
| #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct TrustEntry { |
| #[serde(with = "serialization::string_or_vec")] |
| pub criteria: Vec<Spanned<CriteriaName>>, |
| #[serde(rename = "user-id")] |
| pub user_id: CratesUserId, |
| pub start: Spanned<chrono::NaiveDate>, |
| pub end: Spanned<chrono::NaiveDate>, |
| pub notes: Option<String>, |
| #[serde(rename = "aggregated-from")] |
| #[serde(skip_serializing_if = "Vec::is_empty")] |
| #[serde(with = "serialization::string_or_vec")] |
| #[serde(default)] |
| pub aggregated_from: Vec<Spanned<String>>, |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // config.toml // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| /// config.toml |
| #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] |
| pub struct ConfigFile { |
| #[serde(rename = "cargo-vet")] |
| #[serde(default = "CargoVetConfig::missing")] |
| pub cargo_vet: CargoVetConfig, |
| |
| /// This top-level key specifies the default criteria that cargo vet certify will use |
| /// when recording audits. If unspecified, this defaults to "safe-to-deploy". |
| #[serde(rename = "default-criteria")] |
| #[serde(default = "get_default_criteria")] |
| #[serde(skip_serializing_if = "is_default_criteria")] |
| pub default_criteria: CriteriaName, |
| |
| /// Remote audits.toml's that we trust and want to import. |
| #[serde(skip_serializing_if = "SortedMap::is_empty")] |
| #[serde(default)] |
| pub imports: SortedMap<ImportName, RemoteImport>, |
| |
| /// A table of policies for crates. |
| #[serde(skip_serializing_if = "Policy::is_empty")] |
| #[serde(default)] |
| pub policy: Policy, |
| |
| /// All of the "foreign" dependencies that we rely on but haven't audited yet. |
| /// Foreign dependencies are just "things on crates.io", everything else |
| /// (paths, git, etc) is assumed to be "under your control" and therefore implicitly trusted. |
| #[serde(skip_serializing_if = "SortedMap::is_empty")] |
| #[serde(default)] |
| #[serde(alias = "unaudited")] |
| pub exemptions: SortedMap<PackageName, Vec<ExemptedDependency>>, |
| } |
| |
| pub static SAFE_TO_DEPLOY: CriteriaStr = "safe-to-deploy"; |
| pub static SAFE_TO_RUN: CriteriaStr = "safe-to-run"; |
| pub static DEFAULT_CRITERIA: CriteriaStr = SAFE_TO_DEPLOY; |
| |
| pub fn get_default_criteria() -> CriteriaName { |
| CriteriaName::from(DEFAULT_CRITERIA) |
| } |
| fn is_default_criteria(val: &CriteriaName) -> bool { |
| val == DEFAULT_CRITERIA |
| } |
| |
| /// The table of crate policies. |
| #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] |
| #[serde(try_from = "serialization::policy::AllPolicies")] |
| #[serde(into = "serialization::policy::AllPolicies")] |
| pub struct Policy { |
| pub package: SortedMap<PackageName, PackagePolicyEntry>, |
| } |
| |
| impl Policy { |
| /// Get the policy entry for the given crate, if any. |
| pub fn get(&self, name: PackageStr, version: &VetVersion) -> Option<&PolicyEntry> { |
| self.package |
| .get(name) |
| .and_then(|pkg_policy| match pkg_policy { |
| PackagePolicyEntry::Unversioned(e) => Some(e), |
| PackagePolicyEntry::Versioned { version: v } => v.get(version), |
| }) |
| } |
| |
| /// Get the mutable policy entry for the given crate, if any. |
| pub fn get_mut( |
| &mut self, |
| name: PackageStr, |
| version: Option<&VetVersion>, |
| ) -> Option<&mut PolicyEntry> { |
| self.package |
| .get_mut(name) |
| .and_then(|pkg_policy| match pkg_policy { |
| PackagePolicyEntry::Unversioned(e) => Some(e), |
| PackagePolicyEntry::Versioned { version: v } => { |
| version.and_then(|version| v.get_mut(version)) |
| } |
| }) |
| } |
| |
| /// Get the mutable policy entry for the given crate, creating a default if none exists. |
| /// |
| /// Unlike `get_mut`, this guarantees that the policy is represented as versioned or |
| /// unversioned based on the whether the `version` is provided. If the `version` passed is |
| /// incompatible with the current policy, None is returned. |
| /// |
| /// `all_versions` is required to maintain proper structure of the policy map if the entry is |
| /// missing: if one policy version is provided, they all must be. |
| pub fn get_mut_or_default<F: FnOnce() -> Vec<VetVersion>>( |
| &mut self, |
| name: PackageName, |
| version: Option<&VetVersion>, |
| all_versions: F, |
| ) -> Option<&mut PolicyEntry> { |
| let pkg_policy = self.package.entry(name).or_insert_with(|| { |
| if version.is_none() { |
| PackagePolicyEntry::Unversioned(Default::default()) |
| } else { |
| PackagePolicyEntry::Versioned { |
| version: all_versions() |
| .into_iter() |
| .map(|v| (v, Default::default())) |
| .collect(), |
| } |
| } |
| }); |
| |
| match (pkg_policy, version) { |
| (PackagePolicyEntry::Unversioned(e), None) => Some(e), |
| (PackagePolicyEntry::Versioned { version }, Some(v)) => version.get_mut(v), |
| _ => None, |
| } |
| } |
| |
| /// Insert a new package policy entry. |
| pub fn insert( |
| &mut self, |
| name: PackageName, |
| entry: PackagePolicyEntry, |
| ) -> Option<PackagePolicyEntry> { |
| self.package.insert(name, entry) |
| } |
| |
| /// Return whether there are no policies defined. |
| pub fn is_empty(&self) -> bool { |
| self.package.is_empty() |
| } |
| |
| /// Return an iterator over defined policies. |
| pub fn iter(&self) -> PolicyIter { |
| PolicyIter { |
| iter: self.package.iter(), |
| versioned: None, |
| } |
| } |
| } |
| |
| pub struct PolicyIter<'a> { |
| iter: <&'a SortedMap<PackageName, PackagePolicyEntry> as IntoIterator>::IntoIter, |
| versioned: Option<( |
| &'a PackageName, |
| <&'a SortedMap<VetVersion, PolicyEntry> as IntoIterator>::IntoIter, |
| )>, |
| } |
| |
| impl<'a> Iterator for PolicyIter<'a> { |
| type Item = (&'a PackageName, Option<&'a VetVersion>, &'a PolicyEntry); |
| |
| fn next(&mut self) -> Option<Self::Item> { |
| match &mut self.versioned { |
| Some((name, versioned)) => match versioned.next() { |
| Some((v, p)) => Some((name, Some(v), p)), |
| None => { |
| self.versioned = None; |
| self.next() |
| } |
| }, |
| None => { |
| let (name, ppe) = self.iter.next()?; |
| match ppe { |
| PackagePolicyEntry::Versioned { version } => { |
| self.versioned = Some((name, version.iter())); |
| self.next() |
| } |
| PackagePolicyEntry::Unversioned(p) => Some((name, None, p)), |
| } |
| } |
| } |
| } |
| } |
| |
| impl<'a> IntoIterator for &'a Policy { |
| type IntoIter = PolicyIter<'a>; |
| type Item = <PolicyIter<'a> as Iterator>::Item; |
| |
| fn into_iter(self) -> Self::IntoIter { |
| self.iter() |
| } |
| } |
| |
| /// Policies for a particular package (crate). |
| /// |
| /// If the crate exists as a third-party crate anywhere in the dependency tree, crate versions for |
| /// _all_ and _only_ the versions present in the dependency tree must be provided to set policies. |
| /// Otherwise, versions may be omitted. |
| #[derive(Debug, Clone)] |
| // We have to use a slightly different serialization than than `serde(untagged)`, because toml only |
| // parses `Spanned` elements (as contained in `PolicyEntry`) through their own Deseralizer, and |
| // `serde(untagged)` deserializes everything into a buffer first to try different deserialization |
| // branches (which will use an internal `serde` Deserializer rather than the `toml` Deserializer). |
| pub enum PackagePolicyEntry { |
| Versioned { |
| version: SortedMap<VetVersion, PolicyEntry>, |
| }, |
| Unversioned(PolicyEntry), |
| } |
| |
| /// Policies that crates must pass. |
| /// |
| /// Policy settings here are basically the equivalent of audits.toml, which is separated out |
| /// because it's not supposed to be shared (or, doesn't really make sense to share, since |
| /// first-party crates are defined by "not on crates.io"). |
| /// |
| /// Because first-party crates are implicitly trusted, the only purpose of this table is to define |
| /// the boundary between first-party and third-party ones. More specifically, the criteria of the |
| /// dependency edges between a first-party crate and its direct third-party dependencies. |
| /// |
| /// If this sounds overwhelming, don't worry, everything defaults to "nothing special" |
| /// and an empty PolicyTable basically just means "everything should satisfy the |
| /// default criteria in audits.toml". |
| #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] |
| pub struct PolicyEntry { |
| /// Whether this nominally-first-party crate should actually be subject to audits |
| /// as-if it was third-party, based on matches to crates.io packages with the same |
| /// name and version. This field is optional for any package that *doesn't* have |
| /// such a match, and mandatory for all others (None == Some(false)). |
| /// |
| /// If true, this package will be handled like a third-party package and require |
| /// audits. If the package is not in the crates.io registry, it will be an error |
| /// and you should either make sure the current version is published or flip |
| /// this back to false. |
| /// |
| /// Setting this value to true is intended for actual externally developed projects |
| /// that you are importing into your project in a weird way with minimal modifications. |
| /// For instance, if you manually vendor the package in, or maintain a small patchset |
| /// on top of the currently published version. |
| /// |
| /// It should not be used for packages that are directly developed in this project |
| /// (a project shouldn't publish audits for its own code) or for non-trivial forks. |
| /// |
| /// Audits you *do* perform should be for the actual version published to crates.io, |
| /// which are the versions `cargo vet diff` and `cargo vet inspect` will fetch. |
| #[serde(rename = "audit-as-crates-io")] |
| pub audit_as_crates_io: Option<bool>, |
| |
| /// Default criteria that must be satisfied by all *direct* third-party (foreign) dependencies |
| /// of the crate. If satisfied, the crate is set to satisfying all criteria. |
| /// |
| /// If not present, this defaults to the default criteria in the audits table. |
| #[serde(default)] |
| #[serde(with = "serialization::string_or_vec_or_none")] |
| pub criteria: Option<Vec<Spanned<CriteriaName>>>, |
| |
| /// Same as `criteria`, but for crates that are only used as dev-dependencies. |
| #[serde(rename = "dev-criteria")] |
| #[serde(default)] |
| #[serde(with = "serialization::string_or_vec_or_none")] |
| pub dev_criteria: Option<Vec<Spanned<CriteriaName>>>, |
| |
| /// Custom criteria for a specific crate's dependencies. |
| /// |
| /// Any dependency edge that isn't explicitly specified defaults to `criteria`. |
| #[serde(rename = "dependency-criteria")] |
| #[serde(skip_serializing_if = "CriteriaMap::is_empty")] |
| #[serde(with = "serialization::criteria_map")] |
| #[serde(default)] |
| pub dependency_criteria: CriteriaMap, |
| |
| /// Freeform notes |
| pub notes: Option<String>, |
| } |
| |
| /// Helper type for managing a mapping from a string to a set of criteria. This |
| /// is used for dependency-criteria to specify the criteria that transitive |
| /// dependencies must satisfy, as well as for criteria-maps when specifying the |
| /// criteria implied by foreign criteria. |
| /// |
| /// Example: |
| /// |
| /// ```toml |
| /// dependency_criteria = { hmac = ['secure', 'crypto_reviewed'] } |
| /// ``` |
| /// |
| /// ```toml |
| /// criteria-map = { fuzzed = 'safe-to-deploy' } |
| /// ``` |
| pub type CriteriaMap = SortedMap<Spanned<String>, Vec<Spanned<CriteriaName>>>; |
| |
| pub static DEFAULT_POLICY_CRITERIA: CriteriaStr = SAFE_TO_DEPLOY; |
| pub static DEFAULT_POLICY_DEV_CRITERIA: CriteriaStr = SAFE_TO_RUN; |
| |
| /// A remote audits.toml that we trust the contents of (by virtue of trusting the maintainer). |
| #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] |
| pub struct RemoteImport { |
| /// URL(s) of the foreign audits.toml |
| #[serde(with = "serialization::string_or_vec")] |
| pub url: Vec<String>, |
| /// A list of crates for which no audits or violations should be imported. |
| #[serde(skip_serializing_if = "Vec::is_empty")] |
| #[serde(default)] |
| pub exclude: Vec<PackageName>, |
| /// A list of criteria that are implied by foreign criteria |
| #[serde(rename = "criteria-map")] |
| #[serde(skip_serializing_if = "CriteriaMap::is_empty")] |
| #[serde(with = "serialization::criteria_map")] |
| #[serde(default)] |
| pub criteria_map: CriteriaMap, |
| } |
| |
| /// Translations of foreign criteria to local criteria. |
| #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] |
| pub struct CriteriaMapping { |
| /// This local criteria is implied... |
| pub ours: CriteriaName, |
| /// If this foreign criteria applies |
| pub theirs: Spanned<ForeignCriteriaName>, |
| } |
| |
| /// Semantically identical to a 'full audit' entry, but private to our project |
| /// and tracked as less-good than a proper audit, so that you try to get rid of it. |
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct ExemptedDependency { |
| /// The version of the crate that we are currently "fine" with leaving unaudited. |
| pub version: VetVersion, |
| /// Criteria that we're willing to handwave for this version (assuming our dependencies |
| /// satisfy this criteria). This isn't defaulted, 'vet init' and similar commands will |
| /// pick a "good" initial value. |
| #[serde(default)] |
| #[serde(with = "serialization::string_or_vec")] |
| pub criteria: Vec<Spanned<CriteriaName>>, |
| /// Whether 'suggest' should bother mentioning this (defaults true). |
| #[serde(default = "get_default_exemptions_suggest")] |
| #[serde(skip_serializing_if = "is_default_exemptions_suggest")] |
| pub suggest: bool, |
| /// Freeform notes, put whatever you want here. Just more stable/reliable than comments. |
| pub notes: Option<String>, |
| } |
| |
| static DEFAULT_EXEMPTIONS_SUGGEST: bool = true; |
| pub fn get_default_exemptions_suggest() -> bool { |
| DEFAULT_EXEMPTIONS_SUGGEST |
| } |
| fn is_default_exemptions_suggest(val: &bool) -> bool { |
| val == &DEFAULT_EXEMPTIONS_SUGGEST |
| } |
| |
| /// Special version type used for store versions. Only contains two components |
| /// (major/minor) to avoid patch version changes from causing changes to the |
| /// store. |
| #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct StoreVersion { |
| pub major: u64, |
| pub minor: u64, |
| } |
| |
| impl StoreVersion { |
| #[cfg(not(test))] |
| pub fn current() -> Self { |
| StoreVersion { |
| major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(), |
| minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(), |
| } |
| } |
| |
| // To keep output from tests stable, when running unit tests we always |
| // pretend we're version 1.0 |
| #[cfg(test)] |
| pub fn current() -> Self { |
| StoreVersion { major: 1, minor: 0 } |
| } |
| } |
| |
| impl FromStr for StoreVersion { |
| type Err = StoreVersionParseError; |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| match s.split_once('.') { |
| Some((major, minor)) => Ok(StoreVersion { |
| major: major.parse()?, |
| minor: minor.parse()?, |
| }), |
| None => Err(StoreVersionParseError::MissingSeparator), |
| } |
| } |
| } |
| |
| impl fmt::Display for StoreVersion { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| write!(f, "{}.{}", self.major, self.minor) |
| } |
| } |
| |
| impl Serialize for StoreVersion { |
| fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
| where |
| S: Serializer, |
| { |
| self.to_string().serialize(serializer) |
| } |
| } |
| |
| impl<'de> Deserialize<'de> for StoreVersion { |
| fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
| where |
| D: Deserializer<'de>, |
| { |
| struct VersionVisitor; |
| |
| impl<'de> Visitor<'de> for VersionVisitor { |
| type Value = StoreVersion; |
| |
| fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| f.write_str("store version") |
| } |
| fn visit_str<E>(self, string: &str) -> Result<Self::Value, E> |
| where |
| E: de::Error, |
| { |
| StoreVersion::from_str(string).map_err(de::Error::custom) |
| } |
| } |
| |
| deserializer.deserialize_str(VersionVisitor) |
| } |
| } |
| |
| /// Cargo vet config metadata field for the store's config file. |
| #[derive(Debug, Serialize, Deserialize, Clone)] |
| pub struct CargoVetConfig { |
| pub version: StoreVersion, |
| } |
| |
| impl CargoVetConfig { |
| /// Pretend that any store which was created without a version specified is |
| /// from version 0.4. |
| fn missing() -> Self { |
| Self { |
| version: StoreVersion { major: 0, minor: 4 }, |
| } |
| } |
| } |
| |
| impl Default for CargoVetConfig { |
| fn default() -> Self { |
| Self { |
| version: StoreVersion::current(), |
| } |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // imports.lock // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)] |
| pub struct ImportsFile { |
| #[serde(default)] |
| #[serde(skip_serializing_if = "SortedMap::is_empty")] |
| pub unpublished: SortedMap<PackageName, Vec<UnpublishedEntry>>, |
| #[serde(default)] |
| #[serde(skip_serializing_if = "SortedMap::is_empty")] |
| pub publisher: SortedMap<PackageName, Vec<CratesPublisher>>, |
| #[serde(default)] |
| #[serde(skip_serializing_if = "SortedMap::is_empty")] |
| pub audits: SortedMap<ImportName, AuditsFile>, |
| } |
| |
| /// Information about who published a specific version of a crate to be cached |
| /// in imports.lock. |
| #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct CratesPublisher { |
| // NOTE: This will only ever be a `semver::Version`, however the resolver |
| // code works on borrowed `VetVersion` instances, so we use one here so it |
| // is easier to use within the resolver. |
| pub version: VetVersion, |
| pub when: chrono::NaiveDate, |
| #[serde(rename = "user-id")] |
| pub user_id: CratesUserId, |
| #[serde(rename = "user-login")] |
| pub user_login: String, |
| #[serde(rename = "user-name")] |
| pub user_name: Option<String>, |
| /// See `AuditEntry::is_fresh_import`. |
| #[serde(skip)] |
| pub is_fresh_import: bool, |
| } |
| |
| // Information about a specific crate being unpublished |
| #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct UnpublishedEntry { |
| // NOTE: This will only ever be a `semver::Version`, however the resolver |
| // code works on borrowed `VetVersion` instances, so we use one here so it |
| // is easier to use within the resolver. |
| pub version: VetVersion, |
| pub audited_as: VetVersion, |
| /// Set to `true` if `version` was not published when acquiring the Store. |
| /// Always set to `false` when locked. |
| #[serde(skip)] |
| pub still_unpublished: bool, |
| /// See `AuditEntry::is_fresh_import`. |
| #[serde(skip)] |
| pub is_fresh_import: bool, |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // diffcache.toml // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| /// The current DiffCache file format in a tagged enum. |
| /// |
| /// If we fail to read the DiffCache it will be silently re-built, meaning that |
| /// the version enum tag can be changed to force the DiffCache to be |
| /// re-generated after a breaking change to the format, such as a change to how |
| /// diffs are computed or identified. |
| #[derive(Serialize, Deserialize, Clone)] |
| #[serde(tag = "version")] |
| pub enum DiffCache { |
| #[serde(rename = "2")] |
| V2 { |
| diffs: SortedMap<PackageName, SortedMap<Delta, DiffStat>>, |
| }, |
| } |
| |
| impl Default for DiffCache { |
| fn default() -> Self { |
| DiffCache::V2 { |
| diffs: SortedMap::new(), |
| } |
| } |
| } |
| |
| #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] |
| pub struct DiffStat { |
| pub insertions: u64, |
| pub deletions: u64, |
| pub files_changed: u64, |
| } |
| |
| impl DiffStat { |
| pub fn count(&self) -> u64 { |
| self.insertions + self.deletions |
| } |
| } |
| |
| impl fmt::Display for DiffStat { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| write!(f, "{} files changed", self.files_changed)?; |
| if self.insertions > 0 { |
| write!(f, ", {} insertions(+)", self.insertions)?; |
| } |
| if self.deletions > 0 { |
| write!(f, ", {} deletions(-)", self.deletions)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // command-history.json // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub enum FetchCommand { |
| Inspect { |
| package: PackageName, |
| version: VetVersion, |
| }, |
| Diff { |
| package: PackageName, |
| version1: VetVersion, |
| version2: VetVersion, |
| }, |
| } |
| |
| impl FetchCommand { |
| pub fn package(&self) -> PackageStr { |
| match self { |
| FetchCommand::Inspect { package, .. } => package, |
| FetchCommand::Diff { package, .. } => package, |
| } |
| } |
| } |
| |
| #[derive(Serialize, Deserialize, Debug, Clone, Default)] |
| pub struct CommandHistory { |
| #[serde(flatten)] |
| pub last_fetch: Option<FetchCommand>, |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // crates-io-cache.json // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct CratesCacheUser { |
| pub login: String, |
| pub name: Option<String>, |
| } |
| impl fmt::Display for CratesCacheUser { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| if let Some(name) = &self.name { |
| write!(f, "{} ({})", name, &self.login) |
| } else { |
| write!(f, "{}", &self.login) |
| } |
| } |
| } |
| |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct CratesCacheVersionDetails { |
| pub created_at: chrono::DateTime<chrono::Utc>, |
| pub published_by: Option<CratesUserId>, |
| } |
| |
| #[derive(Serialize, Deserialize, Default, Debug, Clone)] |
| pub struct CratesCacheEntry { |
| pub last_fetched: chrono::DateTime<chrono::Utc>, |
| /// If versions is empty, this indicates that we queried the info and found that the crate has |
| /// no published versions (and thus doesn't exist as of `last_fetched`). |
| /// |
| /// Versions are a sorted map because we sometimes need to iterate in order. We don't use a sorted |
| /// Vec because we may partially update the versions when we access the index (though technically |
| /// that update _should_ only have new versions which would append to a Vec if it were that). |
| pub versions: SortedMap<semver::Version, Option<CratesCacheVersionDetails>>, |
| pub metadata: Option<CratesAPICrateMetadata>, |
| } |
| |
| #[derive(Serialize, Deserialize, Clone, Default)] |
| pub struct CratesCache { |
| pub users: SortedMap<CratesUserId, CratesCacheUser>, |
| pub crates: SortedMap<PackageName, CratesCacheEntry>, |
| } |
| |
| impl CratesCacheEntry { |
| /// Return whether the crate exists. |
| /// |
| /// The cache stores non-existent results when fetching. |
| pub fn exists(&self) -> bool { |
| !self.versions.is_empty() |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // crates.io API // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| // NOTE: This is a subset of the format returned from the crates.io v1 API. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct CratesAPIUser { |
| pub id: CratesUserId, |
| pub login: String, |
| pub name: Option<String>, |
| } |
| |
| // NOTE: This is a subset of the format returned from the crates.io v1 API. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct CratesAPIVersion { |
| pub created_at: chrono::DateTime<chrono::Utc>, |
| pub num: semver::Version, |
| pub published_by: Option<CratesAPIUser>, |
| } |
| |
| // NOTE: This is a subset of the format returned from the crates.io v1 API. |
| #[derive(Serialize, Deserialize, Debug, Default, Clone)] |
| pub struct CratesAPICrateMetadata { |
| pub description: Option<String>, |
| pub repository: Option<String>, |
| } |
| |
| // NOTE: This is a subset of the format returned from the crates.io v1 API. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct CratesAPICrate { |
| #[serde(rename = "crate")] |
| pub crate_data: CratesAPICrateMetadata, |
| pub versions: Vec<CratesAPIVersion>, |
| } |
| |
| impl CratesAPICrateMetadata { |
| /// Whether this metadata is similar enough to that of the given package to be considered the |
| /// same. |
| pub fn consider_as_same(&self, p: &Package) -> bool { |
| (self.description.is_some() && p.description == self.description) |
| || (self.repository.is_some() && p.repository == self.repository) |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // registry.toml // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| #[derive(Serialize, Deserialize, Debug, Clone, Default)] |
| pub struct RegistryFile { |
| pub registry: SortedMap<ImportName, RegistryEntry>, |
| } |
| |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct RegistryEntry { |
| #[serde(with = "serialization::string_or_vec")] |
| pub url: Vec<String>, |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////// |
| // // |
| // // |
| // // |
| // <json report output> // |
| // // |
| // // |
| // // |
| //////////////////////////////////////////////////////////////////////////////////// |
| |
| /// cargo-vet's `--output-format=json` for `check` and `suggest` on: |
| /// |
| /// * success |
| /// * audit failure |
| /// * violation conflicts |
| /// |
| /// Other errors like i/o or supply-chain integrity issues will show |
| /// up as miette-style json errors. |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct JsonReport { |
| #[serde(flatten)] |
| pub conclusion: JsonReportConclusion, |
| } |
| |
| /// The conclusion of running `check` or `suggest` |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| #[serde(tag = "conclusion")] |
| pub enum JsonReportConclusion { |
| /// Success! Everything's Good. |
| #[serde(rename = "success")] |
| Success(JsonReportSuccess), |
| /// The violations and audits/exemptions are contradictory! |
| #[serde(rename = "fail (violation)")] |
| FailForViolationConflict(JsonReportFailForViolationConflict), |
| /// The audit failed, here's why and what to do. |
| #[serde(rename = "fail (vetting)")] |
| FailForVet(JsonReportFailForVet), |
| } |
| |
| /// Success! Everything is audited! |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct JsonReportSuccess { |
| /// These packages are fully vetted |
| pub vetted_fully: Vec<JsonPackage>, |
| /// These packages are partially vetted (some audits but relies on an `exemption`). |
| pub vetted_partially: Vec<JsonPackage>, |
| /// These packages are exempted |
| pub vetted_with_exemptions: Vec<JsonPackage>, |
| } |
| |
| /// Failure! The violations and audits/exemptions are contradictory! |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct JsonReportFailForViolationConflict { |
| /// These packages have the following conflicts |
| // FIXME(SCHEMA): we probably shouldn't expose this internal type |
| pub violations: SortedMap<PackageAndVersion, Vec<ViolationConflict>>, |
| } |
| |
| /// Failure! You need more audits! |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct JsonReportFailForVet { |
| /// Here are the problems we found |
| pub failures: Vec<JsonVetFailure>, |
| /// And here are the fixes we recommend |
| pub suggest: Option<JsonSuggest>, |
| } |
| |
| /// Suggested fixes for a FailForVet |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct JsonSuggest { |
| /// Here are the suggestions sorted in the order of priority |
| pub suggestions: Vec<JsonSuggestItem>, |
| /// The same set of suggestions but grouped by the criteria (lists) needed to audit them |
| // FIXME(SCHEMA): this is kinda redundant? do consumers want this? |
| pub suggest_by_criteria: SortedMap<String, Vec<JsonSuggestItem>>, |
| /// The total number of lines you would need to review to resolve this |
| pub total_lines: u64, |
| } |
| |
| /// This specific package needed the following criteria but doesn't have them! |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct JsonVetFailure { |
| /// The name of the package |
| pub name: PackageName, |
| /// The version of the package |
| pub version: VetVersion, |
| /// The missing criteria |
| pub missing_criteria: Vec<CriteriaName>, |
| } |
| |
| /// We recommend auditing the following package |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct JsonSuggestItem { |
| /// The name of the package |
| pub name: PackageName, |
| /// Any notable parents the package has (can be helpful in giving context to the user) |
| // FIXME(SCHEMA): we probably shouldn't expose this as a String |
| pub notable_parents: String, |
| /// The criteria we recommend auditing the package for |
| pub suggested_criteria: Vec<CriteriaName>, |
| /// The diff (or full version) we recommend auditing |
| // FIXME(SCHEMA): we probably shouldn't expose this internal type |
| pub suggested_diff: DiffRecommendation, |
| } |
| |
| /// A string of the form "package:version" |
| pub type PackageAndVersion = String; |
| |
| /// A Package |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct JsonPackage { |
| /// Name of the package |
| pub name: PackageName, |
| /// Version of the package |
| pub version: VetVersion, |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| #[test] |
| fn vet_version_parsing() { |
| assert_eq!( |
| VetVersion::parse("1.0.0").unwrap(), |
| VetVersion { |
| semver: "1.0.0".parse().unwrap(), |
| git_rev: None |
| } |
| ); |
| |
| assert_eq!( |
| VetVersion::parse("1.0.1@git:00112233445566778899aabbccddeeff00112233").unwrap(), |
| VetVersion { |
| semver: "1.0.1".parse().unwrap(), |
| git_rev: Some("00112233445566778899aabbccddeeff00112233".into()) |
| } |
| ); |
| |
| match VetVersion::parse("1.0.1@git:00112233445566778899aabbccddeeff0011223g") { |
| Err(VersionParseError::InvalidGitHash) => (), |
| _ => panic!("expected invalid git hash"), |
| } |
| |
| match VetVersion::parse("1.0.1@git:00112233") { |
| Err(VersionParseError::InvalidGitHash) => (), |
| _ => panic!("expected invalid git hash"), |
| } |
| |
| match VetVersion::parse("1.0.1@pijul:00112233") { |
| Err(VersionParseError::UnknownRevision) => (), |
| _ => panic!("expected unknown revision"), |
| } |
| } |
| } |