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