1use crate::{CargoAllowError, LedgerProvenance, normalize_path};
2use std::fmt;
3use std::path::PathBuf;
4use std::str::FromStr;
5
6pub const STRUCTURAL_IDENTITY_SCHEMA_ID: &str = "cargo-allow.structural-identity.v1";
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Span {
10 pub line: u32,
11 pub column: u32,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub enum FindingKind {
16 Panic,
17 Unsafe,
18 LintException,
19 NonRustFile,
20 GeneratedCode,
21 PolicyException,
22}
23
24impl FindingKind {
25 pub const ALL: &[Self] = &[
26 Self::Panic,
27 Self::Unsafe,
28 Self::LintException,
29 Self::NonRustFile,
30 Self::GeneratedCode,
31 Self::PolicyException,
32 ];
33
34 pub fn as_str(self) -> &'static str {
35 match self {
36 Self::Panic => "panic",
37 Self::Unsafe => "unsafe",
38 Self::LintException => "lint_exception",
39 Self::NonRustFile => "non_rust_file",
40 Self::GeneratedCode => "generated_code",
41 Self::PolicyException => "policy_exception",
42 }
43 }
44
45 pub fn requires_source_selector_identity(self) -> bool {
46 matches!(self, Self::Panic | Self::Unsafe | Self::LintException)
47 }
48}
49
50impl fmt::Display for FindingKind {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 write!(f, "{}", self.as_str())
53 }
54}
55
56impl FromStr for FindingKind {
57 type Err = CargoAllowError;
58
59 fn from_str(s: &str) -> Result<Self, Self::Err> {
60 let trimmed = s.trim();
61 let normalized = trimmed.to_ascii_lowercase();
62 match normalized.as_str() {
63 "panic" | "panic_family" | "panic-family" | "indexing" => Ok(Self::Panic),
64 "unsafe" => Ok(Self::Unsafe),
65 "lint_exception" | "lint-exception" | "clippy" | "allow_attribute"
66 | "allow-attribute" | "expect_attribute" | "expect-attribute" => {
67 Ok(Self::LintException)
68 }
69 "non_rust_file" | "non-rust-file" | "non_rust" | "non-rust" | "file" => {
70 Ok(Self::NonRustFile)
71 }
72 "generated_code" | "generated-code" | "generated" => Ok(Self::GeneratedCode),
73 "policy_exception" | "policy-exception" | "policy" => Ok(Self::PolicyException),
74 _ => Err(CargoAllowError::new(format!(
75 "unsupported finding kind `{trimmed}`; valid values: panic, unsafe, lint_exception, non_rust_file, generated_code, policy_exception"
76 ))),
77 }
78 }
79}
80
81pub const MAX_IDENTITY_FIELD_LEN: usize = 512;
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct StructuralIdentity {
90 pub language: String,
91 pub crate_name: Option<String>,
92 pub module: Option<String>,
93 pub container: Option<String>,
94 pub ast_kind: String,
95 pub symbol: Option<String>,
96 pub callee: Option<String>,
97 pub macro_name: Option<String>,
98 pub lint: Option<String>,
99 pub receiver_fingerprint: Option<String>,
100 pub target_fingerprint: Option<String>,
101 pub normalized_snippet_hash: Option<String>,
102 pub line_hint: Option<u32>,
103 pub column_hint: Option<u32>,
104}
105
106impl StructuralIdentity {
107 pub fn schema_id() -> &'static str {
108 STRUCTURAL_IDENTITY_SCHEMA_ID
109 }
110
111 pub fn new(language: impl Into<String>, ast_kind: impl Into<String>) -> Self {
112 Self {
113 language: language.into(),
114 crate_name: None,
115 module: None,
116 container: None,
117 ast_kind: ast_kind.into(),
118 symbol: None,
119 callee: None,
120 macro_name: None,
121 lint: None,
122 receiver_fingerprint: None,
123 target_fingerprint: None,
124 normalized_snippet_hash: None,
125 line_hint: None,
126 column_hint: None,
127 }
128 }
129
130 pub fn truncate_in_place(&mut self) {
137 let cap_opt = |s: &mut Option<String>| {
138 if let Some(value) = s {
139 truncate_identity_field(value);
140 }
141 };
142 let cap_str = |s: &mut String| {
143 truncate_identity_field(s);
144 };
145 cap_str(&mut self.language);
146 cap_opt(&mut self.crate_name);
147 cap_opt(&mut self.module);
148 cap_opt(&mut self.container);
149 cap_str(&mut self.ast_kind);
150 cap_opt(&mut self.symbol);
151 cap_opt(&mut self.callee);
152 cap_opt(&mut self.macro_name);
153 cap_opt(&mut self.lint);
154 cap_opt(&mut self.receiver_fingerprint);
155 cap_opt(&mut self.target_fingerprint);
156 }
157
158 pub fn redact_source_text_fields(&mut self) {
165 self.symbol = None;
166 self.callee = None;
167 self.container = None;
168 self.module = None;
169 self.macro_name = None;
170 self.lint = None;
171 }
172
173 pub fn stable_key(&self) -> String {
174 stable_identity_key_from_parts(self.stable_key_parts())
175 }
176
177 pub fn stable_key_parts(&self) -> Vec<(&'static str, String)> {
178 vec![
179 ("language", self.language.clone()),
180 (
181 "crate_name",
182 self.crate_name
183 .as_deref()
184 .map(str::trim)
185 .unwrap_or_default()
186 .to_string(),
187 ),
188 ("module", self.module.clone().unwrap_or_default()),
189 ("container", self.container.clone().unwrap_or_default()),
190 ("ast_kind", self.ast_kind.clone()),
191 ("symbol", self.symbol.clone().unwrap_or_default()),
192 ("callee", self.callee.clone().unwrap_or_default()),
193 ("macro_name", self.macro_name.clone().unwrap_or_default()),
194 ("lint", self.lint.clone().unwrap_or_default()),
195 (
196 "receiver_fingerprint",
197 self.receiver_fingerprint.clone().unwrap_or_default(),
198 ),
199 (
200 "target_fingerprint",
201 self.target_fingerprint.clone().unwrap_or_default(),
202 ),
203 (
204 "normalized_snippet_hash",
205 self.normalized_snippet_hash.clone().unwrap_or_default(),
206 ),
207 ]
208 }
209}
210
211fn truncate_identity_field(value: &mut String) {
212 if value.len() <= MAX_IDENTITY_FIELD_LEN {
213 return;
214 }
215 let mut end = MAX_IDENTITY_FIELD_LEN;
216 while !value.is_char_boundary(end) {
217 end -= 1;
218 }
219 value.truncate(end);
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct Finding {
224 pub kind: FindingKind,
225 pub family: Option<String>,
226 pub path: PathBuf,
227 pub span: Option<Span>,
228 pub identity: StructuralIdentity,
229 pub message: String,
230 pub ledger: Option<LedgerProvenance>,
231}
232
233impl Finding {
234 pub fn source_package_name(&self) -> Option<&str> {
235 self.identity
236 .crate_name
237 .as_deref()
238 .map(str::trim)
239 .filter(|name| !name.is_empty())
240 }
241}
242
243pub fn finding_identity_key(finding: &Finding) -> String {
244 let mut parts = vec![
245 ("kind", finding.kind.as_str().to_string()),
246 ("family", finding.family.clone().unwrap_or_default()),
247 ("path", normalize_path(&finding.path)),
248 ];
249 parts.extend(finding.identity.stable_key_parts());
250 stable_identity_key_from_parts(parts)
251}
252
253fn stable_identity_key_from_parts(parts: Vec<(&'static str, String)>) -> String {
254 parts
255 .into_iter()
256 .map(|(name, value)| format!("{name}:{}:{value}", value.len()))
257 .collect::<Vec<_>>()
258 .join("|")
259}
260
261#[cfg(test)]
262#[path = "finding_tests.rs"]
263mod tests;