dcbor_pattern/pattern/value/
digest_pattern.rs

1use bc_components::{Digest, tags};
2use bc_ur::UREncodable;
3use dcbor::prelude::*;
4
5use crate::pattern::{Matcher, Path, Pattern, vm::Instr};
6
7/// Pattern for matching dCBOR digest values (CBOR tag 40001).
8#[derive(Debug, Clone)]
9pub enum DigestPattern {
10    /// Matches any digest value.
11    Any,
12    /// Matches the exact digest.
13    Digest(Digest),
14    /// Matches the prefix of a digest (case insensitive).
15    Prefix(Vec<u8>),
16    /// Matches the binary regular expression for a digest.
17    BinaryRegex(regex::bytes::Regex),
18}
19
20impl PartialEq for DigestPattern {
21    fn eq(&self, other: &Self) -> bool {
22        match (self, other) {
23            (DigestPattern::Any, DigestPattern::Any) => true,
24            (DigestPattern::Digest(a), DigestPattern::Digest(b)) => a == b,
25            (DigestPattern::Prefix(a), DigestPattern::Prefix(b)) => {
26                a.eq_ignore_ascii_case(b)
27            }
28            (DigestPattern::BinaryRegex(a), DigestPattern::BinaryRegex(b)) => {
29                a.as_str() == b.as_str()
30            }
31            _ => false,
32        }
33    }
34}
35
36impl Eq for DigestPattern {}
37
38impl std::hash::Hash for DigestPattern {
39    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
40        match self {
41            DigestPattern::Any => {
42                0u8.hash(state);
43            }
44            DigestPattern::Digest(a) => {
45                1u8.hash(state);
46                a.hash(state);
47            }
48            DigestPattern::Prefix(prefix) => {
49                2u8.hash(state);
50                prefix.hash(state);
51            }
52            DigestPattern::BinaryRegex(regex) => {
53                3u8.hash(state);
54                // Regex does not implement Hash, so we hash its pattern string.
55                regex.as_str().hash(state);
56            }
57        }
58    }
59}
60
61impl DigestPattern {
62    /// Creates a new `DigestPattern` that matches any digest.
63    pub fn any() -> Self { DigestPattern::Any }
64
65    /// Creates a new `DigestPattern` that matches the exact digest.
66    pub fn digest(digest: Digest) -> Self { DigestPattern::Digest(digest) }
67
68    /// Creates a new `DigestPattern` that matches the prefix of a digest.
69    pub fn prefix(prefix: impl AsRef<[u8]>) -> Self {
70        DigestPattern::Prefix(prefix.as_ref().to_vec())
71    }
72
73    /// Creates a new `DigestPattern` that matches the binary regex for a
74    /// digest.
75    pub fn binary_regex(regex: regex::bytes::Regex) -> Self {
76        DigestPattern::BinaryRegex(regex)
77    }
78}
79
80impl Matcher for DigestPattern {
81    fn paths(&self, haystack: &CBOR) -> Vec<Path> {
82        // Check if the CBOR value is a tagged digest (tag 40001)
83        if let CBORCase::Tagged(tag, content) = haystack.as_case() {
84            if tag.value() == tags::TAG_DIGEST {
85                // Try to extract the digest from the tagged content
86                match CBOR::try_into_byte_string(content.clone()) {
87                    Ok(digest_bytes) => {
88                        if digest_bytes.len() == Digest::DIGEST_SIZE {
89                            let is_hit = match self {
90                                DigestPattern::Any => true,
91                                DigestPattern::Digest(pattern_digest) => {
92                                    digest_bytes == pattern_digest.data()
93                                }
94                                DigestPattern::Prefix(prefix) => {
95                                    digest_bytes.starts_with(prefix)
96                                }
97                                DigestPattern::BinaryRegex(regex) => {
98                                    regex.is_match(&digest_bytes)
99                                }
100                            };
101
102                            if is_hit {
103                                return vec![vec![haystack.clone()]];
104                            }
105                        }
106                    }
107                    Err(_) => {
108                        // Not a byte string, no match
109                    }
110                }
111            }
112        }
113
114        vec![]
115    }
116
117    fn compile(
118        &self,
119        code: &mut Vec<Instr>,
120        literals: &mut Vec<Pattern>,
121        _captures: &mut Vec<String>,
122    ) {
123        let idx = literals.len();
124        literals.push(Pattern::Value(crate::pattern::ValuePattern::Digest(
125            self.clone(),
126        )));
127        code.push(Instr::MatchPredicate(idx));
128    }
129}
130
131impl std::fmt::Display for DigestPattern {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        match self {
134            DigestPattern::Any => write!(f, "digest"),
135            DigestPattern::Digest(digest) => {
136                write!(f, "digest'{}'", digest.ur_string())
137            }
138            DigestPattern::Prefix(prefix) => {
139                write!(f, "digest'{}'", hex::encode(prefix))
140            }
141            DigestPattern::BinaryRegex(regex) => {
142                write!(f, "digest'/{}/'", regex.as_str())
143            }
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use bc_components::{Digest, DigestProvider};
151    use bc_ur::UREncodable;
152
153    use super::*;
154
155    fn test_digest() -> Digest {
156        let data: &[u8] = b"test data";
157        data.digest().into_owned()
158    }
159
160    fn create_digest_cbor(digest: &Digest) -> CBOR { digest.to_cbor() }
161
162    #[test]
163    fn test_digest_pattern_display() {
164        bc_components::register_tags();
165
166        let digest = test_digest();
167        let pattern = DigestPattern::digest(digest.clone());
168        assert_eq!(
169            format!("{}", pattern),
170            format!("digest'{}'", digest.ur_string())
171        );
172
173        let prefix = vec![0x74, 0x65, 0x73]; // "tes"
174        let pattern = DigestPattern::prefix(prefix.clone());
175        assert_eq!(
176            format!("{}", pattern),
177            format!("digest'{}'", hex::encode(&prefix))
178        );
179
180        let regex = regex::bytes::Regex::new(r"^te.*").unwrap();
181        let pattern = DigestPattern::binary_regex(regex.clone());
182        assert_eq!(format!("{}", pattern), format!("digest'/{}/'", regex));
183    }
184
185    #[test]
186    fn test_digest_pattern_exact_match() {
187        let digest = test_digest();
188        let digest_cbor = create_digest_cbor(&digest);
189        let pattern = DigestPattern::digest(digest.clone());
190
191        assert!(pattern.matches(&digest_cbor));
192        assert_eq!(
193            pattern.paths(&digest_cbor),
194            vec![vec![digest_cbor.clone()]]
195        );
196
197        // Test with different digest
198        let other_data: &[u8] = b"other data";
199        let other_digest = other_data.digest().into_owned();
200        let other_digest_cbor = create_digest_cbor(&other_digest);
201        assert!(!pattern.matches(&other_digest_cbor));
202        assert!(pattern.paths(&other_digest_cbor).is_empty());
203    }
204
205    #[test]
206    fn test_digest_pattern_prefix_match() {
207        let digest = test_digest();
208        let digest_cbor = create_digest_cbor(&digest);
209
210        // Test prefix matching - use first 4 bytes of the digest
211        let prefix = digest.data()[..4].to_vec();
212        let pattern = DigestPattern::prefix(prefix);
213
214        assert!(pattern.matches(&digest_cbor));
215        assert_eq!(
216            pattern.paths(&digest_cbor),
217            vec![vec![digest_cbor.clone()]]
218        );
219
220        // Test with digest that doesn't match the prefix
221        let other_data: &[u8] = b"completely different data";
222        let other_digest = other_data.digest().into_owned();
223        let other_digest_cbor = create_digest_cbor(&other_digest);
224
225        // Only match if the other digest happens to have the same prefix
226        // (unlikely)
227        let matches = pattern.matches(&other_digest_cbor);
228        assert_eq!(
229            matches,
230            other_digest.data().starts_with(&digest.data()[..4])
231        );
232    }
233
234    #[test]
235    fn test_digest_pattern_regex_match() {
236        let digest = test_digest();
237        let digest_cbor = create_digest_cbor(&digest);
238
239        // Create a regex that matches any binary data (any sequence of bytes)
240        let match_all_regex = regex::bytes::Regex::new(r".*").unwrap();
241        let pattern = DigestPattern::binary_regex(match_all_regex);
242
243        assert!(pattern.matches(&digest_cbor));
244        assert_eq!(
245            pattern.paths(&digest_cbor),
246            vec![vec![digest_cbor.clone()]]
247        );
248
249        // Test with a regex that definitely won't match - look for bytes that
250        // don't exist
251        let no_match_regex =
252            regex::bytes::Regex::new(r"^\xFF\xFF\xFF\xFF").unwrap();
253        let no_match_pattern = DigestPattern::binary_regex(no_match_regex);
254
255        // This should only match if the digest happens to start with 0xFFFFFFFF
256        // (very unlikely)
257        let matches = no_match_pattern.matches(&digest_cbor);
258        assert_eq!(
259            matches,
260            digest.data().starts_with(&[0xFF, 0xFF, 0xFF, 0xFF])
261        );
262    }
263
264    #[test]
265    fn test_digest_pattern_non_digest_cbor() {
266        let pattern = DigestPattern::digest(test_digest());
267
268        // Test with non-digest CBOR values
269        let text_cbor = "hello".to_cbor();
270        let number_cbor = 42.to_cbor();
271        let array_cbor = vec![1, 2, 3].to_cbor();
272
273        assert!(!pattern.matches(&text_cbor));
274        assert!(!pattern.matches(&number_cbor));
275        assert!(!pattern.matches(&array_cbor));
276
277        assert!(pattern.paths(&text_cbor).is_empty());
278        assert!(pattern.paths(&number_cbor).is_empty());
279        assert!(pattern.paths(&array_cbor).is_empty());
280    }
281
282    #[test]
283    fn test_digest_pattern_equality() {
284        let digest1 = test_digest();
285        let digest2 = test_digest();
286        assert_eq!(digest1, digest2); // Same data produces same digest
287
288        let pattern1 = DigestPattern::digest(digest1);
289        let pattern2 = DigestPattern::digest(digest2);
290        assert_eq!(pattern1, pattern2);
291
292        let prefix = vec![0x12, 0x34];
293        let prefix_pattern1 = DigestPattern::prefix(&prefix);
294        let prefix_pattern2 = DigestPattern::prefix(&prefix);
295        assert_eq!(prefix_pattern1, prefix_pattern2);
296
297        let regex = regex::bytes::Regex::new(r"^test").unwrap();
298        let regex_pattern1 = DigestPattern::binary_regex(regex.clone());
299        let regex_pattern2 = DigestPattern::binary_regex(regex);
300        assert_eq!(regex_pattern1, regex_pattern2);
301    }
302}