rustack_cloudfront_core/
id_gen.rs1use std::hash::{Hash, Hasher};
8
9use rand::RngExt;
10
11const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
12
13const ID_BODY_LEN: usize = 13;
15
16#[must_use]
18pub fn new_distribution_id() -> String {
19 new_id_with_prefix('E')
20}
21
22#[must_use]
24pub fn new_invalidation_id() -> String {
25 new_id_with_prefix('I')
26}
27
28#[must_use]
30pub fn new_id_with_prefix(prefix: char) -> String {
31 let mut rng = rand::rng();
32 let mut buf = String::with_capacity(ID_BODY_LEN + 1);
33 buf.push(prefix);
34 for _ in 0..ID_BODY_LEN {
35 let idx = rng.random_range(0..ALPHABET.len());
36 buf.push(ALPHABET[idx] as char);
37 }
38 buf
39}
40
41#[must_use]
46pub fn deterministic_id_with_prefix(prefix: char, seed: &str) -> String {
47 let mut hasher = std::collections::hash_map::DefaultHasher::new();
48 seed.hash(&mut hasher);
49 let mut value = hasher.finish();
50 let mut buf = String::with_capacity(ID_BODY_LEN + 1);
51 buf.push(prefix);
52 for _ in 0..ID_BODY_LEN {
53 let idx = (value % ALPHABET.len() as u64) as usize;
54 buf.push(ALPHABET[idx] as char);
55 value /= ALPHABET.len() as u64;
56 value = value
58 .wrapping_mul(6_364_136_223_846_793_005)
59 .wrapping_add(1);
60 }
61 buf
62}
63
64#[must_use]
69pub fn new_etag() -> String {
70 new_id_with_prefix('E')
71}
72
73#[must_use]
75pub fn distribution_domain_name(distribution_id: &str, domain_suffix: &str) -> String {
76 format!("{}.{}", distribution_id.to_ascii_lowercase(), domain_suffix)
77}
78
79#[must_use]
81pub fn is_valid_resource_id(id: &str, expected_prefix: Option<char>) -> bool {
82 if id.len() != ID_BODY_LEN + 1 {
83 return false;
84 }
85 let mut iter = id.chars();
86 let first = match iter.next() {
87 Some(c) => c,
88 None => return false,
89 };
90 if !first.is_ascii_uppercase() {
91 return false;
92 }
93 if let Some(prefix) = expected_prefix {
94 if first != prefix {
95 return false;
96 }
97 }
98 iter.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
99}
100
101#[must_use]
103pub fn new_s3_canonical_user_id() -> String {
104 let mut rng = rand::rng();
105 const HEX: &[u8] = b"0123456789abcdef";
106 let mut buf = String::with_capacity(64);
107 for _ in 0..64 {
108 buf.push(HEX[rng.random_range(0..HEX.len())] as char);
109 }
110 buf
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn test_should_generate_14_char_id() {
119 let id = new_distribution_id();
120 assert_eq!(id.len(), 14);
121 assert!(id.starts_with('E'));
122 assert!(is_valid_resource_id(&id, Some('E')));
123 }
124
125 #[test]
126 fn test_should_generate_deterministic_id() {
127 let a = deterministic_id_with_prefix('E', "hello");
128 let b = deterministic_id_with_prefix('E', "hello");
129 assert_eq!(a, b);
130 assert_ne!(a, deterministic_id_with_prefix('E', "world"));
131 }
132
133 #[test]
134 fn test_should_derive_domain_name() {
135 let name = distribution_domain_name("E1ABCDEF123456", "cloudfront.net");
136 assert_eq!(name, "e1abcdef123456.cloudfront.net");
137 }
138
139 #[test]
140 fn test_should_validate_resource_id_shape() {
141 assert!(is_valid_resource_id("E1ABCDEF123456", Some('E')));
142 assert!(!is_valid_resource_id("E1ABCDEF123456", Some('I')));
143 assert!(!is_valid_resource_id("e1abcdef123456", Some('E')));
144 assert!(!is_valid_resource_id("E1", Some('E')));
145 }
146}