Skip to main content

veil_engine/
kind.rs

1//! Entity kinds: the `TYPE` in a placeholder such as `[EMAIL_1]`.
2
3use std::fmt;
4use std::sync::Arc;
5
6/// A kind of sensitive value. Built-in kinds are listed in [`kinds`]; custom
7/// rules can define their own, such as `CLIENT` or `TICKET`.
8#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
9pub struct EntityKind(Arc<str>);
10
11/// Names of the built-in kinds.
12pub mod kinds {
13    /// Email addresses.
14    pub const EMAIL: &str = "EMAIL";
15    /// Phone numbers.
16    pub const PHONE: &str = "PHONE";
17    /// Payment card numbers.
18    pub const CARD: &str = "CARD";
19    /// International bank account numbers (IBANs).
20    pub const IBAN: &str = "IBAN";
21    /// API keys, tokens and similar secrets.
22    pub const SECRET: &str = "SECRET";
23    /// IPv4 and IPv6 addresses.
24    pub const IP: &str = "IP";
25    /// URLs.
26    pub const URL: &str = "URL";
27    /// National ID numbers, such as US Social Security numbers.
28    pub const NATID: &str = "NATID";
29    /// People's names, from name detection.
30    pub const PERSON: &str = "PERSON";
31    /// Organisation names, from name detection.
32    pub const ORG: &str = "ORG";
33    /// Place names, from name detection.
34    pub const LOCATION: &str = "LOCATION";
35}
36
37/// A name that isn't a valid [`EntityKind`]. Holds the name.
38#[derive(Debug, thiserror::Error)]
39#[error("invalid type name {0:?}: use 1 to 32 characters A-Z, 0-9 and _, starting with a letter")]
40pub struct InvalidKind(pub String);
41
42impl EntityKind {
43    /// Checks and wraps a kind name: 1 to 32 characters `A`-`Z`, `0`-`9` and `_`,
44    /// starting with a letter.
45    pub fn new(name: &str) -> Result<Self, InvalidKind> {
46        let valid = (1..=32).contains(&name.len())
47            && name.starts_with(|c: char| c.is_ascii_uppercase())
48            && name.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_');
49        if valid {
50            Ok(EntityKind(name.into()))
51        } else {
52            Err(InvalidKind(name.to_owned()))
53        }
54    }
55
56    pub(crate) fn builtin(name: &'static str) -> Self {
57        EntityKind(name.into())
58    }
59
60    /// The kind's name, such as `EMAIL`.
61    pub fn as_str(&self) -> &str {
62        &self.0
63    }
64}
65
66impl fmt::Display for EntityKind {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.write_str(&self.0)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn validates_names() {
78        assert!(EntityKind::new("EMAIL").is_ok());
79        assert!(EntityKind::new("NATIONAL_ID2").is_ok());
80        for bad in ["", "email", "1A", "A-B", "A B", &"A".repeat(33)] {
81            assert!(EntityKind::new(bad).is_err(), "{bad}");
82        }
83    }
84}