Skip to main content

didbot_schema/
record_key.rs

1//! Record keys: the last segment of an `at://` URI, and the name a record is
2//! stored under in its collection.
3
4/// Longest a record key may be.
5pub const MAX_RECORD_KEY_LENGTH: usize = 512;
6
7/// Why a string is not a usable record key.
8#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
9pub enum RecordKeyError {
10    /// Empty, which would make the record unaddressable.
11    #[error("record key is empty")]
12    Empty,
13    /// Over [`MAX_RECORD_KEY_LENGTH`] characters.
14    #[error("record key is {0} characters, over the limit of {MAX_RECORD_KEY_LENGTH}")]
15    TooLong(usize),
16    /// A character outside `A-Za-z0-9._:~-`.
17    #[error("invalid character {0:?} in record key")]
18    InvalidCharacter(char),
19    /// The key is `.` or `..`.
20    ///
21    /// Both are legal under the character rule and both are path traversal
22    /// waiting to happen, because a record key is the last segment of an
23    /// `at://` URI and of many implementations' file layouts.
24    #[error("record key {0:?} is a relative path segment")]
25    RelativePath(String),
26}
27
28/// Checks a string against the record key rules.
29///
30/// This is the *generic* rule, which is far looser than what this server
31/// mints: every key `didbot-pds`'s record store produces is a TID, and a TID is
32/// a much narrower thing (see [`crate::tid`]). Both matter. Keys arriving from
33/// elsewhere — in an `at://` URI naming somebody else's record — follow only
34/// this rule, and treating them as TIDs would reject `self`, which is the key
35/// every singleton record in atproto uses.
36///
37/// ```
38/// use didbot_schema::validate_record_key;
39/// assert!(validate_record_key("self").is_ok());
40/// assert!(validate_record_key("3jzfcijpj2z2a").is_ok());
41/// assert!(validate_record_key("..").is_err());
42/// assert!(validate_record_key("alpha/beta").is_err());
43/// ```
44pub fn validate_record_key(rkey: &str) -> Result<(), RecordKeyError> {
45    if rkey.is_empty() {
46        return Err(RecordKeyError::Empty);
47    }
48    if rkey == "." || rkey == ".." {
49        return Err(RecordKeyError::RelativePath(rkey.to_owned()));
50    }
51    let length = rkey.chars().count();
52    if length > MAX_RECORD_KEY_LENGTH {
53        return Err(RecordKeyError::TooLong(length));
54    }
55    if let Some(ch) = rkey
56        .chars()
57        .find(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_' | ':' | '~')))
58    {
59        return Err(RecordKeyError::InvalidCharacter(ch));
60    }
61    Ok(())
62}