use crate::fingerprint::sha256_v1_bytes;
pub const DIAGNOSTIC_KERNEL_SCHEMA: &str = "cargo-allow.diagnostic-kernel.v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DiagnosticSeverity {
Info,
Low,
Medium,
High,
Critical,
}
impl DiagnosticSeverity {
pub fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Critical => "critical",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RulePosture {
Informational,
Advisory,
Shadow,
Blocking,
}
impl RulePosture {
pub fn as_str(self) -> &'static str {
match self {
Self::Informational => "informational",
Self::Advisory => "advisory",
Self::Shadow => "shadow",
Self::Blocking => "blocking",
}
}
pub fn is_blocking(self) -> bool {
matches!(self, Self::Blocking)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticConfidence {
Exact,
High,
Bounded,
Uncertain,
Unavailable,
}
impl DiagnosticConfidence {
pub fn as_str(self) -> &'static str {
match self {
Self::Exact => "exact",
Self::High => "high",
Self::Bounded => "bounded",
Self::Uncertain => "uncertain",
Self::Unavailable => "unavailable",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticResultClass {
Finding,
Stale,
NotProven,
Unsupported,
InstrumentFailure,
}
impl DiagnosticResultClass {
pub fn as_str(self) -> &'static str {
match self {
Self::Finding => "finding",
Self::Stale => "stale",
Self::NotProven => "not_proven",
Self::Unsupported => "unsupported",
Self::InstrumentFailure => "instrument_failure",
}
}
pub fn is_repository_condition(self) -> bool {
matches!(self, Self::Finding | Self::Stale | Self::NotProven)
}
pub fn result_class_is_tool_side(self) -> bool {
!self.is_repository_condition()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SourceEncoding {
Utf8,
Utf16,
}
impl SourceEncoding {
pub fn as_str(self) -> &'static str {
match self {
Self::Utf8 => "utf8",
Self::Utf16 => "utf16",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PositionBase {
Zero,
One,
}
impl PositionBase {
pub fn as_str(self) -> &'static str {
match self {
Self::Zero => "zero_based",
Self::One => "one_based",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SourceProvenance {
Authored,
Generated,
}
impl SourceProvenance {
pub fn as_str(self) -> &'static str {
match self {
Self::Authored => "authored",
Self::Generated => "generated",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SourcePosition {
pub line: u32,
pub column: Option<u32>,
}
impl SourcePosition {
pub fn line_only(line: u32) -> Self {
Self { line, column: None }
}
pub fn precise(line: u32, column: u32) -> Self {
Self {
line,
column: Some(column),
}
}
pub fn is_precise(&self) -> bool {
self.column.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SourceRange {
pub path: String,
pub start: Option<SourcePosition>,
pub end: Option<SourcePosition>,
pub encoding: SourceEncoding,
pub base: PositionBase,
pub provenance: SourceProvenance,
pub content_identity: Option<String>,
}
impl SourceRange {
pub fn file(path: impl Into<String>) -> Self {
Self {
path: path.into(),
start: None,
end: None,
encoding: SourceEncoding::Utf8,
base: PositionBase::One,
provenance: SourceProvenance::Authored,
content_identity: None,
}
}
pub fn with_span(mut self, start: SourcePosition, end: SourcePosition) -> Self {
self.start = Some(start);
self.end = Some(end);
self
}
pub fn with_provenance(mut self, provenance: SourceProvenance) -> Self {
self.provenance = provenance;
self
}
pub fn with_content_identity(mut self, identity: impl Into<String>) -> Self {
self.content_identity = Some(identity.into());
self
}
pub fn is_precise(&self) -> bool {
self.start.is_some_and(|start| start.is_precise())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RelatedRole {
Requirement,
ImplementationSeam,
TestSubject,
Receipt,
Definition,
Reference,
}
impl RelatedRole {
pub fn as_str(self) -> &'static str {
match self {
Self::Requirement => "requirement",
Self::ImplementationSeam => "implementation_seam",
Self::TestSubject => "test_subject",
Self::Receipt => "receipt",
Self::Definition => "definition",
Self::Reference => "reference",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RelatedLocation {
pub role: RelatedRole,
pub range: SourceRange,
pub note: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MissingObligation {
NormativeRequirementMissing,
ImplementationSliceMissingOrStale,
ImplementationSeamOwnerMissing,
EvidencePurposeMissing,
EvidenceSubjectMissingOrAmbiguous,
NegativeDiscriminatorMissing,
ProofCommandMissingOrIncompatible,
ExternalReceiptMissingOrStale,
ReceiptSubjectsMissing,
SpecCodeTestAtomicityBroken,
AuthorityMissingOrContradictory,
GeneratedArtifactStale,
PackOrAdapterDrift,
UnsupportedCapability,
RepositoryDecisionRequired,
}
impl MissingObligation {
pub fn as_str(self) -> &'static str {
match self {
Self::NormativeRequirementMissing => "normative_requirement_missing",
Self::ImplementationSliceMissingOrStale => "implementation_slice_missing_or_stale",
Self::ImplementationSeamOwnerMissing => "implementation_seam_owner_missing",
Self::EvidencePurposeMissing => "evidence_purpose_missing",
Self::EvidenceSubjectMissingOrAmbiguous => "evidence_subject_missing_or_ambiguous",
Self::NegativeDiscriminatorMissing => "negative_discriminator_missing",
Self::ProofCommandMissingOrIncompatible => "proof_command_missing_or_incompatible",
Self::ExternalReceiptMissingOrStale => "external_receipt_missing_or_stale",
Self::ReceiptSubjectsMissing => "receipt_subjects_missing",
Self::SpecCodeTestAtomicityBroken => "spec_code_test_atomicity_broken",
Self::AuthorityMissingOrContradictory => "authority_missing_or_contradictory",
Self::GeneratedArtifactStale => "generated_artifact_stale",
Self::PackOrAdapterDrift => "pack_or_adapter_drift",
Self::UnsupportedCapability => "unsupported_capability",
Self::RepositoryDecisionRequired => "repository_decision_required",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ActionKind {
AutomaticSafeEdit,
PreviewableWorkspaceEdit,
GenerateOwnedArtifact,
RunCargoAllowCommand,
OpenOrNavigate,
RefreshOrReissue,
ChooseBetweenAuthorities,
RequestRepositoryDecision,
PerformExternalAction,
DeferWithTypedReason,
SuppressOrExemptUnderPolicy,
NoSafeActionKnown,
}
impl ActionKind {
pub fn as_str(self) -> &'static str {
match self {
Self::AutomaticSafeEdit => "automatic_safe_edit",
Self::PreviewableWorkspaceEdit => "previewable_workspace_edit",
Self::GenerateOwnedArtifact => "generate_owned_artifact",
Self::RunCargoAllowCommand => "run_cargo_allow_command",
Self::OpenOrNavigate => "open_or_navigate",
Self::RefreshOrReissue => "refresh_or_reissue",
Self::ChooseBetweenAuthorities => "choose_between_authorities",
Self::RequestRepositoryDecision => "request_repository_decision",
Self::PerformExternalAction => "perform_external_action",
Self::DeferWithTypedReason => "defer_with_typed_reason",
Self::SuppressOrExemptUnderPolicy => "suppress_or_exempt_under_policy",
Self::NoSafeActionKnown => "no_safe_action_known",
}
}
pub fn mutates_source(self) -> bool {
matches!(
self,
Self::AutomaticSafeEdit
| Self::PreviewableWorkspaceEdit
| Self::GenerateOwnedArtifact
| Self::RefreshOrReissue
| Self::SuppressOrExemptUnderPolicy
)
}
pub fn may_be_automatic(self) -> bool {
matches!(
self,
Self::AutomaticSafeEdit | Self::GenerateOwnedArtifact | Self::RefreshOrReissue
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ActionApplicability {
Automatic,
Preview,
Manual,
Unavailable,
}
impl ActionApplicability {
pub fn as_str(self) -> &'static str {
match self {
Self::Automatic => "automatic",
Self::Preview => "preview",
Self::Manual => "manual",
Self::Unavailable => "unavailable",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RequiredProof {
pub command_argv: Vec<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CargoAllowActionV1 {
pub id: String,
pub kind: ActionKind,
pub applicability: ActionApplicability,
pub preconditions: Vec<String>,
pub mutation_scope: Option<String>,
pub expected_effect: String,
pub rollback: Option<String>,
pub required_proof: Option<RequiredProof>,
pub residual_claim: Vec<String>,
}
impl CargoAllowActionV1 {
pub fn navigate(id: impl Into<String>, expected_effect: impl Into<String>) -> Self {
Self {
id: id.into(),
kind: ActionKind::OpenOrNavigate,
applicability: ActionApplicability::Manual,
preconditions: Vec::new(),
mutation_scope: None,
expected_effect: expected_effect.into(),
rollback: None,
required_proof: None,
residual_claim: Vec::new(),
}
}
pub fn applicability_is_coherent(&self) -> bool {
if self.applicability == ActionApplicability::Automatic {
return self.kind.may_be_automatic();
}
true
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PartialDataBoundary {
pub complete: bool,
pub reasons: Vec<String>,
}
impl PartialDataBoundary {
pub fn complete() -> Self {
Self {
complete: true,
reasons: Vec::new(),
}
}
pub fn partial(reasons: Vec<String>) -> Self {
Self {
complete: false,
reasons,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoAllowDiagnosticV1 {
pub rule_id: String,
pub rule_generation: u32,
pub subject_key: String,
pub severity: DiagnosticSeverity,
pub posture: RulePosture,
pub confidence: DiagnosticConfidence,
pub result_class: DiagnosticResultClass,
pub primary_location: SourceRange,
pub related: Vec<RelatedLocation>,
pub missing_obligation: Option<MissingObligation>,
pub snapshot_identity: String,
pub message: String,
pub actions: Vec<CargoAllowActionV1>,
}
impl CargoAllowDiagnosticV1 {
pub fn fingerprint(&self) -> String {
let mut canonical = Vec::new();
push_field(&mut canonical, "cargo-allow.diagnostic-id.v1");
push_field(&mut canonical, &self.rule_id);
push_field(&mut canonical, &self.rule_generation.to_string());
push_field(&mut canonical, &self.subject_key);
push_field(&mut canonical, self.result_class.as_str());
push_field(
&mut canonical,
self.missing_obligation
.map(MissingObligation::as_str)
.unwrap_or(""),
);
push_field(&mut canonical, &self.primary_location.path);
push_field(
&mut canonical,
&location_position_key(self.primary_location.start),
);
push_field(
&mut canonical,
&location_position_key(self.primary_location.end),
);
push_field(&mut canonical, self.primary_location.encoding.as_str());
push_field(&mut canonical, self.primary_location.base.as_str());
push_field(&mut canonical, self.primary_location.provenance.as_str());
push_field(&mut canonical, &self.snapshot_identity);
sha256_v1_bytes(&canonical)
}
pub fn actions_are_coherent(&self) -> bool {
self.actions
.iter()
.all(CargoAllowActionV1::applicability_is_coherent)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoAllowDiagnosticBatchV1 {
pub schema: &'static str,
pub snapshot_identity: String,
pub diagnostics: Vec<CargoAllowDiagnosticV1>,
pub partial_data: PartialDataBoundary,
}
impl CargoAllowDiagnosticBatchV1 {
pub fn new(snapshot_identity: impl Into<String>) -> Self {
Self {
schema: DIAGNOSTIC_KERNEL_SCHEMA,
snapshot_identity: snapshot_identity.into(),
diagnostics: Vec::new(),
partial_data: PartialDataBoundary::complete(),
}
}
pub fn with_diagnostic(mut self, diagnostic: CargoAllowDiagnosticV1) -> Self {
self.diagnostics.push(diagnostic);
self
}
pub fn with_partial_data(mut self, partial_data: PartialDataBoundary) -> Self {
self.partial_data = partial_data;
self
}
pub fn diagnostic_fingerprints(&self) -> Vec<String> {
self.diagnostics
.iter()
.map(CargoAllowDiagnosticV1::fingerprint)
.collect()
}
}
fn location_position_key(position: Option<SourcePosition>) -> String {
match position {
None => String::new(),
Some(position) => match position.column {
Some(column) => format!("{}:{column}", position.line),
None => format!("{}:", position.line),
},
}
}
fn push_field(output: &mut Vec<u8>, value: &str) {
output.extend_from_slice(&(value.len() as u64).to_be_bytes());
output.extend_from_slice(value.as_bytes());
}
#[cfg(test)]
#[path = "actionable_diagnostic_tests.rs"]
mod tests;