Skip to main content

allow_core/
fingerprint.rs

1use crate::policy::AllowEntry;
2use sha2::{Digest, Sha256};
3
4pub fn normalize_snippet(input: &str) -> String {
5    strip_rust_comments(input)
6        .split_whitespace()
7        .collect::<Vec<_>>()
8        .join(" ")
9}
10
11fn strip_rust_comments(input: &str) -> String {
12    let mut out = String::with_capacity(input.len());
13    let mut chars = input.chars().peekable();
14    while let Some(ch) = chars.next() {
15        match ch {
16            '"' => consume_quoted_string(ch, &mut chars, &mut out),
17            'r' if consume_raw_string(&mut chars, &mut out) => {}
18            '/' => match chars.peek().copied() {
19                Some('/') => {
20                    let _ = chars.next();
21                    consume_line_comment(&mut chars);
22                    out.push(' ');
23                }
24                Some('*') => {
25                    let _ = chars.next();
26                    consume_block_comment(&mut chars);
27                    out.push(' ');
28                }
29                _ => out.push(ch),
30            },
31            _ => out.push(ch),
32        }
33    }
34    out
35}
36
37fn consume_quoted_string(
38    quote: char,
39    chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
40    out: &mut String,
41) {
42    out.push(quote);
43    let mut escaped = false;
44    for ch in chars.by_ref() {
45        out.push(ch);
46        if escaped {
47            escaped = false;
48        } else if ch == '\\' {
49            escaped = true;
50        } else if ch == quote {
51            break;
52        }
53    }
54}
55
56fn consume_raw_string(
57    chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
58    out: &mut String,
59) -> bool {
60    let mut lookahead = chars.clone();
61    let mut hashes = 0usize;
62    while lookahead.peek().copied() == Some('#') {
63        let _ = lookahead.next();
64        hashes += 1;
65    }
66    if lookahead.next() != Some('"') {
67        return false;
68    }
69
70    out.push('r');
71    for _ in 0..hashes {
72        let Some(hash) = chars.next() else {
73            return true;
74        };
75        out.push(hash);
76    }
77    let Some(quote) = chars.next() else {
78        return true;
79    };
80    out.push(quote);
81    consume_raw_string_tail(chars, out, hashes);
82    true
83}
84
85fn consume_raw_string_tail(
86    chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
87    out: &mut String,
88    hashes: usize,
89) {
90    while let Some(ch) = chars.next() {
91        out.push(ch);
92        if ch != '"' {
93            continue;
94        }
95        let mut matched = 0usize;
96        while matched < hashes && chars.peek().copied() == Some('#') {
97            let Some(hash) = chars.next() else {
98                break;
99            };
100            out.push(hash);
101            matched += 1;
102        }
103        if matched == hashes {
104            break;
105        }
106    }
107}
108
109fn consume_line_comment(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
110    for ch in chars.by_ref() {
111        if ch == '\n' {
112            break;
113        }
114    }
115}
116
117fn consume_block_comment(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
118    let mut depth = 1usize;
119    while let Some(ch) = chars.next() {
120        match (ch, chars.peek().copied()) {
121            ('/', Some('*')) => {
122                let _ = chars.next();
123                depth += 1;
124            }
125            ('*', Some('/')) => {
126                let _ = chars.next();
127                depth = depth.saturating_sub(1);
128                if depth == 0 {
129                    break;
130                }
131            }
132            _ => {}
133        }
134    }
135}
136
137pub fn stable_hash_hex(input: &str) -> String {
138    // FNV-1a 64-bit. Not cryptographic; stable across platforms and enough for drift hints.
139    let mut hash: u64 = 0xcbf29ce484222325;
140    for byte in input.as_bytes() {
141        hash ^= u64::from(*byte);
142        hash = hash.wrapping_mul(0x100000001b3);
143    }
144    format!("fnv1a64:{hash:016x}")
145}
146
147const ALLOW_ENTRY_FINGERPRINT_SCHEMA: &str = "cargo-allow.allow-entry-fingerprint.v1";
148
149/// Deterministic content fingerprint of an allow entry's full state, for
150/// mutation-receipt provenance (CARGO-ALLOW-SPEC-0008 "Mutation Receipt
151/// Envelope"). The `v1` canonical serialization is length-prefixed and has a
152/// fixed field order, so it is independent of Rust's `Debug` formatting and
153/// platform path separators — `path`, `glob`, and `selector.glob` are all
154/// slash-normalized before hashing, so semantically identical entries
155/// authored on Windows and Unix fingerprint identically. The SHA-256 digest is
156/// provenance evidence, not an identity or matching key.
157pub fn allow_entry_content_fingerprint(entry: &AllowEntry) -> String {
158    let mut canonical = Vec::new();
159    write_string(&mut canonical, ALLOW_ENTRY_FINGERPRINT_SCHEMA);
160    write_string(&mut canonical, &entry.id);
161    write_string(&mut canonical, entry.kind.as_str());
162    write_optional_string(&mut canonical, entry.family.as_deref());
163    write_optional_string(
164        &mut canonical,
165        entry.path.as_deref().map(crate::normalize_path).as_deref(),
166    );
167    write_optional_string(
168        &mut canonical,
169        entry.glob.as_deref().map(crate::normalize_path).as_deref(),
170    );
171    write_string(&mut canonical, &entry.owner);
172    write_string(&mut canonical, &entry.classification);
173    write_string(&mut canonical, &entry.reason);
174    write_string_list(&mut canonical, &entry.evidence);
175    write_string_list(&mut canonical, &entry.links);
176    write_optional_u32(&mut canonical, entry.occurrence_limit);
177    write_optional_string(&mut canonical, entry.lifecycle.created.as_deref());
178    write_optional_string(&mut canonical, entry.lifecycle.review_after.as_deref());
179    write_optional_string(&mut canonical, entry.lifecycle.expires.as_deref());
180
181    write_optional_string(&mut canonical, entry.selector.ast_kind.as_deref());
182    write_optional_string(&mut canonical, entry.selector.container.as_deref());
183    write_optional_string(&mut canonical, entry.selector.callee.as_deref());
184    write_optional_string(&mut canonical, entry.selector.macro_name.as_deref());
185    write_optional_string(&mut canonical, entry.selector.lint.as_deref());
186    write_optional_string(&mut canonical, entry.selector.symbol.as_deref());
187    write_optional_string(
188        &mut canonical,
189        entry.selector.receiver_fingerprint.as_deref(),
190    );
191    write_optional_string(&mut canonical, entry.selector.target_fingerprint.as_deref());
192    write_optional_string(
193        &mut canonical,
194        entry.selector.normalized_snippet_hash.as_deref(),
195    );
196    write_optional_u32(&mut canonical, entry.selector.line_hint);
197    write_optional_string(
198        &mut canonical,
199        entry
200            .selector
201            .glob
202            .as_deref()
203            .map(crate::normalize_path)
204            .as_deref(),
205    );
206    match &entry.last_seen {
207        Some(last_seen) => {
208            canonical.push(1);
209            canonical.extend_from_slice(&last_seen.line.to_be_bytes());
210            canonical.extend_from_slice(&last_seen.column.to_be_bytes());
211        }
212        None => canonical.push(0),
213    }
214
215    let digest = Sha256::digest(canonical);
216    let hex = digest
217        .iter()
218        .map(|byte| format!("{byte:02x}"))
219        .collect::<String>();
220    format!("sha256:v1:{hex}")
221}
222
223fn write_string(output: &mut Vec<u8>, value: &str) {
224    output.extend_from_slice(&(value.len() as u64).to_be_bytes());
225    output.extend_from_slice(value.as_bytes());
226}
227
228fn write_optional_string(output: &mut Vec<u8>, value: Option<&str>) {
229    match value {
230        Some(value) => {
231            output.push(1);
232            write_string(output, value);
233        }
234        None => output.push(0),
235    }
236}
237
238fn write_string_list(output: &mut Vec<u8>, values: &[String]) {
239    output.extend_from_slice(&(values.len() as u64).to_be_bytes());
240    for value in values {
241        write_string(output, value);
242    }
243}
244
245fn write_optional_u32(output: &mut Vec<u8>, value: Option<u32>) {
246    match value {
247        Some(value) => {
248            output.push(1);
249            output.extend_from_slice(&value.to_be_bytes());
250        }
251        None => output.push(0),
252    }
253}