Skip to main content

falcon_library/
ids.rs

1//! Item and folder IDs.
2//!
3//! Eagle builds an ID from the creation time in milliseconds, written in base
4//! 36, followed by 5 random base-36 characters, all upper case. Eagle's own
5//! random part can come out shorter than 5 characters; falcon always pads it,
6//! because Eagle only loads `.info` folders whose name is 13 or 36 long.
7
8use std::time::{SystemTime, UNIX_EPOCH};
9
10const DIGITS: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
11const SUFFIX_LEN: usize = 5;
12
13/// A new ID for the current time.
14pub fn new_id() -> String {
15    id_at(now_ms(), &mut fastrand::Rng::new())
16}
17
18/// An ID for `ms` (milliseconds since the epoch) using `rng` for the suffix.
19pub fn id_at(ms: u64, rng: &mut fastrand::Rng) -> String {
20    let mut id = to_base36(ms);
21    id.extend((0..SUFFIX_LEN).map(|_| DIGITS[rng.usize(..36)] as char));
22    id
23}
24
25/// The creation time encoded in an ID's prefix, in milliseconds.
26///
27/// Returns `None` for legacy 36-character UUIDs and malformed IDs.
28pub fn timestamp(id: &str) -> Option<u64> {
29    let prefix = id.len().checked_sub(SUFFIX_LEN).filter(|_| id.len() != 36)?;
30    u64::from_str_radix(&id[..prefix], 36).ok()
31}
32
33/// Whether Eagle would load an `.info` folder with this ID.
34pub fn is_valid(id: &str) -> bool {
35    matches!(id.len(), 13 | 36)
36}
37
38fn to_base36(mut n: u64) -> String {
39    let mut out = Vec::new();
40    loop {
41        out.push(DIGITS[(n % 36) as usize]);
42        n /= 36;
43        if n == 0 {
44            break;
45        }
46    }
47    out.reverse();
48    String::from_utf8(out).expect("base-36 digits are ASCII")
49}
50
51fn now_ms() -> u64 {
52    let since = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
53    since.as_millis() as u64
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn prefix_matches_fixture_ids() {
62        // Taken from the Assets fixture, where each id's prefix equals the
63        // item's modificationTime.
64        assert_eq!(timestamp("MVP1BZ832CIJ1"), Some(1793005755411));
65        assert_eq!(timestamp("MVP1JTFJI1HWU"), Some(1793006121151));
66        assert_eq!(to_base36(1793005755411), "MVP1BZ83");
67    }
68
69    #[test]
70    fn generated_ids_round_trip() {
71        let mut rng = fastrand::Rng::with_seed(7);
72        let id = id_at(1793005755411, &mut rng);
73        assert_eq!(id.len(), 13);
74        assert!(id.starts_with("MVP1BZ83"));
75        assert!(is_valid(&id));
76        assert_eq!(timestamp(&id), Some(1793005755411));
77    }
78
79    #[test]
80    fn new_ids_sort_by_time() {
81        let mut rng = fastrand::Rng::with_seed(1);
82        assert!(id_at(1_000_000_000_000, &mut rng) < id_at(1_000_000_000_001, &mut rng));
83    }
84}