1use crate::fingerprint::sha256_v1_bytes;
23
24pub const DIAGNOSTIC_KERNEL_SCHEMA: &str = "cargo-allow.diagnostic-kernel.v1";
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub enum DiagnosticSeverity {
31 Info,
32 Low,
33 Medium,
34 High,
35 Critical,
36}
37
38impl DiagnosticSeverity {
39 pub fn as_str(self) -> &'static str {
40 match self {
41 Self::Info => "info",
42 Self::Low => "low",
43 Self::Medium => "medium",
44 Self::High => "high",
45 Self::Critical => "critical",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum RulePosture {
54 Informational,
55 Advisory,
56 Shadow,
57 Blocking,
58}
59
60impl RulePosture {
61 pub fn as_str(self) -> &'static str {
62 match self {
63 Self::Informational => "informational",
64 Self::Advisory => "advisory",
65 Self::Shadow => "shadow",
66 Self::Blocking => "blocking",
67 }
68 }
69
70 pub fn is_blocking(self) -> bool {
72 matches!(self, Self::Blocking)
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum DiagnosticConfidence {
79 Exact,
80 High,
81 Bounded,
82 Uncertain,
83 Unavailable,
84}
85
86impl DiagnosticConfidence {
87 pub fn as_str(self) -> &'static str {
88 match self {
89 Self::Exact => "exact",
90 Self::High => "high",
91 Self::Bounded => "bounded",
92 Self::Uncertain => "uncertain",
93 Self::Unavailable => "unavailable",
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub enum DiagnosticResultClass {
102 Finding,
103 Stale,
104 NotProven,
105 Unsupported,
106 InstrumentFailure,
107}
108
109impl DiagnosticResultClass {
110 pub fn as_str(self) -> &'static str {
111 match self {
112 Self::Finding => "finding",
113 Self::Stale => "stale",
114 Self::NotProven => "not_proven",
115 Self::Unsupported => "unsupported",
116 Self::InstrumentFailure => "instrument_failure",
117 }
118 }
119
120 pub fn is_repository_condition(self) -> bool {
123 matches!(self, Self::Finding | Self::Stale | Self::NotProven)
124 }
125
126 pub fn result_class_is_tool_side(self) -> bool {
129 !self.is_repository_condition()
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136pub enum SourceEncoding {
137 Utf8,
138 Utf16,
139}
140
141impl SourceEncoding {
142 pub fn as_str(self) -> &'static str {
143 match self {
144 Self::Utf8 => "utf8",
145 Self::Utf16 => "utf16",
146 }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum PositionBase {
154 Zero,
155 One,
156}
157
158impl PositionBase {
159 pub fn as_str(self) -> &'static str {
160 match self {
161 Self::Zero => "zero_based",
162 Self::One => "one_based",
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
170pub enum SourceProvenance {
171 Authored,
172 Generated,
173}
174
175impl SourceProvenance {
176 pub fn as_str(self) -> &'static str {
177 match self {
178 Self::Authored => "authored",
179 Self::Generated => "generated",
180 }
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
187pub struct SourcePosition {
188 pub line: u32,
189 pub column: Option<u32>,
190}
191
192impl SourcePosition {
193 pub fn line_only(line: u32) -> Self {
194 Self { line, column: None }
195 }
196
197 pub fn precise(line: u32, column: u32) -> Self {
198 Self {
199 line,
200 column: Some(column),
201 }
202 }
203
204 pub fn is_precise(&self) -> bool {
205 self.column.is_some()
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Hash)]
213pub struct SourceRange {
214 pub path: String,
216 pub start: Option<SourcePosition>,
217 pub end: Option<SourcePosition>,
218 pub encoding: SourceEncoding,
219 pub base: PositionBase,
220 pub provenance: SourceProvenance,
221 pub content_identity: Option<String>,
223}
224
225impl SourceRange {
226 pub fn file(path: impl Into<String>) -> Self {
228 Self {
229 path: path.into(),
230 start: None,
231 end: None,
232 encoding: SourceEncoding::Utf8,
233 base: PositionBase::One,
234 provenance: SourceProvenance::Authored,
235 content_identity: None,
236 }
237 }
238
239 pub fn with_span(mut self, start: SourcePosition, end: SourcePosition) -> Self {
240 self.start = Some(start);
241 self.end = Some(end);
242 self
243 }
244
245 pub fn with_provenance(mut self, provenance: SourceProvenance) -> Self {
246 self.provenance = provenance;
247 self
248 }
249
250 pub fn with_content_identity(mut self, identity: impl Into<String>) -> Self {
251 self.content_identity = Some(identity.into());
252 self
253 }
254
255 pub fn is_precise(&self) -> bool {
257 self.start.is_some_and(|start| start.is_precise())
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
263pub enum RelatedRole {
264 Requirement,
265 ImplementationSeam,
266 TestSubject,
267 Receipt,
268 Definition,
269 Reference,
270}
271
272impl RelatedRole {
273 pub fn as_str(self) -> &'static str {
274 match self {
275 Self::Requirement => "requirement",
276 Self::ImplementationSeam => "implementation_seam",
277 Self::TestSubject => "test_subject",
278 Self::Receipt => "receipt",
279 Self::Definition => "definition",
280 Self::Reference => "reference",
281 }
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Hash)]
288pub struct RelatedLocation {
289 pub role: RelatedRole,
290 pub range: SourceRange,
291 pub note: Option<String>,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub enum MissingObligation {
298 NormativeRequirementMissing,
299 ImplementationSliceMissingOrStale,
300 ImplementationSeamOwnerMissing,
301 EvidencePurposeMissing,
302 EvidenceSubjectMissingOrAmbiguous,
303 NegativeDiscriminatorMissing,
304 ProofCommandMissingOrIncompatible,
305 ExternalReceiptMissingOrStale,
306 ReceiptSubjectsMissing,
307 SpecCodeTestAtomicityBroken,
308 AuthorityMissingOrContradictory,
309 GeneratedArtifactStale,
310 PackOrAdapterDrift,
311 UnsupportedCapability,
312 RepositoryDecisionRequired,
313}
314
315impl MissingObligation {
316 pub fn as_str(self) -> &'static str {
317 match self {
318 Self::NormativeRequirementMissing => "normative_requirement_missing",
319 Self::ImplementationSliceMissingOrStale => "implementation_slice_missing_or_stale",
320 Self::ImplementationSeamOwnerMissing => "implementation_seam_owner_missing",
321 Self::EvidencePurposeMissing => "evidence_purpose_missing",
322 Self::EvidenceSubjectMissingOrAmbiguous => "evidence_subject_missing_or_ambiguous",
323 Self::NegativeDiscriminatorMissing => "negative_discriminator_missing",
324 Self::ProofCommandMissingOrIncompatible => "proof_command_missing_or_incompatible",
325 Self::ExternalReceiptMissingOrStale => "external_receipt_missing_or_stale",
326 Self::ReceiptSubjectsMissing => "receipt_subjects_missing",
327 Self::SpecCodeTestAtomicityBroken => "spec_code_test_atomicity_broken",
328 Self::AuthorityMissingOrContradictory => "authority_missing_or_contradictory",
329 Self::GeneratedArtifactStale => "generated_artifact_stale",
330 Self::PackOrAdapterDrift => "pack_or_adapter_drift",
331 Self::UnsupportedCapability => "unsupported_capability",
332 Self::RepositoryDecisionRequired => "repository_decision_required",
333 }
334 }
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
340pub enum ActionKind {
341 AutomaticSafeEdit,
342 PreviewableWorkspaceEdit,
343 GenerateOwnedArtifact,
344 RunCargoAllowCommand,
345 OpenOrNavigate,
346 RefreshOrReissue,
347 ChooseBetweenAuthorities,
348 RequestRepositoryDecision,
349 PerformExternalAction,
350 DeferWithTypedReason,
351 SuppressOrExemptUnderPolicy,
352 NoSafeActionKnown,
353}
354
355impl ActionKind {
356 pub fn as_str(self) -> &'static str {
357 match self {
358 Self::AutomaticSafeEdit => "automatic_safe_edit",
359 Self::PreviewableWorkspaceEdit => "previewable_workspace_edit",
360 Self::GenerateOwnedArtifact => "generate_owned_artifact",
361 Self::RunCargoAllowCommand => "run_cargo_allow_command",
362 Self::OpenOrNavigate => "open_or_navigate",
363 Self::RefreshOrReissue => "refresh_or_reissue",
364 Self::ChooseBetweenAuthorities => "choose_between_authorities",
365 Self::RequestRepositoryDecision => "request_repository_decision",
366 Self::PerformExternalAction => "perform_external_action",
367 Self::DeferWithTypedReason => "defer_with_typed_reason",
368 Self::SuppressOrExemptUnderPolicy => "suppress_or_exempt_under_policy",
369 Self::NoSafeActionKnown => "no_safe_action_known",
370 }
371 }
372
373 pub fn mutates_source(self) -> bool {
377 matches!(
378 self,
379 Self::AutomaticSafeEdit
380 | Self::PreviewableWorkspaceEdit
381 | Self::GenerateOwnedArtifact
382 | Self::RefreshOrReissue
383 | Self::SuppressOrExemptUnderPolicy
384 )
385 }
386
387 pub fn may_be_automatic(self) -> bool {
395 matches!(
396 self,
397 Self::AutomaticSafeEdit | Self::GenerateOwnedArtifact | Self::RefreshOrReissue
398 )
399 }
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
404pub enum ActionApplicability {
405 Automatic,
407 Preview,
409 Manual,
411 Unavailable,
413}
414
415impl ActionApplicability {
416 pub fn as_str(self) -> &'static str {
417 match self {
418 Self::Automatic => "automatic",
419 Self::Preview => "preview",
420 Self::Manual => "manual",
421 Self::Unavailable => "unavailable",
422 }
423 }
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Hash)]
428pub struct RequiredProof {
429 pub command_argv: Vec<String>,
431 pub description: Option<String>,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Hash)]
436pub struct CargoAllowActionV1 {
437 pub id: String,
438 pub kind: ActionKind,
439 pub applicability: ActionApplicability,
440 pub preconditions: Vec<String>,
442 pub mutation_scope: Option<String>,
446 pub expected_effect: String,
447 pub rollback: Option<String>,
449 pub required_proof: Option<RequiredProof>,
450 pub residual_claim: Vec<String>,
452}
453
454impl CargoAllowActionV1 {
455 pub fn navigate(id: impl Into<String>, expected_effect: impl Into<String>) -> Self {
457 Self {
458 id: id.into(),
459 kind: ActionKind::OpenOrNavigate,
460 applicability: ActionApplicability::Manual,
461 preconditions: Vec::new(),
462 mutation_scope: None,
463 expected_effect: expected_effect.into(),
464 rollback: None,
465 required_proof: None,
466 residual_claim: Vec::new(),
467 }
468 }
469
470 pub fn applicability_is_coherent(&self) -> bool {
474 if self.applicability == ActionApplicability::Automatic {
475 return self.kind.may_be_automatic();
476 }
477 true
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, Hash)]
484pub struct PartialDataBoundary {
485 pub complete: bool,
486 pub reasons: Vec<String>,
487}
488
489impl PartialDataBoundary {
490 pub fn complete() -> Self {
491 Self {
492 complete: true,
493 reasons: Vec::new(),
494 }
495 }
496
497 pub fn partial(reasons: Vec<String>) -> Self {
498 Self {
499 complete: false,
500 reasons,
501 }
502 }
503}
504
505#[derive(Debug, Clone, PartialEq, Eq)]
508pub struct CargoAllowDiagnosticV1 {
509 pub rule_id: String,
510 pub rule_generation: u32,
511 pub subject_key: String,
513 pub severity: DiagnosticSeverity,
514 pub posture: RulePosture,
515 pub confidence: DiagnosticConfidence,
516 pub result_class: DiagnosticResultClass,
517 pub primary_location: SourceRange,
518 pub related: Vec<RelatedLocation>,
519 pub missing_obligation: Option<MissingObligation>,
520 pub snapshot_identity: String,
523 pub message: String,
524 pub actions: Vec<CargoAllowActionV1>,
525}
526
527impl CargoAllowDiagnosticV1 {
528 pub fn fingerprint(&self) -> String {
534 let mut canonical = Vec::new();
535 push_field(&mut canonical, "cargo-allow.diagnostic-id.v1");
536 push_field(&mut canonical, &self.rule_id);
537 push_field(&mut canonical, &self.rule_generation.to_string());
538 push_field(&mut canonical, &self.subject_key);
539 push_field(&mut canonical, self.result_class.as_str());
540 push_field(
541 &mut canonical,
542 self.missing_obligation
543 .map(MissingObligation::as_str)
544 .unwrap_or(""),
545 );
546 push_field(&mut canonical, &self.primary_location.path);
547 push_field(
548 &mut canonical,
549 &location_position_key(self.primary_location.start),
550 );
551 push_field(
552 &mut canonical,
553 &location_position_key(self.primary_location.end),
554 );
555 push_field(&mut canonical, self.primary_location.encoding.as_str());
559 push_field(&mut canonical, self.primary_location.base.as_str());
560 push_field(&mut canonical, self.primary_location.provenance.as_str());
564 push_field(&mut canonical, &self.snapshot_identity);
565 sha256_v1_bytes(&canonical)
566 }
567
568 pub fn actions_are_coherent(&self) -> bool {
570 self.actions
571 .iter()
572 .all(CargoAllowActionV1::applicability_is_coherent)
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Eq)]
578pub struct CargoAllowDiagnosticBatchV1 {
579 pub schema: &'static str,
580 pub snapshot_identity: String,
581 pub diagnostics: Vec<CargoAllowDiagnosticV1>,
582 pub partial_data: PartialDataBoundary,
583}
584
585impl CargoAllowDiagnosticBatchV1 {
586 pub fn new(snapshot_identity: impl Into<String>) -> Self {
587 Self {
588 schema: DIAGNOSTIC_KERNEL_SCHEMA,
589 snapshot_identity: snapshot_identity.into(),
590 diagnostics: Vec::new(),
591 partial_data: PartialDataBoundary::complete(),
592 }
593 }
594
595 pub fn with_diagnostic(mut self, diagnostic: CargoAllowDiagnosticV1) -> Self {
596 self.diagnostics.push(diagnostic);
597 self
598 }
599
600 pub fn with_partial_data(mut self, partial_data: PartialDataBoundary) -> Self {
601 self.partial_data = partial_data;
602 self
603 }
604
605 pub fn diagnostic_fingerprints(&self) -> Vec<String> {
607 self.diagnostics
608 .iter()
609 .map(CargoAllowDiagnosticV1::fingerprint)
610 .collect()
611 }
612}
613
614fn location_position_key(position: Option<SourcePosition>) -> String {
615 match position {
616 None => String::new(),
617 Some(position) => match position.column {
618 Some(column) => format!("{}:{column}", position.line),
619 None => format!("{}:", position.line),
620 },
621 }
622}
623
624fn push_field(output: &mut Vec<u8>, value: &str) {
626 output.extend_from_slice(&(value.len() as u64).to_be_bytes());
627 output.extend_from_slice(value.as_bytes());
628}
629
630#[cfg(test)]
631#[path = "actionable_diagnostic_tests.rs"]
632mod tests;