1use crate::clock::itc::Stamp;
2
3pub const ITC_TEXT_PREFIX: &str = "itc:v3:";
4const LEGACY_ITC_TEXT_PREFIX: &str = "itc:v1:";
5const COMPACT_ITC_VERSION: u8 = 1;
6const SPARSE_ITC_WIRE_VERSION: u8 = 1;
7const BASE64_URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
8
9#[must_use]
10pub fn stamp_to_text(stamp: &Stamp) -> String {
11 let compact = stamp.serialize_compact();
12 compact_to_sparse_payload(&compact).map_or_else(
13 || format!("{LEGACY_ITC_TEXT_PREFIX}{}", encode_hex(&compact)),
14 |sparse_payload| {
15 format!(
16 "{ITC_TEXT_PREFIX}{}",
17 encode_base64_url_no_pad(&sparse_payload)
18 )
19 },
20 )
21}
22
23#[must_use]
24pub fn stamp_from_text(raw: &str) -> Option<Stamp> {
25 if let Some(encoded) = raw.strip_prefix(ITC_TEXT_PREFIX) {
26 let sparse_payload = decode_base64_url_no_pad(encoded)?;
27 let compact = sparse_payload_to_compact(&sparse_payload)?;
28 return Stamp::deserialize_compact(&compact).ok();
29 }
30
31 let encoded = raw.strip_prefix(LEGACY_ITC_TEXT_PREFIX)?;
32 let compact = decode_hex(encoded)?;
33 Stamp::deserialize_compact(&compact).ok()
34}
35
36#[derive(Debug, Clone, Copy)]
37struct CompactSections<'a> {
38 id_bit_len: usize,
39 id_bits: &'a [u8],
40 event_bit_len: usize,
41 event_bits: &'a [u8],
42 event_values: &'a [u8],
43}
44
45fn compact_to_sparse_payload(compact: &[u8]) -> Option<Vec<u8>> {
46 let sections = parse_compact_sections(compact)?;
47 let values = decode_u32_varint_list(sections.event_values, sections.event_bit_len)?;
48
49 let mut sparse = Vec::new();
50 sparse.push(SPARSE_ITC_WIRE_VERSION);
51 encode_varint_usize(sections.id_bit_len, &mut sparse);
52 sparse.extend_from_slice(sections.id_bits);
53 encode_varint_usize(sections.event_bit_len, &mut sparse);
54 sparse.extend_from_slice(sections.event_bits);
55
56 let non_zero_count = values.iter().filter(|&&value| value != 0).count();
57 encode_varint_usize(non_zero_count, &mut sparse);
58
59 let mut previous_index = 0usize;
60 let mut wrote_any = false;
61 for (index, value) in values.into_iter().enumerate() {
62 if value == 0 {
63 continue;
64 }
65 let delta = if wrote_any {
66 index.checked_sub(previous_index)?
67 } else {
68 index
69 };
70 encode_varint_usize(delta, &mut sparse);
71 encode_varint_u32(value, &mut sparse);
72 previous_index = index;
73 wrote_any = true;
74 }
75
76 Some(sparse)
77}
78
79fn sparse_payload_to_compact(sparse: &[u8]) -> Option<Vec<u8>> {
80 let mut cursor = 0usize;
81 let version = *sparse.get(cursor)?;
82 cursor += 1;
83 if version != SPARSE_ITC_WIRE_VERSION {
84 return None;
85 }
86
87 let id_bit_len = decode_varint_usize(sparse, &mut cursor)?;
88 let id_byte_len = bytes_for_bits(id_bit_len)?;
89 let id_bits = take_slice(sparse, &mut cursor, id_byte_len)?;
90
91 let event_bit_len = decode_varint_usize(sparse, &mut cursor)?;
92 let event_byte_len = bytes_for_bits(event_bit_len)?;
93 let event_bits = take_slice(sparse, &mut cursor, event_byte_len)?;
94
95 let non_zero_count = decode_varint_usize(sparse, &mut cursor)?;
96
97 let mut values = vec![0_u32; event_bit_len];
98 let mut previous_index = 0usize;
99 let mut has_previous = false;
100
101 for _ in 0..non_zero_count {
102 let delta = decode_varint_usize(sparse, &mut cursor)?;
103 let index = if has_previous {
104 previous_index.checked_add(delta)?
105 } else {
106 delta
107 };
108 if index >= values.len() {
109 return None;
110 }
111 if values[index] != 0 {
112 return None;
113 }
114
115 let value = decode_varint_u32(sparse, &mut cursor)?;
116 if value == 0 {
117 return None;
118 }
119 values[index] = value;
120 previous_index = index;
121 has_previous = true;
122 }
123
124 if cursor != sparse.len() {
125 return None;
126 }
127
128 let mut event_values = Vec::new();
129 for value in values {
130 encode_varint_u32(value, &mut event_values);
131 }
132
133 let mut compact = Vec::new();
134 compact.push(COMPACT_ITC_VERSION);
135 encode_varint_usize(id_bit_len, &mut compact);
136 compact.extend_from_slice(id_bits);
137 encode_varint_usize(event_bit_len, &mut compact);
138 compact.extend_from_slice(event_bits);
139 encode_varint_usize(event_values.len(), &mut compact);
140 compact.extend_from_slice(&event_values);
141 Some(compact)
142}
143
144fn parse_compact_sections(raw: &[u8]) -> Option<CompactSections<'_>> {
145 let mut cursor = 0usize;
146 let version = *raw.get(cursor)?;
147 cursor += 1;
148 if version != COMPACT_ITC_VERSION {
149 return None;
150 }
151
152 let id_bit_len = decode_varint_usize(raw, &mut cursor)?;
153 let id_byte_len = bytes_for_bits(id_bit_len)?;
154 let id_bits = take_slice(raw, &mut cursor, id_byte_len)?;
155
156 let event_bit_len = decode_varint_usize(raw, &mut cursor)?;
157 let event_byte_len = bytes_for_bits(event_bit_len)?;
158 let event_bits = take_slice(raw, &mut cursor, event_byte_len)?;
159
160 let event_values_len = decode_varint_usize(raw, &mut cursor)?;
161 let event_values = take_slice(raw, &mut cursor, event_values_len)?;
162
163 if cursor != raw.len() {
164 return None;
165 }
166
167 Some(CompactSections {
168 id_bit_len,
169 id_bits,
170 event_bit_len,
171 event_bits,
172 event_values,
173 })
174}
175
176fn decode_u32_varint_list(raw: &[u8], expected_len: usize) -> Option<Vec<u32>> {
177 let mut cursor = 0usize;
178 let mut out = Vec::with_capacity(expected_len);
179 while out.len() < expected_len {
180 out.push(decode_varint_u32(raw, &mut cursor)?);
181 }
182 if cursor != raw.len() {
183 return None;
184 }
185 Some(out)
186}
187
188fn take_slice<'a>(raw: &'a [u8], cursor: &mut usize, len: usize) -> Option<&'a [u8]> {
189 let end = cursor.checked_add(len)?;
190 let slice = raw.get(*cursor..end)?;
191 *cursor = end;
192 Some(slice)
193}
194
195fn bytes_for_bits(bit_len: usize) -> Option<usize> {
196 bit_len.checked_add(7).map(|value| value / 8)
197}
198
199fn encode_varint_usize(value: usize, out: &mut Vec<u8>) {
200 encode_varint_u64(u64::try_from(value).unwrap_or(u64::MAX), out);
201}
202
203fn encode_varint_u32(value: u32, out: &mut Vec<u8>) {
204 encode_varint_u64(u64::from(value), out);
205}
206
207fn encode_varint_u64(mut value: u64, out: &mut Vec<u8>) {
208 while value >= 0x80 {
209 let lower = u8::try_from(value & 0x7f).unwrap_or(0);
210 out.push(lower | 0x80);
211 value >>= 7;
212 }
213 let final_byte = u8::try_from(value & 0x7f).unwrap_or(0);
214 out.push(final_byte);
215}
216
217fn decode_varint_usize(raw: &[u8], cursor: &mut usize) -> Option<usize> {
218 let value = decode_varint_u64(raw, cursor)?;
219 usize::try_from(value).ok()
220}
221
222fn decode_varint_u32(raw: &[u8], cursor: &mut usize) -> Option<u32> {
223 let value = decode_varint_u64(raw, cursor)?;
224 u32::try_from(value).ok()
225}
226
227fn decode_varint_u64(raw: &[u8], cursor: &mut usize) -> Option<u64> {
228 let mut shift = 0_u32;
229 let mut value = 0_u64;
230
231 loop {
232 let byte = *raw.get(*cursor)?;
233 *cursor += 1;
234 let payload = u64::from(byte & 0x7f);
235 let shifted = payload.checked_shl(shift)?;
236 value = value.checked_add(shifted)?;
237 if (byte & 0x80) == 0 {
238 return Some(value);
239 }
240 if shift >= 63 {
241 return None;
242 }
243 shift += 7;
244 }
245}
246
247fn encode_base64_url_no_pad(bytes: &[u8]) -> String {
248 let mut out = String::with_capacity((bytes.len() * 4).div_ceil(3));
249 let mut idx = 0usize;
250
251 while idx + 3 <= bytes.len() {
252 let b0 = bytes[idx];
253 let b1 = bytes[idx + 1];
254 let b2 = bytes[idx + 2];
255 out.push(char::from(BASE64_URL[usize::from(b0 >> 2)]));
256 out.push(char::from(
257 BASE64_URL[usize::from(((b0 & 0b0000_0011) << 4) | (b1 >> 4))],
258 ));
259 out.push(char::from(
260 BASE64_URL[usize::from(((b1 & 0b0000_1111) << 2) | (b2 >> 6))],
261 ));
262 out.push(char::from(BASE64_URL[usize::from(b2 & 0b0011_1111)]));
263 idx += 3;
264 }
265
266 let remainder = bytes.len() - idx;
267 if remainder == 1 {
268 let b0 = bytes[idx];
269 out.push(char::from(BASE64_URL[usize::from(b0 >> 2)]));
270 out.push(char::from(BASE64_URL[usize::from((b0 & 0b0000_0011) << 4)]));
271 } else if remainder == 2 {
272 let b0 = bytes[idx];
273 let b1 = bytes[idx + 1];
274 out.push(char::from(BASE64_URL[usize::from(b0 >> 2)]));
275 out.push(char::from(
276 BASE64_URL[usize::from(((b0 & 0b0000_0011) << 4) | (b1 >> 4))],
277 ));
278 out.push(char::from(BASE64_URL[usize::from((b1 & 0b0000_1111) << 2)]));
279 }
280
281 out
282}
283
284fn decode_base64_url_no_pad(raw: &str) -> Option<Vec<u8>> {
285 let input = raw.as_bytes();
286 if input.len() % 4 == 1 {
287 return None;
288 }
289
290 let mut out = Vec::with_capacity(input.len() * 3 / 4 + 2);
291 let mut cursor = 0usize;
292
293 while cursor + 4 <= input.len() {
294 let a = decode_base64_url_digit(*input.get(cursor)?)?;
295 let b = decode_base64_url_digit(*input.get(cursor + 1)?)?;
296 let c = decode_base64_url_digit(*input.get(cursor + 2)?)?;
297 let d = decode_base64_url_digit(*input.get(cursor + 3)?)?;
298 out.push((a << 2) | (b >> 4));
299 out.push(((b & 0b0000_1111) << 4) | (c >> 2));
300 out.push(((c & 0b0000_0011) << 6) | d);
301 cursor += 4;
302 }
303
304 let remainder = input.len() - cursor;
305 if remainder == 2 {
306 let a = decode_base64_url_digit(*input.get(cursor)?)?;
307 let b = decode_base64_url_digit(*input.get(cursor + 1)?)?;
308 out.push((a << 2) | (b >> 4));
309 } else if remainder == 3 {
310 let a = decode_base64_url_digit(*input.get(cursor)?)?;
311 let b = decode_base64_url_digit(*input.get(cursor + 1)?)?;
312 let c = decode_base64_url_digit(*input.get(cursor + 2)?)?;
313 out.push((a << 2) | (b >> 4));
314 out.push(((b & 0b0000_1111) << 4) | (c >> 2));
315 } else if remainder != 0 {
316 return None;
317 }
318
319 Some(out)
320}
321
322const fn decode_base64_url_digit(raw: u8) -> Option<u8> {
323 match raw {
324 b'A'..=b'Z' => Some(raw - b'A'),
325 b'a'..=b'z' => Some(raw - b'a' + 26),
326 b'0'..=b'9' => Some(raw - b'0' + 52),
327 b'-' => Some(62),
328 b'_' => Some(63),
329 _ => None,
330 }
331}
332
333fn encode_hex(bytes: &[u8]) -> String {
334 const HEX: &[u8; 16] = b"0123456789abcdef";
335 let mut out = String::with_capacity(bytes.len() * 2);
336 for byte in bytes {
337 out.push(HEX[(byte >> 4) as usize] as char);
338 out.push(HEX[(byte & 0x0f) as usize] as char);
339 }
340 out
341}
342
343fn decode_hex(raw: &str) -> Option<Vec<u8>> {
344 if !raw.len().is_multiple_of(2) {
345 return None;
346 }
347
348 let mut out = Vec::with_capacity(raw.len() / 2);
349 let chars: Vec<char> = raw.chars().collect();
350 let mut idx = 0;
351 while idx < chars.len() {
352 let hi = decode_hex_nibble(chars[idx])?;
353 let lo = decode_hex_nibble(chars[idx + 1])?;
354 out.push((hi << 4) | lo);
355 idx += 2;
356 }
357
358 Some(out)
359}
360
361const fn decode_hex_nibble(c: char) -> Option<u8> {
362 match c {
363 '0'..='9' => Some((c as u8) - b'0'),
364 'a'..='f' => Some((c as u8) - b'a' + 10),
365 'A'..='F' => Some((c as u8) - b'A' + 10),
366 _ => None,
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn stamp_text_roundtrip_v3() {
376 let mut stamp = Stamp::seed();
377 stamp.event();
378 stamp.event();
379
380 let encoded = stamp_to_text(&stamp);
381 assert!(encoded.starts_with(ITC_TEXT_PREFIX));
382 let decoded = stamp_from_text(&encoded).expect("parse encoded stamp");
383 assert_eq!(decoded, stamp);
384 }
385
386 #[test]
387 fn stamp_text_roundtrip_v1_legacy_decode() {
388 let mut stamp = Stamp::seed();
389 for _ in 0..8 {
390 stamp.event();
391 }
392
393 let legacy = format!(
394 "{LEGACY_ITC_TEXT_PREFIX}{}",
395 encode_hex(&stamp.serialize_compact())
396 );
397 let decoded = stamp_from_text(&legacy).expect("parse legacy stamp");
398 assert_eq!(decoded, stamp);
399 }
400
401 #[test]
402 fn sparse_payload_smaller_than_legacy_hex() {
403 let mut stamp = Stamp::seed();
404 for _ in 0..1024 {
405 stamp.event();
406 }
407
408 let compact = stamp.serialize_compact();
409 let sparse = compact_to_sparse_payload(&compact).expect("sparse payload");
410 let legacy_text = encode_hex(&compact);
411 let sparse_text = encode_base64_url_no_pad(&sparse);
412
413 assert!(sparse_text.len() < legacy_text.len());
414 }
415
416 #[test]
417 fn decode_rejects_malformed_v3_payloads() {
418 assert!(stamp_from_text("itc:v3:^").is_none());
419
420 let encoded = encode_base64_url_no_pad(&[99, 0]);
422 assert!(stamp_from_text(&format!("itc:v3:{encoded}")).is_none());
423 }
424
425 #[test]
426 fn sparse_compact_roundtrip_preserves_bytes() {
427 let mut stamp = Stamp::seed();
428 for _ in 0..64 {
429 stamp.event();
430 }
431
432 let compact = stamp.serialize_compact();
433 let sparse = compact_to_sparse_payload(&compact).expect("to sparse");
434 let reconstructed = sparse_payload_to_compact(&sparse).expect("to compact");
435 assert_eq!(reconstructed, compact);
436 }
437
438 #[test]
439 fn sparse_decode_rejects_duplicate_indices() {
440 let mut sparse = Vec::new();
441 sparse.push(SPARSE_ITC_WIRE_VERSION);
442 encode_varint_usize(2, &mut sparse); sparse.push(0b0100_0000); encode_varint_usize(1, &mut sparse); sparse.push(0); encode_varint_usize(2, &mut sparse); encode_varint_usize(0, &mut sparse); encode_varint_u32(1, &mut sparse);
449 encode_varint_usize(0, &mut sparse); encode_varint_u32(2, &mut sparse);
451
452 assert!(sparse_payload_to_compact(&sparse).is_none());
453 }
454
455 #[test]
456 fn sparse_decode_rejects_out_of_range_index() {
457 let mut sparse = Vec::new();
458 sparse.push(SPARSE_ITC_WIRE_VERSION);
459 encode_varint_usize(2, &mut sparse); sparse.push(0b0100_0000); encode_varint_usize(1, &mut sparse); sparse.push(0); encode_varint_usize(1, &mut sparse); encode_varint_usize(1, &mut sparse); encode_varint_u32(3, &mut sparse);
466
467 assert!(sparse_payload_to_compact(&sparse).is_none());
468 }
469
470 #[test]
471 fn decode_rejects_bad_input() {
472 assert!(stamp_from_text("itc:v1:not-hex").is_none());
473 assert!(stamp_from_text("itc:v1:abc").is_none());
474 assert!(stamp_from_text("itc:v3:abcde").is_none());
475 assert!(stamp_from_text("itc:AQ").is_none());
476 }
477}