1pub mod constants;
9
10use anyhow::{bail, Context, Result};
11use constants::*;
12use std::collections::{HashMap, HashSet};
13
14use crate::orchestrator::BLOB_SIZE_C_BIT;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use crate::piv::PivBackend;
18
19#[derive(Debug, Clone)]
25pub struct Object {
26 pub(crate) index: u8,
28 pub(crate) object_size: usize,
30
31 pub(crate) yblob_magic: u32,
33 pub(crate) object_count: u8,
34 pub(crate) store_key_slot: u8,
35 pub(crate) age: u32,
37
38 pub(crate) chunk_pos: u8,
40 pub(crate) next_chunk: u8,
41
42 pub blob_mtime: u32,
45 pub blob_size: u32,
47 pub blob_key_slot: u8,
49 pub blob_plain_size: u32,
51 pub is_compressed: bool,
53 pub blob_name: String,
54
55 pub(crate) payload: Vec<u8>,
57
58 pub(crate) dirty: bool,
60}
61
62impl Object {
63 pub fn from_bytes(index: u8, data: &[u8]) -> Result<Self> {
65 let object_size = data.len();
66 if !(OBJECT_MIN_SIZE..=OBJECT_MAX_SIZE).contains(&object_size) {
67 bail!(
68 "object {index}: invalid size {object_size} \
69 (expected {OBJECT_MIN_SIZE}..={OBJECT_MAX_SIZE})"
70 );
71 }
72
73 let magic = read_u32_le(data, MAGIC_O)?;
74 if magic != YBLOB_MAGIC {
75 bail!("object {index}: bad magic 0x{magic:08x} (expected 0x{YBLOB_MAGIC:08x})");
76 }
77
78 let object_count = data[OBJECT_COUNT_O];
79 let store_key_slot = data[STORE_KEY_SLOT_O];
80 let age = read_u24_le(data, OBJECT_AGE_O)?;
81
82 if age == 0 {
83 return Ok(Self {
85 index,
86 object_size,
87 yblob_magic: magic,
88 object_count,
89 store_key_slot,
90 age: 0,
91 chunk_pos: 0,
92 next_chunk: 0,
93 blob_mtime: 0,
94 blob_size: 0,
95 blob_key_slot: 0,
96 blob_plain_size: 0,
97 is_compressed: false,
98 blob_name: String::new(),
99 payload: Vec::new(),
100 dirty: false,
101 });
102 }
103
104 let chunk_pos = data[CHUNK_POS_O];
105 let next_chunk = data[NEXT_CHUNK_O];
106
107 let (
108 blob_mtime,
109 blob_size,
110 blob_key_slot,
111 blob_plain_size,
112 is_compressed,
113 blob_name,
114 payload_start,
115 ) = if chunk_pos == 0 {
116 let mtime = read_u32_le(data, BLOB_MTIME_O)?;
117 let bsize = read_u24_le(data, BLOB_SIZE_O)?;
118 let bkslot = data[BLOB_KEY_SLOT_O];
119 let raw_plain = read_u24_le(data, BLOB_PLAIN_SIZE_O)?;
120 let compressed = raw_plain & BLOB_SIZE_C_BIT as u32 != 0;
121 let plain = raw_plain & !(BLOB_SIZE_C_BIT as u32);
122 let nlen = data[BLOB_NAME_LEN_O] as usize;
123 if BLOB_NAME_O + nlen > object_size {
124 bail!("object {index}: name length {nlen} overflows object");
125 }
126 let name = std::str::from_utf8(&data[BLOB_NAME_O..BLOB_NAME_O + nlen])
127 .with_context(|| format!("object {index}: blob name is not valid UTF-8"))?
128 .to_owned();
129 (
130 mtime,
131 bsize,
132 bkslot,
133 plain,
134 compressed,
135 name,
136 BLOB_NAME_O + nlen,
137 )
138 } else {
139 (0, 0, 0, 0, false, String::new(), CONTINUATION_PAYLOAD_O)
140 };
141
142 let raw_payload = &data[payload_start..];
147 let payload = raw_payload.to_vec();
148
149 Ok(Self {
150 index,
151 object_size,
152 yblob_magic: magic,
153 object_count,
154 store_key_slot,
155 age,
156 chunk_pos,
157 next_chunk,
158 blob_mtime,
159 blob_size,
160 blob_key_slot,
161 blob_plain_size,
162 is_compressed,
163 blob_name,
164 payload,
165 dirty: false,
166 })
167 }
168
169 pub fn to_bytes(&self) -> Vec<u8> {
177 if self.age == 0 {
178 let mut buf = vec![0u8; OBJECT_MIN_SIZE];
180 write_u32_le(&mut buf, MAGIC_O, self.yblob_magic);
181 buf[OBJECT_COUNT_O] = self.object_count;
182 buf[STORE_KEY_SLOT_O] = self.store_key_slot;
183 return buf;
185 }
186
187 if self.chunk_pos == 0 {
188 let name_bytes = self.blob_name.as_bytes();
190 let total = (BLOB_NAME_O + name_bytes.len() + self.payload.len())
191 .clamp(OBJECT_MIN_SIZE, OBJECT_MAX_SIZE);
192 let mut buf = vec![0u8; total];
193 write_u32_le(&mut buf, MAGIC_O, self.yblob_magic);
194 buf[OBJECT_COUNT_O] = self.object_count;
195 buf[STORE_KEY_SLOT_O] = self.store_key_slot;
196 write_u24_le(&mut buf, OBJECT_AGE_O, self.age);
197 buf[CHUNK_POS_O] = self.chunk_pos;
198 buf[NEXT_CHUNK_O] = self.next_chunk;
199 write_u32_le(&mut buf, BLOB_MTIME_O, self.blob_mtime);
200 write_u24_le(&mut buf, BLOB_SIZE_O, self.blob_size);
201 buf[BLOB_KEY_SLOT_O] = self.blob_key_slot;
202 write_u24_le(
203 &mut buf,
204 BLOB_PLAIN_SIZE_O,
205 self.blob_plain_size | self.c_bit(),
206 );
207 buf[BLOB_NAME_LEN_O] = name_bytes.len() as u8;
208 buf[BLOB_NAME_O..BLOB_NAME_O + name_bytes.len()].copy_from_slice(name_bytes);
209 let payload_start = BLOB_NAME_O + name_bytes.len();
210 let payload_end = (payload_start + self.payload.len()).min(total);
211 buf[payload_start..payload_end]
212 .copy_from_slice(&self.payload[..payload_end - payload_start]);
213 buf
214 } else {
215 let total = (CONTINUATION_PAYLOAD_O + self.payload.len())
217 .clamp(OBJECT_MIN_SIZE, OBJECT_MAX_SIZE);
218 let mut buf = vec![0u8; total];
219 write_u32_le(&mut buf, MAGIC_O, self.yblob_magic);
220 buf[OBJECT_COUNT_O] = self.object_count;
221 buf[STORE_KEY_SLOT_O] = self.store_key_slot;
222 write_u24_le(&mut buf, OBJECT_AGE_O, self.age);
223 buf[CHUNK_POS_O] = self.chunk_pos;
224 buf[NEXT_CHUNK_O] = self.next_chunk;
225 let payload_end = (CONTINUATION_PAYLOAD_O + self.payload.len()).min(total);
226 buf[CONTINUATION_PAYLOAD_O..payload_end]
227 .copy_from_slice(&self.payload[..payload_end - CONTINUATION_PAYLOAD_O]);
228 buf
229 }
230 }
231
232 pub fn reset(&mut self) {
238 let index = self.index;
239 let object_count = self.object_count;
240 let store_key_slot = self.store_key_slot;
241 *self = Self {
242 index,
243 object_size: OBJECT_MIN_SIZE, yblob_magic: crate::store::constants::YBLOB_MAGIC,
245 object_count,
246 store_key_slot,
247 age: 0,
248 chunk_pos: 0,
249 next_chunk: 0,
250 blob_mtime: 0,
251 blob_size: 0,
252 blob_key_slot: 0,
253 blob_plain_size: 0,
254 is_compressed: false,
255 blob_name: String::new(),
256 payload: Vec::new(),
257 dirty: true,
258 };
259 }
260
261 pub fn head_payload_capacity(_object_size: usize, name_len: usize) -> usize {
266 OBJECT_MAX_SIZE.saturating_sub(BLOB_NAME_O + name_len)
267 }
268
269 pub fn continuation_payload_capacity(_object_size: usize) -> usize {
274 OBJECT_MAX_SIZE.saturating_sub(CONTINUATION_PAYLOAD_O)
275 }
276
277 fn c_bit(&self) -> u32 {
280 BLOB_SIZE_C_BIT as u32 * self.is_compressed as u32
281 }
282
283 pub fn is_empty(&self) -> bool {
284 self.age == 0
285 }
286
287 pub fn is_head(&self) -> bool {
288 self.age != 0 && self.chunk_pos == 0
289 }
290
291 pub fn is_encrypted(&self) -> bool {
292 self.blob_key_slot != 0
293 }
294
295 pub fn set_payload(&mut self, payload: Vec<u8>) {
300 self.payload = payload;
301 }
302
303 pub fn index(&self) -> u8 {
304 self.index
305 }
306
307 pub fn age(&self) -> u32 {
308 self.age
309 }
310
311 pub fn chunk_pos(&self) -> u8 {
312 self.chunk_pos
313 }
314
315 pub fn next_chunk(&self) -> u8 {
316 self.next_chunk
317 }
318
319 pub fn payload(&self) -> &[u8] {
321 &self.payload
322 }
323
324 pub fn payload_len(&self) -> usize {
326 self.payload.len()
327 }
328
329 pub fn object_size(&self) -> usize {
330 self.object_size
331 }
332}
333
334pub struct ObjectParams {
340 pub index: u8,
341 pub age: u32,
342 pub chunk_pos: u8,
343 pub next_chunk: u8,
344}
345
346pub struct Store {
352 pub reader: String,
353 pub object_size: usize,
354 pub object_count: u8,
355 pub store_key_slot: u8,
356 pub objects: Vec<Object>,
357 pub store_age: u32,
359}
360
361impl Store {
362 pub fn from_device(reader: &str, piv: &dyn PivBackend) -> Result<Self> {
364 let first_id = OBJECT_ID_ZERO;
365 let raw = piv
366 .read_object(reader, first_id)
367 .with_context(|| format!("reading object 0x{first_id:06x}"))?;
368
369 let first =
370 Object::from_bytes(0, &raw).with_context(|| "parsing object 0 (store header)")?;
371
372 let object_count = first.object_count;
373 let object_size = raw.len();
374 let store_key_slot = first.store_key_slot;
375
376 let mut objects = vec![first];
377 for i in 1..object_count {
378 let id = OBJECT_ID_ZERO + i as u32;
379 let raw = piv
380 .read_object(reader, id)
381 .with_context(|| format!("reading object 0x{id:06x}"))?;
382 let obj = Object::from_bytes(i, &raw).with_context(|| format!("parsing object {i}"))?;
383 objects.push(obj);
384 }
385
386 let store_age = objects.iter().map(|o| o.age).max().unwrap_or(0);
387
388 Ok(Self {
389 reader: reader.to_owned(),
390 object_size,
391 object_count,
392 store_key_slot,
393 objects,
394 store_age,
395 })
396 }
397
398 pub fn format(
404 reader: &str,
405 piv: &dyn PivBackend,
406 object_count: u8,
407 store_key_slot: u8,
408 management_key: Option<&str>,
409 pin: Option<&str>,
410 ) -> Result<Self> {
411 let mut store = Self {
413 reader: reader.to_owned(),
414 object_size: OBJECT_MIN_SIZE,
415 object_count,
416 store_key_slot,
417 objects: Vec::with_capacity(object_count as usize),
418 store_age: 0,
419 };
420 for i in 0..object_count {
421 store.objects.push(store.make_object(ObjectParams {
423 index: i,
424 age: 0,
425 chunk_pos: 0,
426 next_chunk: 0,
427 }));
428 }
429 store.sync(piv, management_key, pin)?;
430 Ok(store)
431 }
432
433 pub fn sanitize(&mut self) {
435 let mut seen: HashMap<String, (u8, u32)> = HashMap::new();
441 let mut to_reset: Vec<u8> = Vec::new();
442
443 for obj in self.objects.iter().filter(|o| o.is_head()) {
444 if let Some(&(prev_idx, prev_age)) = seen.get(&obj.blob_name) {
445 if obj.age > prev_age {
447 to_reset.push(prev_idx);
448 seen.insert(obj.blob_name.clone(), (obj.index, obj.age));
449 } else {
450 to_reset.push(obj.index);
451 }
452 } else {
453 seen.insert(obj.blob_name.clone(), (obj.index, obj.age));
454 }
455 }
456
457 let mut reachable: HashSet<u8> = HashSet::new();
459 for (head_idx, _) in seen.values() {
460 let mut idx = *head_idx;
461 loop {
462 reachable.insert(idx);
463 let next = self.objects[idx as usize].next_chunk;
464 if next == idx {
465 break;
466 }
467 idx = next;
468 }
469 }
470
471 for obj in self.objects.iter().filter(|o| !o.is_empty()) {
473 if !reachable.contains(&obj.index) {
474 to_reset.push(obj.index);
475 }
476 }
477
478 for idx in to_reset {
479 self.objects[idx as usize].reset();
480 }
481 }
482
483 pub fn sync(
485 &mut self,
486 piv: &dyn PivBackend,
487 management_key: Option<&str>,
488 pin: Option<&str>,
489 ) -> Result<()> {
490 use indicatif::{ProgressBar, ProgressStyle};
491
492 let dirty: Vec<u8> = self
493 .objects
494 .iter()
495 .filter(|o| o.dirty)
496 .map(|o| o.index)
497 .collect();
498
499 let pb = ProgressBar::new(dirty.len() as u64);
500 pb.set_style(
501 ProgressStyle::with_template("Writing objects: [{bar:30}] {pos}/{len}")
502 .unwrap()
503 .progress_chars("=>-"),
504 );
505
506 for idx in &dirty {
507 let obj = &mut self.objects[*idx as usize];
508 let id = OBJECT_ID_ZERO + obj.index as u32;
509 let data = obj.to_bytes();
510 piv.write_object(&self.reader, id, &data, management_key, pin)
511 .with_context(|| format!("writing object 0x{id:06x}"))?;
512 obj.dirty = false;
513 pb.inc(1);
514 }
515 pb.finish_and_clear();
516 Ok(())
517 }
518
519 pub fn alloc_free(&self) -> Option<u8> {
521 self.objects.iter().find(|o| o.is_empty()).map(|o| o.index)
522 }
523
524 pub fn free_count(&self) -> usize {
526 self.objects.iter().filter(|o| o.is_empty()).count()
527 }
528
529 pub fn find_head(&self, name: &str) -> Option<&Object> {
531 self.objects
532 .iter()
533 .find(|o| o.is_head() && o.blob_name == name)
534 }
535
536 pub fn chunk_chain(&self, head_index: u8) -> Vec<u8> {
542 let mut chain = vec![head_index];
543 let mut seen: HashSet<u8> = HashSet::from([head_index]);
544 let mut idx = head_index;
545 loop {
546 let next = self.objects[idx as usize].next_chunk;
547 if next == idx {
548 break;
549 }
550 if seen.contains(&next) {
551 break;
553 }
554 seen.insert(next);
555 chain.push(next);
556 idx = next;
557 }
558 chain
559 }
560
561 pub fn now_unix() -> u32 {
563 SystemTime::now()
564 .duration_since(UNIX_EPOCH)
565 .map(|d| d.as_secs() as u32)
566 .unwrap_or(0)
567 }
568
569 pub fn next_age(&mut self) -> u32 {
571 self.store_age += 1;
572 self.store_age
573 }
574
575 pub fn make_object(&self, p: ObjectParams) -> Object {
581 Object {
582 index: p.index,
583 object_size: self.object_size,
584 yblob_magic: crate::store::constants::YBLOB_MAGIC,
585 object_count: self.object_count,
586 store_key_slot: self.store_key_slot,
587 age: p.age,
588 chunk_pos: p.chunk_pos,
589 next_chunk: p.next_chunk,
590 blob_mtime: 0,
591 blob_size: 0,
592 blob_key_slot: 0,
593 blob_plain_size: 0,
594 is_compressed: false,
595 blob_name: String::new(),
596 payload: Vec::new(),
597 dirty: true,
598 }
599 }
600}
601
602pub(crate) fn read_u32_le(buf: &[u8], offset: usize) -> Result<u32> {
607 let end = offset + 4;
608 if end > buf.len() {
609 bail!(
610 "read_u32_le: offset {offset}+4 out of bounds (buf len {})",
611 buf.len()
612 );
613 }
614 Ok(u32::from_le_bytes(buf[offset..end].try_into().unwrap()))
615}
616
617pub(crate) fn read_u24_le(buf: &[u8], offset: usize) -> Result<u32> {
618 let end = offset + 3;
619 if end > buf.len() {
620 bail!(
621 "read_u24_le: offset {offset}+3 out of bounds (buf len {})",
622 buf.len()
623 );
624 }
625 let b = &buf[offset..end];
626 Ok(b[0] as u32 | ((b[1] as u32) << 8) | ((b[2] as u32) << 16))
627}
628
629pub(crate) fn write_u32_le(buf: &mut [u8], offset: usize, v: u32) {
630 buf[offset..offset + 4].copy_from_slice(&v.to_le_bytes());
631}
632
633pub(crate) fn write_u24_le(buf: &mut [u8], offset: usize, v: u32) {
634 buf[offset] = (v & 0xff) as u8;
635 buf[offset + 1] = ((v >> 8) & 0xff) as u8;
636 buf[offset + 2] = ((v >> 16) & 0xff) as u8;
637}
638
639#[cfg(test)]
644mod tests;