1use std::{
4 fs::{File, OpenOptions},
5 io::{self, Read, Seek, SeekFrom, Write},
6 path::{Path, PathBuf},
7};
8
9use hyphae_core::DISK_FORMAT_VERSION;
10use thiserror::Error;
11
12use crate::{
13 CommitReceipt, MAX_KEY_BYTES, MaterializedIndexError, index::MaterializedIndex,
14 log::MAX_OPERATION_BYTES,
15};
16
17const MAGIC: [u8; 8] = *b"HYSNAP01";
18const HEADER_LENGTH: usize = 112;
19const HEADER_LENGTH_U64: u64 = 112;
20const CHECKSUM_PREFIX_LENGTH: usize = 76;
21const DIGEST_PREFIX_LENGTH: usize = 80;
22const ENTRY_HEADER_LENGTH: usize = 12;
23const ENTRY_HEADER_LENGTH_U64: u64 = 12;
24const RECEIPT_LENGTH: usize = 88;
25const RECEIPT_LENGTH_U64: u64 = 88;
26const COPY_BUFFER_LENGTH: usize = 64 * 1024;
27const COPY_BUFFER_LENGTH_U64: u64 = 64 * 1024;
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct SnapshotInfo {
32 pub path: PathBuf,
34 pub checkpoint_sequence: u64,
36 pub checkpoint_digest: Option<[u8; 32]>,
38 pub entry_count: u64,
40 pub receipt_count: u64,
42 pub snapshot_digest: [u8; 32],
44 pub file_bytes: u64,
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct SnapshotReadLimits {
52 pub file_bytes: u64,
54 pub entries: u64,
56 pub decoded_bytes: u64,
58}
59
60impl Default for SnapshotReadLimits {
61 fn default() -> Self {
62 Self {
63 file_bytes: 512 * 1024 * 1024,
64 entries: 1_000_000,
65 decoded_bytes: 256 * 1024 * 1024,
66 }
67 }
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct SnapshotEntry {
73 pub key: Vec<u8>,
75 pub value: Vec<u8>,
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct SnapshotContents {
82 pub info: SnapshotInfo,
84 pub entries: Vec<SnapshotEntry>,
86}
87
88#[derive(Debug, Error)]
90pub enum SnapshotError {
91 #[error(transparent)]
93 Io(#[from] io::Error),
94
95 #[error("materialized index failure during snapshot: {source}")]
97 Index {
98 #[source]
100 source: Box<MaterializedIndexError>,
101 },
102
103 #[error("invalid snapshot: {reason}")]
105 Invalid {
106 reason: &'static str,
108 },
109
110 #[error("unsupported snapshot format {found}; supported format is {supported}")]
112 UnsupportedVersion {
113 found: u16,
115 supported: u16,
117 },
118
119 #[error("snapshot sequence {sequence} already exists for a different commit")]
121 CheckpointConflict {
122 sequence: u64,
124 },
125
126 #[error("snapshot file length {actual} exceeds verification limit {maximum}")]
128 FileLimitExceeded {
129 actual: u64,
131 maximum: u64,
133 },
134
135 #[error("snapshot entry count {actual} exceeds verification limit {maximum}")]
137 EntryLimitExceeded {
138 actual: u64,
140 maximum: u64,
142 },
143
144 #[error("snapshot decoded bytes exceed verification limit {maximum}")]
146 DecodedBytesLimitExceeded {
147 maximum: u64,
149 },
150}
151
152impl From<MaterializedIndexError> for SnapshotError {
153 fn from(source: MaterializedIndexError) -> Self {
154 Self::Index {
155 source: Box::new(source),
156 }
157 }
158}
159
160pub(crate) fn create_snapshot(
161 index: &MaterializedIndex,
162 snapshots_directory: &Path,
163 temporary_directory: &Path,
164) -> Result<SnapshotInfo, SnapshotError> {
165 let checkpoint = index.checkpoint()?;
166 if checkpoint.sequence == 0 && checkpoint.digest.is_some() {
167 return Err(SnapshotError::Invalid {
168 reason: "empty checkpoint has a digest",
169 });
170 }
171
172 let measurements = measure_payload(index, checkpoint.sequence)?;
173 let final_path =
174 snapshots_directory.join(format!("snapshot-{:020}.hysnap", checkpoint.sequence));
175 if final_path.exists() {
176 let existing = verify_snapshot(&final_path)?;
177 if existing.checkpoint_digest != checkpoint.digest {
178 return Err(SnapshotError::CheckpointConflict {
179 sequence: checkpoint.sequence,
180 });
181 }
182 return Ok(existing);
183 }
184
185 let mut header = [0_u8; HEADER_LENGTH];
186 header[0..8].copy_from_slice(&MAGIC);
187 header[8..10].copy_from_slice(&DISK_FORMAT_VERSION.to_le_bytes());
188 header[10..12].copy_from_slice(&0_u16.to_le_bytes());
189 header[12..20].copy_from_slice(&checkpoint.sequence.to_le_bytes());
190 header[20..52].copy_from_slice(&checkpoint.digest.unwrap_or([0; 32]));
191 header[52..60].copy_from_slice(&measurements.entry_count.to_le_bytes());
192 header[60..68].copy_from_slice(&measurements.receipt_count.to_le_bytes());
193 header[68..76].copy_from_slice(&measurements.payload_length.to_le_bytes());
194
195 let mut checksum = crc32c::crc32c(&header[..CHECKSUM_PREFIX_LENGTH]);
196 let mut checksum_error = None;
197 index.for_each_entry(|key, value| {
198 if checksum_error.is_some() {
199 return;
200 }
201 match encode_entry_header(key, value) {
202 Ok(entry_header) => {
203 checksum = crc32c::crc32c_append(checksum, &entry_header);
204 checksum = crc32c::crc32c_append(checksum, key);
205 checksum = crc32c::crc32c_append(checksum, value);
206 }
207 Err(source) => checksum_error = Some(source),
208 }
209 })?;
210 if let Some(source) = checksum_error {
211 return Err(source);
212 }
213 index.for_each_receipt(|receipt| {
214 checksum = crc32c::crc32c_append(checksum, &encode_receipt(receipt));
215 })?;
216 header[76..80].copy_from_slice(&checksum.to_le_bytes());
217
218 let temporary_path = temporary_directory.join(format!(
219 "snapshot-{:020}-{}.tmp",
220 checkpoint.sequence,
221 uuid::Uuid::now_v7()
222 ));
223 let mut file = OpenOptions::new()
224 .create_new(true)
225 .read(true)
226 .write(true)
227 .open(&temporary_path)?;
228 file.write_all(&header)?;
229 let mut hasher = blake3::Hasher::new();
230 hasher.update(&header[..DIGEST_PREFIX_LENGTH]);
231 let mut write_error = None;
232 index.for_each_entry(|key, value| {
233 if write_error.is_none()
234 && let Err(source) = write_entry(&mut file, &mut hasher, key, value)
235 {
236 write_error = Some(source);
237 }
238 })?;
239 if let Some(source) = write_error {
240 return Err(source);
241 }
242 let mut receipt_write_error = None;
243 index.for_each_receipt(|receipt| {
244 if receipt_write_error.is_none()
245 && let Err(source) = write_receipt(&mut file, &mut hasher, receipt)
246 {
247 receipt_write_error = Some(source);
248 }
249 })?;
250 if let Some(source) = receipt_write_error {
251 return Err(source);
252 }
253 let snapshot_digest = *hasher.finalize().as_bytes();
254 file.seek(SeekFrom::Start(80))?;
255 file.write_all(&snapshot_digest)?;
256 file.sync_all()?;
257 drop(file);
258
259 let temporary_info = verify_snapshot(&temporary_path)?;
260 std::fs::rename(&temporary_path, &final_path)?;
261 #[cfg(unix)]
262 sync_directory(snapshots_directory)?;
263 Ok(SnapshotInfo {
264 path: final_path,
265 ..temporary_info
266 })
267}
268
269pub fn verify_snapshot(path: impl AsRef<Path>) -> Result<SnapshotInfo, SnapshotError> {
276 let path = path.as_ref();
277 let mut file = File::open(path)?;
278 let file_bytes = file.metadata()?.len();
279 let mut header = [0_u8; HEADER_LENGTH];
280 read_exact_or_invalid(&mut file, &mut header, "truncated header")?;
281 let decoded = decode_header(&header, file_bytes)?;
282 verify_payload(&mut file, &header, &decoded)?;
283
284 Ok(SnapshotInfo {
285 path: path.to_path_buf(),
286 checkpoint_sequence: decoded.checkpoint_sequence,
287 checkpoint_digest: decoded.checkpoint_digest,
288 entry_count: decoded.entry_count,
289 receipt_count: decoded.receipt_count,
290 snapshot_digest: decoded.expected_digest,
291 file_bytes,
292 })
293}
294
295pub fn load_snapshot(
307 path: impl AsRef<Path>,
308 limits: &SnapshotReadLimits,
309) -> Result<SnapshotContents, SnapshotError> {
310 let path = path.as_ref();
311 let mut collector = SnapshotCollector {
312 entries: Vec::new(),
313 decoded_bytes: 0,
314 limits,
315 };
316 let info = read_snapshot_records_with_limits(path, &mut collector, Some(limits))?;
317 Ok(SnapshotContents {
318 info,
319 entries: collector.entries,
320 })
321}
322
323pub(crate) trait SnapshotRecordVisitor {
324 fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), SnapshotError>;
325 fn receipt(&mut self, receipt: &CommitReceipt) -> Result<(), SnapshotError>;
326}
327
328pub(crate) fn read_snapshot_records(
329 path: &Path,
330 visitor: &mut impl SnapshotRecordVisitor,
331) -> Result<SnapshotInfo, SnapshotError> {
332 read_snapshot_records_with_limits(path, visitor, None)
333}
334
335fn read_snapshot_records_with_limits(
336 path: &Path,
337 visitor: &mut impl SnapshotRecordVisitor,
338 limits: Option<&SnapshotReadLimits>,
339) -> Result<SnapshotInfo, SnapshotError> {
340 let before = verify_snapshot(path)?;
341 if let Some(limits) = limits {
342 validate_read_limits(&before, limits)?;
343 }
344 let mut file = File::open(path)?;
345 let file_bytes = file.metadata()?.len();
346 let mut header = [0_u8; HEADER_LENGTH];
347 read_exact_or_invalid(&mut file, &mut header, "truncated header")?;
348 let decoded = decode_header(&header, file_bytes)?;
349 let mut consumed = 0_u64;
350
351 for _ in 0..decoded.entry_count {
352 let mut entry_header = [0_u8; ENTRY_HEADER_LENGTH];
353 read_payload_exact(
354 &mut file,
355 &mut entry_header,
356 &mut consumed,
357 decoded.payload_length,
358 )?;
359 let key_length = usize::try_from(u32::from_le_bytes(copy_array(&entry_header[..4])))
360 .map_err(|_| SnapshotError::Invalid {
361 reason: "key length overflow during restore",
362 })?;
363 let value_length = usize::try_from(u64::from_le_bytes(copy_array(&entry_header[4..12])))
364 .map_err(|_| SnapshotError::Invalid {
365 reason: "value length overflow during restore",
366 })?;
367 if key_length == 0 || key_length > MAX_KEY_BYTES || value_length > MAX_OPERATION_BYTES {
368 return Err(SnapshotError::Invalid {
369 reason: "record exceeds restore bounds",
370 });
371 }
372 let mut key = vec![0_u8; key_length];
373 let mut value = vec![0_u8; value_length];
374 read_payload_exact(&mut file, &mut key, &mut consumed, decoded.payload_length)?;
375 read_payload_exact(&mut file, &mut value, &mut consumed, decoded.payload_length)?;
376 visitor.put(&key, &value)?;
377 }
378 for _ in 0..decoded.receipt_count {
379 let mut encoded = [0_u8; RECEIPT_LENGTH];
380 read_payload_exact(
381 &mut file,
382 &mut encoded,
383 &mut consumed,
384 decoded.payload_length,
385 )?;
386 visitor.receipt(&decode_snapshot_receipt(&encoded))?;
387 }
388 if consumed != decoded.payload_length {
389 return Err(SnapshotError::Invalid {
390 reason: "record counts do not consume payload during restore",
391 });
392 }
393
394 let after = verify_snapshot(path)?;
395 if before != after {
396 return Err(SnapshotError::Invalid {
397 reason: "snapshot changed during restore",
398 });
399 }
400 Ok(after)
401}
402
403fn validate_read_limits(
404 info: &SnapshotInfo,
405 limits: &SnapshotReadLimits,
406) -> Result<(), SnapshotError> {
407 if info.file_bytes > limits.file_bytes {
408 return Err(SnapshotError::FileLimitExceeded {
409 actual: info.file_bytes,
410 maximum: limits.file_bytes,
411 });
412 }
413 if info.entry_count > limits.entries {
414 return Err(SnapshotError::EntryLimitExceeded {
415 actual: info.entry_count,
416 maximum: limits.entries,
417 });
418 }
419 Ok(())
420}
421
422struct SnapshotCollector<'limits> {
423 entries: Vec<SnapshotEntry>,
424 decoded_bytes: u64,
425 limits: &'limits SnapshotReadLimits,
426}
427
428impl SnapshotRecordVisitor for SnapshotCollector<'_> {
429 fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), SnapshotError> {
430 let next_entry_count = u64::try_from(self.entries.len())
431 .ok()
432 .and_then(|count| count.checked_add(1))
433 .ok_or(SnapshotError::EntryLimitExceeded {
434 actual: u64::MAX,
435 maximum: self.limits.entries,
436 })?;
437 if next_entry_count > self.limits.entries {
438 return Err(SnapshotError::EntryLimitExceeded {
439 actual: next_entry_count,
440 maximum: self.limits.entries,
441 });
442 }
443 let entry_bytes = u64::try_from(key.len())
444 .ok()
445 .and_then(|key_bytes| {
446 u64::try_from(value.len())
447 .ok()
448 .and_then(|value_bytes| key_bytes.checked_add(value_bytes))
449 })
450 .ok_or(SnapshotError::DecodedBytesLimitExceeded {
451 maximum: self.limits.decoded_bytes,
452 })?;
453 self.decoded_bytes = self.decoded_bytes.checked_add(entry_bytes).ok_or(
454 SnapshotError::DecodedBytesLimitExceeded {
455 maximum: self.limits.decoded_bytes,
456 },
457 )?;
458 if self.decoded_bytes > self.limits.decoded_bytes {
459 return Err(SnapshotError::DecodedBytesLimitExceeded {
460 maximum: self.limits.decoded_bytes,
461 });
462 }
463 self.entries.push(SnapshotEntry {
464 key: key.to_vec(),
465 value: value.to_vec(),
466 });
467 Ok(())
468 }
469
470 fn receipt(&mut self, _receipt: &CommitReceipt) -> Result<(), SnapshotError> {
471 Ok(())
472 }
473}
474
475#[derive(Clone, Copy, Debug)]
476struct DecodedHeader {
477 checkpoint_sequence: u64,
478 checkpoint_digest: Option<[u8; 32]>,
479 entry_count: u64,
480 receipt_count: u64,
481 payload_length: u64,
482 expected_checksum: u32,
483 expected_digest: [u8; 32],
484}
485
486fn decode_header(
487 header: &[u8; HEADER_LENGTH],
488 file_bytes: u64,
489) -> Result<DecodedHeader, SnapshotError> {
490 if header[0..8] != MAGIC {
491 return Err(SnapshotError::Invalid {
492 reason: "bad magic",
493 });
494 }
495 let version = u16::from_le_bytes(copy_array(&header[8..10]));
496 if version != DISK_FORMAT_VERSION {
497 return Err(SnapshotError::UnsupportedVersion {
498 found: version,
499 supported: DISK_FORMAT_VERSION,
500 });
501 }
502 if u16::from_le_bytes(copy_array(&header[10..12])) != 0 {
503 return Err(SnapshotError::Invalid {
504 reason: "unsupported flags",
505 });
506 }
507
508 let checkpoint_sequence = u64::from_le_bytes(copy_array(&header[12..20]));
509 let raw_checkpoint_digest: [u8; 32] = copy_array(&header[20..52]);
510 let checkpoint_digest = if checkpoint_sequence == 0 {
511 if raw_checkpoint_digest != [0; 32] {
512 return Err(SnapshotError::Invalid {
513 reason: "empty checkpoint has a digest",
514 });
515 }
516 None
517 } else {
518 Some(raw_checkpoint_digest)
519 };
520 let entry_count = u64::from_le_bytes(copy_array(&header[52..60]));
521 let receipt_count = u64::from_le_bytes(copy_array(&header[60..68]));
522 if checkpoint_sequence == 0 && receipt_count != 0 {
523 return Err(SnapshotError::Invalid {
524 reason: "empty checkpoint has idempotency receipts",
525 });
526 }
527 let payload_length = u64::from_le_bytes(copy_array(&header[68..76]));
528 let expected_file_bytes =
529 HEADER_LENGTH_U64
530 .checked_add(payload_length)
531 .ok_or(SnapshotError::Invalid {
532 reason: "file length overflow",
533 })?;
534 if file_bytes != expected_file_bytes {
535 return Err(SnapshotError::Invalid {
536 reason: "file length mismatch",
537 });
538 }
539
540 Ok(DecodedHeader {
541 checkpoint_sequence,
542 checkpoint_digest,
543 entry_count,
544 receipt_count,
545 payload_length,
546 expected_checksum: u32::from_le_bytes(copy_array(&header[76..80])),
547 expected_digest: copy_array(&header[80..112]),
548 })
549}
550
551fn verify_payload(
552 file: &mut File,
553 header: &[u8; HEADER_LENGTH],
554 decoded: &DecodedHeader,
555) -> Result<(), SnapshotError> {
556 let mut checksum = crc32c::crc32c(&header[..CHECKSUM_PREFIX_LENGTH]);
557 let mut hasher = blake3::Hasher::new();
558 hasher.update(&header[..DIGEST_PREFIX_LENGTH]);
559 let mut consumed = 0_u64;
560 let mut previous_key: Option<Vec<u8>> = None;
561 let mut buffer = vec![0_u8; COPY_BUFFER_LENGTH].into_boxed_slice();
562 for _ in 0..decoded.entry_count {
563 let mut entry_header = [0_u8; ENTRY_HEADER_LENGTH];
564 read_payload_exact(
565 file,
566 &mut entry_header,
567 &mut consumed,
568 decoded.payload_length,
569 )?;
570 checksum = crc32c::crc32c_append(checksum, &entry_header);
571 hasher.update(&entry_header);
572 let key_length = usize::try_from(u32::from_le_bytes(copy_array(&entry_header[..4])))
573 .map_err(|_| SnapshotError::Invalid {
574 reason: "key length overflow",
575 })?;
576 let value_length = u64::from_le_bytes(copy_array(&entry_header[4..12]));
577 if key_length == 0 || key_length > MAX_KEY_BYTES {
578 return Err(SnapshotError::Invalid {
579 reason: "invalid key length",
580 });
581 }
582
583 let mut key = vec![0_u8; key_length];
584 read_payload_exact(file, &mut key, &mut consumed, decoded.payload_length)?;
585 checksum = crc32c::crc32c_append(checksum, &key);
586 hasher.update(&key);
587 if previous_key
588 .as_ref()
589 .is_some_and(|previous| previous >= &key)
590 {
591 return Err(SnapshotError::Invalid {
592 reason: "keys are not strictly sorted",
593 });
594 }
595 previous_key = Some(key);
596
597 let mut remaining = value_length;
598 while remaining > 0 {
599 let chunk_length =
600 usize::try_from(remaining.min(COPY_BUFFER_LENGTH_U64)).map_err(|_| {
601 SnapshotError::Invalid {
602 reason: "value length overflow",
603 }
604 })?;
605 let chunk = &mut buffer[..chunk_length];
606 read_payload_exact(file, chunk, &mut consumed, decoded.payload_length)?;
607 checksum = crc32c::crc32c_append(checksum, chunk);
608 hasher.update(chunk);
609 remaining -= u64::try_from(chunk_length).map_err(|_| SnapshotError::Invalid {
610 reason: "value length overflow",
611 })?;
612 }
613 }
614 let mut previous_transaction_id = None;
615 for _ in 0..decoded.receipt_count {
616 let mut encoded = [0_u8; RECEIPT_LENGTH];
617 read_payload_exact(file, &mut encoded, &mut consumed, decoded.payload_length)?;
618 checksum = crc32c::crc32c_append(checksum, &encoded);
619 hasher.update(&encoded);
620
621 let transaction_id: [u8; 16] = copy_array(&encoded[..16]);
622 if previous_transaction_id
623 .as_ref()
624 .is_some_and(|previous| previous >= &transaction_id)
625 {
626 return Err(SnapshotError::Invalid {
627 reason: "transaction identifiers are not strictly sorted",
628 });
629 }
630 previous_transaction_id = Some(transaction_id);
631 let commit_sequence = u64::from_le_bytes(copy_array(&encoded[16..24]));
632 if commit_sequence == 0 || commit_sequence > decoded.checkpoint_sequence {
633 return Err(SnapshotError::Invalid {
634 reason: "idempotency receipt exceeds snapshot checkpoint",
635 });
636 }
637 }
638 if consumed != decoded.payload_length {
639 return Err(SnapshotError::Invalid {
640 reason: "record counts do not consume payload",
641 });
642 }
643 if checksum != decoded.expected_checksum {
644 return Err(SnapshotError::Invalid {
645 reason: "CRC32C mismatch",
646 });
647 }
648 let actual_digest = *hasher.finalize().as_bytes();
649 if actual_digest != decoded.expected_digest {
650 return Err(SnapshotError::Invalid {
651 reason: "BLAKE3 digest mismatch",
652 });
653 }
654 Ok(())
655}
656
657#[derive(Clone, Copy, Debug)]
658struct Measurements {
659 entry_count: u64,
660 receipt_count: u64,
661 payload_length: u64,
662}
663
664fn measure_payload(
665 index: &MaterializedIndex,
666 checkpoint_sequence: u64,
667) -> Result<Measurements, SnapshotError> {
668 let mut entry_count = Some(0_u64);
669 let mut payload_length = Some(0_u64);
670 let mut valid = true;
671 index.for_each_entry(|key, value| {
672 if key.is_empty() || key.len() > MAX_KEY_BYTES {
673 valid = false;
674 return;
675 }
676 let Ok(key_length) = u64::try_from(key.len()) else {
677 valid = false;
678 return;
679 };
680 let Ok(value_length) = u64::try_from(value.len()) else {
681 valid = false;
682 return;
683 };
684 entry_count = entry_count.and_then(|count| count.checked_add(1));
685 payload_length = payload_length.and_then(|length| {
686 length
687 .checked_add(ENTRY_HEADER_LENGTH_U64)
688 .and_then(|length| length.checked_add(key_length))
689 .and_then(|length| length.checked_add(value_length))
690 });
691 })?;
692 let mut receipt_count = Some(0_u64);
693 index.for_each_receipt(|receipt| {
694 if receipt.commit_sequence == 0 || receipt.commit_sequence > checkpoint_sequence {
695 valid = false;
696 return;
697 }
698 receipt_count = receipt_count.and_then(|count| count.checked_add(1));
699 payload_length = payload_length.and_then(|length| length.checked_add(RECEIPT_LENGTH_U64));
700 })?;
701 if !valid {
702 return Err(SnapshotError::Invalid {
703 reason: "index contains an invalid key or idempotency receipt",
704 });
705 }
706 let Some(entry_count) = entry_count else {
707 return Err(SnapshotError::Invalid {
708 reason: "entry count overflow",
709 });
710 };
711 let Some(payload_length) = payload_length else {
712 return Err(SnapshotError::Invalid {
713 reason: "payload length overflow",
714 });
715 };
716 let Some(receipt_count) = receipt_count else {
717 return Err(SnapshotError::Invalid {
718 reason: "receipt count overflow",
719 });
720 };
721 Ok(Measurements {
722 entry_count,
723 receipt_count,
724 payload_length,
725 })
726}
727
728fn encode_entry_header(
729 key: &[u8],
730 value: &[u8],
731) -> Result<[u8; ENTRY_HEADER_LENGTH], SnapshotError> {
732 let key_length = u32::try_from(key.len()).map_err(|_| SnapshotError::Invalid {
733 reason: "key length overflow",
734 })?;
735 let value_length = u64::try_from(value.len()).map_err(|_| SnapshotError::Invalid {
736 reason: "value length overflow",
737 })?;
738 let mut entry_header = [0_u8; ENTRY_HEADER_LENGTH];
739 entry_header[..4].copy_from_slice(&key_length.to_le_bytes());
740 entry_header[4..].copy_from_slice(&value_length.to_le_bytes());
741 Ok(entry_header)
742}
743
744fn write_entry(
745 writer: &mut impl Write,
746 hasher: &mut blake3::Hasher,
747 key: &[u8],
748 value: &[u8],
749) -> Result<(), SnapshotError> {
750 let entry_header = encode_entry_header(key, value)?;
751 for bytes in [&entry_header[..], key, value] {
752 writer.write_all(bytes)?;
753 hasher.update(bytes);
754 }
755 Ok(())
756}
757
758fn encode_receipt(receipt: &CommitReceipt) -> [u8; RECEIPT_LENGTH] {
759 let mut encoded = [0_u8; RECEIPT_LENGTH];
760 encoded[..16].copy_from_slice(receipt.transaction_id.as_bytes());
761 encoded[16..24].copy_from_slice(&receipt.commit_sequence.to_le_bytes());
762 encoded[24..56].copy_from_slice(&receipt.commit_digest);
763 encoded[56..88].copy_from_slice(&receipt.transaction_digest);
764 encoded
765}
766
767fn decode_snapshot_receipt(encoded: &[u8; RECEIPT_LENGTH]) -> CommitReceipt {
768 CommitReceipt {
769 transaction_id: uuid::Uuid::from_bytes(copy_array(&encoded[..16])),
770 commit_sequence: u64::from_le_bytes(copy_array(&encoded[16..24])),
771 commit_digest: copy_array(&encoded[24..56]),
772 transaction_digest: copy_array(&encoded[56..88]),
773 }
774}
775
776fn write_receipt(
777 writer: &mut impl Write,
778 hasher: &mut blake3::Hasher,
779 receipt: &CommitReceipt,
780) -> Result<(), SnapshotError> {
781 let encoded = encode_receipt(receipt);
782 writer.write_all(&encoded)?;
783 hasher.update(&encoded);
784 Ok(())
785}
786
787fn read_payload_exact(
788 reader: &mut impl Read,
789 buffer: &mut [u8],
790 consumed: &mut u64,
791 payload_length: u64,
792) -> Result<(), SnapshotError> {
793 let length = u64::try_from(buffer.len()).map_err(|_| SnapshotError::Invalid {
794 reason: "payload length overflow",
795 })?;
796 let next = consumed.checked_add(length).ok_or(SnapshotError::Invalid {
797 reason: "payload length overflow",
798 })?;
799 if next > payload_length {
800 return Err(SnapshotError::Invalid {
801 reason: "entry exceeds payload",
802 });
803 }
804 read_exact_or_invalid(reader, buffer, "truncated payload")?;
805 *consumed = next;
806 Ok(())
807}
808
809fn read_exact_or_invalid(
810 reader: &mut impl Read,
811 buffer: &mut [u8],
812 reason: &'static str,
813) -> Result<(), SnapshotError> {
814 reader.read_exact(buffer).map_err(|source| {
815 if source.kind() == io::ErrorKind::UnexpectedEof {
816 SnapshotError::Invalid { reason }
817 } else {
818 SnapshotError::Io(source)
819 }
820 })
821}
822
823#[cfg(unix)]
824fn sync_directory(path: &Path) -> Result<(), SnapshotError> {
825 File::open(path)?.sync_all()?;
826 Ok(())
827}
828
829fn copy_array<const N: usize>(source: &[u8]) -> [u8; N] {
830 let mut output = [0_u8; N];
831 output.copy_from_slice(source);
832 output
833}