objectiveai_sdk/laboratories/
composite.rs1use super::{ClientLaboratory, ClientLaboratoryType};
23
24const ALPHABET: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
26
27fn base62_encode_str(s: &str) -> String {
31 let bytes = s.as_bytes();
32 let leading_zeros = bytes.iter().take_while(|b| **b == 0).count();
33 let mut digits: Vec<u8> = Vec::new();
34 let mut num: Vec<u8> = bytes[leading_zeros..].to_vec();
35 while !num.is_empty() {
36 let mut quotient: Vec<u8> = Vec::with_capacity(num.len());
38 let mut remainder: u32 = 0;
39 for byte in &num {
40 let acc = remainder * 256 + *byte as u32;
41 let q = (acc / 62) as u8;
42 remainder = acc % 62;
43 if !(quotient.is_empty() && q == 0) {
44 quotient.push(q);
45 }
46 }
47 digits.push(ALPHABET[remainder as usize]);
48 num = quotient;
49 }
50 let mut out = String::with_capacity(leading_zeros + digits.len());
51 for _ in 0..leading_zeros {
52 out.push('0');
53 }
54 out.extend(digits.iter().rev().map(|d| *d as char));
55 out
56}
57
58fn base62_decode_str(s: &str) -> Option<String> {
62 let digits = s.as_bytes();
63 let leading_zeros = digits.iter().take_while(|d| **d == b'0').count();
64 let mut num: Vec<u8> = Vec::new();
65 for digit in &digits[leading_zeros..] {
66 let value = ALPHABET.iter().position(|a| a == digit)? as u32;
67 let mut carry = value;
69 for byte in num.iter_mut().rev() {
70 let acc = *byte as u32 * 62 + carry;
71 *byte = (acc % 256) as u8;
72 carry = acc / 256;
73 }
74 while carry > 0 {
75 num.insert(0, (carry % 256) as u8);
76 carry /= 256;
77 }
78 }
79 let mut bytes = vec![0u8; leading_zeros];
82 bytes.extend(num);
83 String::from_utf8(bytes).ok()
84}
85
86fn fnv1a32(bytes: &[u8]) -> u32 {
90 let mut hash: u32 = 0x811c9dc5;
91 for byte in bytes {
92 hash ^= *byte as u32;
93 hash = hash.wrapping_mul(0x0100_0193);
94 }
95 hash
96}
97
98fn base62_encode_u32(mut value: u32) -> String {
101 let mut digits = [b'0'; 6];
102 for slot in digits.iter_mut().rev() {
103 *slot = ALPHABET[(value % 62) as usize];
104 value /= 62;
105 }
106 String::from_utf8(digits.to_vec()).expect("alphabet is ASCII")
107}
108
109impl ClientLaboratory {
110 pub fn server_name(&self) -> Option<String> {
121 self.composite_id()
122 .map(|composite| format!("oail-{}", base62_encode_u32(fnv1a32(composite.as_bytes()))))
123 }
124
125 pub fn composite_id(&self) -> Option<String> {
129 let machine = self.machine.as_deref()?;
130 let machine_state = self.machine_state.as_deref()?;
131 Some(format!(
132 "{machine}/{}/{}",
133 base62_encode_str(machine_state),
134 base62_encode_str(&self.id)
135 ))
136 }
137
138 pub fn from_composite_id(s: &str) -> Option<Self> {
145 let mut parts = s.split('/');
146 let machine = parts.next()?;
147 let machine_state = parts.next()?;
148 let id = parts.next()?;
149 if parts.next().is_some() || machine.is_empty() {
150 return None;
151 }
152 Some(Self {
153 r#type: ClientLaboratoryType::Client,
154 id: base62_decode_str(id)?,
155 machine: Some(machine.to_string()),
156 machine_state: Some(base62_decode_str(machine_state)?),
157 })
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn marker(machine: &str, state: &str, id: &str) -> ClientLaboratory {
166 ClientLaboratory {
167 r#type: ClientLaboratoryType::Client,
168 id: id.to_string(),
169 machine: Some(machine.to_string()),
170 machine_state: Some(state.to_string()),
171 }
172 }
173
174 #[test]
175 fn roundtrips() {
176 for (state, id) in [
177 ("default", "my-lab"),
178 ("state/with/slashes", "id/with/slashes"),
179 ("under_score.dots", "0leading-zero-digit"),
180 ("ünïcodé 状態", "研究室/λ"),
181 ("", "empty-state"),
182 ("s", ""),
183 ("\u{0}\u{0}leading-nuls", "trailing-nul\u{0}"),
184 ] {
185 let original = marker("ab12", state, id);
186 let composite = original.composite_id().expect("pair present");
187 assert_eq!(
188 composite.matches('/').count(),
189 2,
190 "exactly three segments: {composite}"
191 );
192 let parsed = ClientLaboratory::from_composite_id(&composite)
193 .unwrap_or_else(|| panic!("parses: {composite}"));
194 assert_eq!(parsed, original, "roundtrip for {state:?}/{id:?}");
195 }
196 }
197
198 #[test]
199 fn server_name_is_fixed_width_and_name_safe() {
200 for (state, id) in [
202 ("default", "my-lab"),
203 ("state/with/slashes", &"x".repeat(500) as &str),
204 ("ünïcodé 状態", "研究室/λ"),
205 ] {
206 let lab = marker("3fa8", state, id);
207 let name = lab.server_name().expect("pair present");
208 assert_eq!(name.len(), "oail-".len() + 6, "fixed width: {name}");
209 assert!(name.starts_with("oail-"));
210 assert!(
211 name["oail-".len()..].bytes().all(|b| b.is_ascii_alphanumeric()),
212 "hash token is [0-9A-Za-z]: {name}"
213 );
214 assert_eq!(lab.server_name(), Some(name));
216 }
217 let mut lab = marker("m", "s", "i");
219 lab.machine = None;
220 assert_eq!(lab.server_name(), None);
221 }
222
223 #[test]
224 fn pairless_marker_has_no_composite() {
225 let mut lab = marker("m", "s", "i");
226 lab.machine_state = None;
227 assert_eq!(lab.composite_id(), None);
228 lab.machine_state = Some("s".to_string());
229 lab.machine = None;
230 assert_eq!(lab.composite_id(), None);
231 }
232
233 #[test]
234 fn malformed_inputs_rejected() {
235 for bad in [
236 "", "bare-id", "machine/only-two", "m/a/b/c", "/state62/id62", "m/bad!chars/aa", "m/aa/bad chars", ] {
244 assert_eq!(
245 ClientLaboratory::from_composite_id(bad),
246 None,
247 "must reject {bad:?}"
248 );
249 }
250 }
251}