1#![forbid(unsafe_code)]
12#![warn(clippy::pedantic)]
13
14use core::fmt;
15
16const BLAKE3_LEN: usize = 32;
17const ORDINAL_LEN: usize = 8;
18const SLAB_ID_LEN: usize = ORDINAL_LEN + BLAKE3_LEN;
19
20pub const MANIFEST_MAGIC: [u8; 4] = *b"LMFS";
21pub const SLAB_MAGIC: [u8; 4] = *b"LIM1";
22
23pub const MANIFEST_HEADER_LEN: usize = 16;
24
25const BASE32_LOWER: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
27
28fn encode_base32_lower_no_pad(input: &[u8]) -> String {
29 let capacity = (input.len() * 8).div_ceil(5);
30 let mut out = String::with_capacity(capacity);
31 let mut buffer: u64 = 0;
32 let mut bits: u32 = 0;
33 for &byte in input {
34 buffer = (buffer << 8) | u64::from(byte);
35 bits += 8;
36 while bits >= 5 {
37 bits -= 5;
38 let shift = buffer >> bits;
39 let idx = (shift & 0x1F) as usize;
40 out.push(BASE32_LOWER[idx] as char);
41 }
42 }
43 if bits > 0 {
44 let shift = 5 - bits;
45 let idx = ((buffer << shift) & 0x1F) as usize;
46 out.push(BASE32_LOWER[idx] as char);
47 }
48 out
49}
50
51fn decode_base32_lower_no_pad(input: &str) -> Option<Vec<u8>> {
52 let mut out = Vec::with_capacity(input.len() * 5 / 8 + 1);
53 let mut buffer: u64 = 0;
54 let mut bits: u32 = 0;
55 for ch in input.chars() {
56 let val = match ch {
57 'a'..='z' => u32::from(ch) - u32::from('a'),
58 '2'..='7' => u32::from(ch) - u32::from('2') + 26,
59 _ => return None,
60 };
61 buffer = (buffer << 5) | u64::from(val);
62 bits += 5;
63 if bits >= 8 {
64 bits -= 8;
65 let shift = buffer >> bits;
66 out.push((shift & 0xFF) as u8);
67 }
68 }
69 if bits >= 5 || (buffer & ((1 << bits) - 1) != 0) {
70 return None;
71 }
72 Some(out)
73}
74
75#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
81pub struct DropId([u8; BLAKE3_LEN]);
82
83impl DropId {
84 #[must_use]
85 pub const fn from_bytes(bytes: [u8; BLAKE3_LEN]) -> Self {
86 Self(bytes)
87 }
88
89 #[must_use]
90 pub fn as_bytes(&self) -> &[u8; BLAKE3_LEN] {
91 &self.0
92 }
93
94 #[must_use]
98 pub fn parse_text(s: &str) -> Option<Self> {
99 let rest = s.strip_prefix("b3:")?;
100 let bytes = decode_base32_lower_no_pad(rest)?;
101 if bytes.len() != BLAKE3_LEN {
102 return None;
103 }
104 let mut arr = [0u8; BLAKE3_LEN];
105 arr.copy_from_slice(&bytes);
106 Some(Self(arr))
107 }
108
109 #[must_use]
110 pub fn to_text(self) -> String {
111 let mut s = String::with_capacity(3 + (BLAKE3_LEN * 8).div_ceil(5));
112 s.push_str("b3:");
113 s.push_str(&encode_base32_lower_no_pad(&self.0));
114 s
115 }
116}
117
118impl fmt::Display for DropId {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 f.write_str(&self.to_text())
121 }
122}
123
124impl fmt::Debug for DropId {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 write!(f, "DropId({self})")
127 }
128}
129
130#[derive(Copy, Clone, Eq, PartialEq, Hash)]
135pub struct SlabId {
136 pub ordinal: u64,
137 pub hash: [u8; BLAKE3_LEN],
138}
139
140impl SlabId {
141 #[must_use]
142 pub const fn new(ordinal: u64, hash: [u8; BLAKE3_LEN]) -> Self {
143 Self { ordinal, hash }
144 }
145
146 #[must_use]
147 pub fn from_bytes(bytes: &[u8; SLAB_ID_LEN]) -> Self {
148 let mut ordinal_bytes = [0u8; ORDINAL_LEN];
149 ordinal_bytes.copy_from_slice(&bytes[..ORDINAL_LEN]);
150 let mut hash = [0u8; BLAKE3_LEN];
151 hash.copy_from_slice(&bytes[ORDINAL_LEN..]);
152 Self {
153 ordinal: u64::from_le_bytes(ordinal_bytes),
154 hash,
155 }
156 }
157
158 #[must_use]
159 pub fn to_bytes(self) -> [u8; SLAB_ID_LEN] {
160 let mut out = [0u8; SLAB_ID_LEN];
161 out[..ORDINAL_LEN].copy_from_slice(&self.ordinal.to_le_bytes());
162 out[ORDINAL_LEN..].copy_from_slice(&self.hash);
163 out
164 }
165}
166
167impl fmt::Debug for SlabId {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.debug_struct("SlabId")
170 .field("ordinal", &self.ordinal)
171 .field("hash", &encode_base32_lower_no_pad(&self.hash))
172 .finish()
173 }
174}
175
176#[derive(Copy, Clone, Eq, PartialEq, Hash)]
183pub struct ManifestRoot([u8; BLAKE3_LEN]);
184
185impl ManifestRoot {
186 #[must_use]
187 pub const fn from_bytes(bytes: [u8; BLAKE3_LEN]) -> Self {
188 Self(bytes)
189 }
190
191 #[must_use]
192 pub fn as_bytes(&self) -> &[u8; BLAKE3_LEN] {
193 &self.0
194 }
195
196 #[must_use]
197 pub fn to_text(self) -> String {
198 let mut s = String::with_capacity(3 + (BLAKE3_LEN * 8).div_ceil(5));
199 s.push_str("b3:");
200 s.push_str(&encode_base32_lower_no_pad(&self.0));
201 s
202 }
203}
204
205impl fmt::Display for ManifestRoot {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 f.write_str(&self.to_text())
208 }
209}
210
211impl fmt::Debug for ManifestRoot {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 write!(f, "ManifestRoot({self})")
214 }
215}
216
217#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
222#[repr(u8)]
223pub enum Tier {
224 Epilimnion = 0x00,
225 Metalimnion = 0x01,
226 Hypolimnion = 0x02,
227}
228
229impl Tier {
230 #[must_use]
231 pub const fn from_byte(byte: u8) -> Option<Self> {
232 match byte {
233 0x00 => Some(Self::Epilimnion),
234 0x01 => Some(Self::Metalimnion),
235 0x02 => Some(Self::Hypolimnion),
236 _ => None,
237 }
238 }
239
240 #[must_use]
241 pub const fn to_byte(self) -> u8 {
242 self as u8
243 }
244}
245
246#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
253pub struct Representation {
254 pub codec: u8,
255 pub aead: u8,
256 pub ec: u8,
257}
258
259impl Representation {
260 pub const STORE_PLAINTEXT: Self = Self {
261 codec: 0x00,
262 aead: 0x00,
263 ec: 0x00,
264 };
265
266 #[must_use]
267 pub const fn new(codec: u8, aead: u8, ec: u8) -> Self {
268 Self { codec, aead, ec }
269 }
270
271 #[must_use]
272 pub fn is_plaintext(self) -> bool {
273 self.aead == 0x00
274 }
275
276 #[must_use]
277 pub fn has_no_ec(self) -> bool {
278 self.ec == 0x00
279 }
280
281 #[must_use]
282 pub fn to_bytes(self) -> [u8; 3] {
283 [self.codec, self.aead, self.ec]
284 }
285
286 #[must_use]
287 pub const fn from_bytes(bytes: [u8; 3]) -> Self {
288 Self {
289 codec: bytes[0],
290 aead: bytes[1],
291 ec: bytes[2],
292 }
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn base32_roundtrip_zero_bytes() {
302 let input = [0u8; 32];
303 let encoded = encode_base32_lower_no_pad(&input);
304 assert_eq!(encoded.len(), (32u32 * 8).div_ceil(5) as usize);
305 let decoded = decode_base32_lower_no_pad(&encoded).expect("roundtrip");
306 assert_eq!(decoded, input.to_vec());
307 }
308
309 #[test]
310 fn base32_roundtrip_mixed_bytes() {
311 let input: Vec<u8> = (0..32u8).collect();
312 let encoded = encode_base32_lower_no_pad(&input);
313 let decoded = decode_base32_lower_no_pad(&encoded).expect("roundtrip");
314 assert_eq!(decoded, input);
315 }
316
317 #[test]
318 fn base32_rejects_invalid_chars() {
319 assert!(decode_base32_lower_no_pad("0").is_none());
320 assert!(decode_base32_lower_no_pad("1").is_none());
321 assert!(decode_base32_lower_no_pad("8").is_none());
322 assert!(decode_base32_lower_no_pad("!").is_none());
323 }
324
325 #[test]
326 fn base32_known_vector() {
327 assert_eq!(encode_base32_lower_no_pad(b"f"), "my");
329 assert_eq!(encode_base32_lower_no_pad(b"fo"), "mzxq");
330 assert_eq!(encode_base32_lower_no_pad(b"foo"), "mzxw6");
331 assert_eq!(encode_base32_lower_no_pad(b"foob"), "mzxw6yq");
332 assert_eq!(encode_base32_lower_no_pad(b"fooba"), "mzxw6ytb");
333 assert_eq!(encode_base32_lower_no_pad(b"foobar"), "mzxw6ytboi");
334 }
335
336 #[test]
337 fn drop_id_text_roundtrip() {
338 let bytes = [
339 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
340 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
341 0x1c, 0x1d, 0x1e, 0x1f,
342 ];
343 let id = DropId::from_bytes(bytes);
344 let text = id.to_text();
345 assert!(text.starts_with("b3:"));
346 let parsed = DropId::parse_text(&text).expect("roundtrip");
347 assert_eq!(parsed, id);
348 }
349
350 #[test]
351 fn drop_id_parse_rejects_wrong_prefix() {
352 assert!(DropId::parse_text("zb3:aaaa").is_none());
353 assert!(DropId::parse_text("aaaa").is_none());
354 }
355
356 #[test]
357 fn drop_id_parse_rejects_wrong_length() {
358 assert!(DropId::parse_text("b3:my").is_none());
359 }
360
361 #[test]
362 fn slab_id_roundtrip() {
363 let id = SlabId::new(0x0123_4567_89ab_cdef, [0xaa; 32]);
364 let bytes = id.to_bytes();
365 assert_eq!(bytes.len(), SLAB_ID_LEN);
366 let back = SlabId::from_bytes(&bytes);
367 assert_eq!(back, id);
368 assert_eq!(back.ordinal, 0x0123_4567_89ab_cdef);
369 assert_eq!(back.hash, [0xaa; 32]);
370 }
371
372 #[test]
373 fn slab_id_ordinal_is_little_endian() {
374 let id = SlabId::new(1, [0; 32]);
375 let bytes = id.to_bytes();
376 assert_eq!(bytes[0], 0x01);
377 for byte in &bytes[1..8] {
378 assert_eq!(*byte, 0);
379 }
380 }
381
382 #[test]
383 fn manifest_root_display_matches_drop_id_format() {
384 let root = ManifestRoot::from_bytes([0x42; 32]);
385 let drop_id = DropId::from_bytes([0x42; 32]);
386 assert_eq!(root.to_text(), drop_id.to_text());
387 }
388
389 #[test]
390 fn tier_roundtrip() {
391 assert_eq!(Tier::from_byte(0x00), Some(Tier::Epilimnion));
392 assert_eq!(Tier::from_byte(0x01), Some(Tier::Metalimnion));
393 assert_eq!(Tier::from_byte(0x02), Some(Tier::Hypolimnion));
394 assert_eq!(Tier::from_byte(0x03), None);
395 assert_eq!(Tier::Epilimnion.to_byte(), 0x00);
396 assert_eq!(Tier::Metalimnion.to_byte(), 0x01);
397 assert_eq!(Tier::Hypolimnion.to_byte(), 0x02);
398 }
399
400 #[test]
401 fn representation_store_plaintext_constant() {
402 let r = Representation::STORE_PLAINTEXT;
403 assert!(r.is_plaintext());
404 assert!(r.has_no_ec());
405 assert_eq!(r.codec, 0x00);
406 assert_eq!(r.aead, 0x00);
407 assert_eq!(r.ec, 0x00);
408 }
409
410 #[test]
411 fn representation_byte_roundtrip() {
412 let r = Representation::new(0x01, 0x02, 0x03);
413 let bytes = r.to_bytes();
414 assert_eq!(bytes, [0x01, 0x02, 0x03]);
415 let back = Representation::from_bytes(bytes);
416 assert_eq!(back, r);
417 }
418
419 #[test]
420 fn representation_predicate_methods() {
421 let plaintext = Representation::new(0x01, 0x00, 0x05);
422 assert!(plaintext.is_plaintext());
423 assert!(!plaintext.has_no_ec());
424
425 let sealed = Representation::new(0x01, 0x02, 0x00);
426 assert!(!sealed.is_plaintext());
427 assert!(sealed.has_no_ec());
428 }
429
430 #[test]
431 fn magic_constants_match_spec() {
432 assert_eq!(&MANIFEST_MAGIC, b"LMFS");
433 assert_eq!(&SLAB_MAGIC, b"LIM1");
434 assert_eq!(MANIFEST_HEADER_LEN, 16);
435 }
436}