1use std::borrow::Cow;
30use std::fmt;
31use std::fs::{self, File};
32use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
33use std::path::{Path, PathBuf};
34
35use serde::de::{MapAccess, Visitor};
36use serde::{Deserialize, Deserializer, Serialize};
37use serde_json::value::RawValue;
38use sha2::{Digest, Sha256};
39
40use crate::system_state::{SimulationTime, SystemState, SystemStateSchema};
41use crate::time_series::StateSeries;
42
43use super::RecordingTiming;
44use super::error::StorageError;
45use super::json_payload_decoder::JsonPayloadDecoderRegistry;
46use super::jsonl_format::{ChunkMetadata, RecordingMetadata, RecordingStatus, StateStreamMetadata};
47use super::queued_state_writer::RecoveredUnsealedRecord;
48
49const METADATA_FILE: &str = "metadata.json";
51
52pub struct StoredStateSeriesReader {
59 root: PathBuf,
60 metadata_path: PathBuf,
61 metadata: RecordingMetadata,
62 timing: RecordingTiming,
63 decoders: JsonPayloadDecoderRegistry,
64}
65
66impl StoredStateSeriesReader {
67 pub fn open_completed_recording(
84 root: impl AsRef<Path>,
85 decoders: JsonPayloadDecoderRegistry,
86 ) -> Result<Self, StorageError> {
87 let root = root.as_ref().to_path_buf();
88 let metadata_path = root.join(METADATA_FILE);
89 let bytes = fs::read(&metadata_path).map_err(|source| StorageError::Io {
90 operation: "read metadata",
91 path: metadata_path.clone(),
92 source,
93 })?;
94 let metadata: RecordingMetadata =
95 serde_json::from_slice(&bytes).map_err(|source| StorageError::Json {
96 operation: "parse metadata",
97 path: metadata_path.clone(),
98 source,
99 })?;
100 metadata.validate(&metadata_path)?;
101 if !matches!(metadata.status, RecordingStatus::Complete) {
102 return Err(StorageError::RecordingNotComplete {
103 path: metadata_path,
104 });
105 }
106 let timing = RecordingTiming::from_stored(&metadata.timing, &metadata_path)?;
107 Ok(Self {
108 root,
109 metadata_path,
110 metadata,
111 timing,
112 decoders,
113 })
114 }
115
116 pub fn recording_directory(&self) -> &Path {
118 &self.root
119 }
120
121 pub fn stream_names(&self) -> impl ExactSizeIterator<Item = &str> {
123 self.metadata
124 .streams
125 .iter()
126 .map(|stream| stream.name.as_str())
127 }
128
129 pub fn format_version(&self) -> u32 {
131 self.metadata.version
132 }
133
134 pub fn user_metadata(&self) -> &serde_json::Map<String, serde_json::Value> {
136 &self.metadata.user_metadata
137 }
138
139 pub fn terminal_metadata(&self) -> &serde_json::Map<String, serde_json::Value> {
141 &self.metadata.terminal_metadata
142 }
143
144 pub fn recording_timing(&self) -> &RecordingTiming {
146 &self.timing
147 }
148
149 pub fn stream_record_count(&self, stream: &str) -> Result<u64, StorageError> {
151 let declaration =
152 self.metadata
153 .stream(stream)
154 .ok_or_else(|| StorageError::UnknownStateStream {
155 stream: stream.to_owned(),
156 })?;
157 declaration
158 .chunks
159 .iter()
160 .try_fold(0_u64, |total, chunk| total.checked_add(chunk.records))
161 .ok_or_else(|| StorageError::ByteCountOverflow {
162 stream: declaration.name.clone(),
163 })
164 }
165
166 pub fn stream_encoded_bytes(&self, stream: &str) -> Result<u64, StorageError> {
168 let declaration =
169 self.metadata
170 .stream(stream)
171 .ok_or_else(|| StorageError::UnknownStateStream {
172 stream: stream.to_owned(),
173 })?;
174 declaration
175 .chunks
176 .iter()
177 .try_fold(0_u64, |total, chunk| total.checked_add(chunk.bytes))
178 .ok_or_else(|| StorageError::ByteCountOverflow {
179 stream: declaration.name.clone(),
180 })
181 }
182
183 pub fn read_stream_as_state_series(&self, stream: &str) -> Result<StateSeries, StorageError> {
196 let declaration =
197 self.metadata
198 .stream(stream)
199 .ok_or_else(|| StorageError::UnknownStateStream {
200 stream: stream.to_owned(),
201 })?;
202 self.decoders
203 .require(declaration.fields.iter().map(|field| field.name.as_str()))?;
204
205 let spec = stream_spec(&self.metadata_path, declaration)?;
206 let total_records = self.stream_record_count(stream)?;
207 let capacity =
208 usize::try_from(total_records).map_err(|_| StorageError::ByteCountOverflow {
209 stream: declaration.name.clone(),
210 })?;
211 let mut series = StateSeries::with_capacity(spec, capacity);
212 let mut previous_iteration = None;
213
214 for chunk in &declaration.chunks {
215 self.read_chunk(declaration, chunk, &mut previous_iteration, &mut series)?;
216 }
217 Ok(series)
218 }
219
220 pub fn read_all_streams_as_state_series(
226 &self,
227 ) -> Result<Vec<(String, StateSeries)>, StorageError> {
228 self.metadata
229 .streams
230 .iter()
231 .map(|stream| {
232 self.read_stream_as_state_series(&stream.name)
233 .map(|series| (stream.name.clone(), series))
234 })
235 .collect()
236 }
237
238 pub fn read_latest_state_from_stream(&self, stream: &str) -> Result<SystemState, StorageError> {
246 let declaration =
247 self.metadata
248 .stream(stream)
249 .ok_or_else(|| StorageError::UnknownStateStream {
250 stream: stream.to_owned(),
251 })?;
252 self.decoders
253 .require(declaration.fields.iter().map(|field| field.name.as_str()))?;
254 let chunk = declaration
255 .chunks
256 .last()
257 .ok_or_else(|| StorageError::NoRecordedState {
258 stream: declaration.name.clone(),
259 })?;
260 let path = self.root.join(&declaration.directory).join(&chunk.file);
261 verify_file_size(&path, chunk.bytes)?;
262 let bytes = fs::read(&path).map_err(|source| {
263 if source.kind() == std::io::ErrorKind::NotFound {
264 StorageError::MissingChunk { path: path.clone() }
265 } else {
266 StorageError::Io {
267 operation: "read latest chunk",
268 path: path.clone(),
269 source,
270 }
271 }
272 })?;
273 verify_checksum(
274 &self.metadata_path,
275 &path,
276 &chunk.checksum,
277 Sha256::digest(&bytes),
278 )?;
279 let record = final_jsonl_record(&path, &bytes)?;
280 let spec = stream_spec(&self.metadata_path, declaration)?;
281 let state =
282 decode_state_record_with_decoders(record, &path, declaration, &spec, &self.decoders)?;
283 if state.simulation_time().iteration() != chunk.last_iteration {
284 return Err(invalid_record(
285 &path,
286 chunk.records,
287 format!(
288 "latest record iteration {} differs from chunk descriptor {}",
289 state.simulation_time().iteration(),
290 chunk.last_iteration
291 ),
292 ));
293 }
294 Ok(state)
295 }
296
297 fn read_chunk(
299 &self,
300 stream: &StateStreamMetadata,
301 chunk: &ChunkMetadata,
302 previous_iteration: &mut Option<u64>,
303 series: &mut StateSeries,
304 ) -> Result<(), StorageError> {
305 let path = self.root.join(&stream.directory).join(&chunk.file);
306 verify_file_size(&path, chunk.bytes)?;
307 let file = File::open(&path).map_err(|source| {
308 if source.kind() == std::io::ErrorKind::NotFound {
309 StorageError::MissingChunk { path: path.clone() }
310 } else {
311 StorageError::Io {
312 operation: "open chunk",
313 path: path.clone(),
314 source,
315 }
316 }
317 })?;
318 let mut input = BufReader::new(file);
319 let mut line = Vec::new();
320 let mut line_number = 0_u64;
321 let mut records = 0_u64;
322 let mut first_iteration = None;
323 let mut last_iteration = None;
324 let mut hasher = Sha256::new();
325
326 loop {
327 line.clear();
328 let bytes_read =
329 input
330 .read_until(b'\n', &mut line)
331 .map_err(|source| StorageError::Io {
332 operation: "read chunk",
333 path: path.clone(),
334 source,
335 })?;
336 if bytes_read == 0 {
337 break;
338 }
339 line_number =
340 line_number
341 .checked_add(1)
342 .ok_or_else(|| StorageError::ByteCountOverflow {
343 stream: stream.name.clone(),
344 })?;
345 hasher.update(&line);
346 if line.last() != Some(&b'\n') {
347 return Err(invalid_record(
348 &path,
349 line_number,
350 "record is not terminated by a newline",
351 ));
352 }
353 line.pop();
354 if line.is_empty() {
355 return Err(invalid_record(
356 &path,
357 line_number,
358 "record line must not be empty",
359 ));
360 }
361
362 let record: BorrowedRecord<'_> = serde_json::from_slice(&line).map_err(|source| {
363 invalid_record(&path, line_number, format!("invalid JSON record: {source}"))
364 })?;
365 validate_iteration(&path, line_number, record.iteration, *previous_iteration)?;
366 let iteration = record.iteration;
367 first_iteration.get_or_insert(iteration);
368 last_iteration = Some(iteration);
369 *previous_iteration = Some(iteration);
370
371 let time = match record.physical_time {
372 Some(physical_time) => {
373 SimulationTime::from_iteration_and_physical_time(iteration, physical_time)
374 .ok_or_else(|| {
375 invalid_record(&path, line_number, "physical time must be finite")
376 })?
377 }
378 None => SimulationTime::from_iteration(iteration),
379 };
380 let mut state = series.schema().create_empty_state(time);
381 decode_values(
382 &self.decoders,
383 stream,
384 &path,
385 line_number,
386 record.values,
387 &mut state,
388 )?;
389 series.push_state(state).map_err(|rejection| {
390 let (source, state) = rejection.into_parts();
391 let index = state.simulation_time().iteration();
392 drop(state);
393 StorageError::StateSeriesInvariant {
394 stream: stream.name.clone(),
395 iteration: index,
396 source,
397 }
398 })?;
399 records = records
400 .checked_add(1)
401 .ok_or_else(|| StorageError::ByteCountOverflow {
402 stream: stream.name.clone(),
403 })?;
404 }
405
406 validate_chunk_facts(
407 &self.metadata_path,
408 stream,
409 chunk,
410 records,
411 first_iteration,
412 last_iteration,
413 )?;
414 verify_checksum(
415 &self.metadata_path,
416 &path,
417 &chunk.checksum,
418 hasher.finalize(),
419 )
420 }
421}
422
423fn final_jsonl_record<'a>(path: &Path, bytes: &'a [u8]) -> Result<&'a [u8], StorageError> {
425 if !bytes.ends_with(b"\n") {
426 return Err(invalid_record(
427 path,
428 1,
429 "latest chunk is not terminated by a newline",
430 ));
431 }
432 let without_final_newline = &bytes[..bytes.len() - 1];
433 let start = without_final_newline
434 .iter()
435 .rposition(|byte| *byte == b'\n')
436 .map_or(0, |position| position + 1);
437 let record = &without_final_newline[start..];
438 if record.is_empty() {
439 return Err(invalid_record(path, 1, "latest record must not be empty"));
440 }
441 Ok(record)
442}
443
444impl fmt::Debug for StoredStateSeriesReader {
445 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
447 formatter
448 .debug_struct("StoredStateSeriesReader")
449 .field("root", &self.root)
450 .field("streams", &self.metadata.streams.len())
451 .field("decoders", &self.decoders)
452 .finish_non_exhaustive()
453 }
454}
455
456pub(crate) fn decode_resume_state(
463 root: &Path,
464 _metadata_path: &Path,
465 stream: &StateStreamMetadata,
466 full_spec: &SystemStateSchema,
467 decoders: &JsonPayloadDecoderRegistry,
468 open_record: Option<&RecoveredUnsealedRecord>,
469) -> Result<SystemState, StorageError> {
470 validate_complete_resume_schema(stream, full_spec)?;
471 decoders.require(full_spec.field_schemas().iter().map(|field| field.name()))?;
472
473 if let Some(record) = open_record {
474 let bytes = read_open_record(record, &stream.name)?;
475 return decode_state_record_with_decoders(
476 &bytes,
477 record.path(),
478 stream,
479 full_spec,
480 decoders,
481 );
482 }
483
484 let chunk = stream
485 .chunks
486 .last()
487 .ok_or_else(|| StorageError::NoCheckpointState {
488 stream: stream.name.clone(),
489 })?;
490 let path = root.join(&stream.directory).join(&chunk.file);
491 let record = read_last_sealed_record(&path)?;
492 if record.is_empty() {
493 return Err(StorageError::NoCheckpointState {
494 stream: stream.name.clone(),
495 });
496 }
497 decode_state_record_with_decoders(&record, &path, stream, full_spec, decoders)
498}
499
500fn read_open_record(
502 record: &RecoveredUnsealedRecord,
503 stream: &str,
504) -> Result<Vec<u8>, StorageError> {
505 let length = usize::try_from(record.bytes()).map_err(|_| StorageError::ByteCountOverflow {
506 stream: stream.to_owned(),
507 })?;
508 let mut file = File::open(record.path()).map_err(|source| StorageError::Io {
509 operation: "open latest recoverable record",
510 path: record.path().to_path_buf(),
511 source,
512 })?;
513 file.seek(SeekFrom::Start(record.offset()))
514 .map_err(|source| StorageError::Io {
515 operation: "seek latest recoverable record",
516 path: record.path().to_path_buf(),
517 source,
518 })?;
519 let mut bytes = vec![0_u8; length];
520 file.read_exact(&mut bytes)
521 .map_err(|source| StorageError::Io {
522 operation: "read latest recoverable record",
523 path: record.path().to_path_buf(),
524 source,
525 })?;
526 Ok(bytes)
527}
528
529fn validate_complete_resume_schema(
531 stream: &StateStreamMetadata,
532 full_spec: &SystemStateSchema,
533) -> Result<(), StorageError> {
534 if stream.fields.len() != full_spec.len() {
535 return Err(StorageError::IncompleteCheckpointStream {
536 stream: stream.name.clone(),
537 reason: format!(
538 "stream declares {} fields but the full state declares {}",
539 stream.fields.len(),
540 full_spec.len()
541 ),
542 });
543 }
544 for (position, (stored, expected)) in stream
545 .fields
546 .iter()
547 .zip(full_spec.field_schemas())
548 .enumerate()
549 {
550 if stored.name != expected.name() || stored.description.as_deref() != expected.description()
551 {
552 return Err(StorageError::IncompleteCheckpointStream {
553 stream: stream.name.clone(),
554 reason: format!(
555 "field {position} is `{}` but full state requires `{}`",
556 stored.name,
557 expected.name()
558 ),
559 });
560 }
561 }
562 Ok(())
563}
564
565fn decode_state_record_with_decoders(
567 record: &[u8],
568 path: &Path,
569 stream: &StateStreamMetadata,
570 full_spec: &SystemStateSchema,
571 decoders: &JsonPayloadDecoderRegistry,
572) -> Result<SystemState, StorageError> {
573 let record: BorrowedRecord<'_> = serde_json::from_slice(record)
574 .map_err(|source| invalid_record(path, 1, format!("invalid JSON record: {source}")))?;
575 let time = match record.physical_time {
576 Some(physical_time) => {
577 SimulationTime::from_iteration_and_physical_time(record.iteration, physical_time)
578 .ok_or_else(|| invalid_record(path, 1, "physical time must be finite"))?
579 }
580 None => SimulationTime::from_iteration(record.iteration),
581 };
582 let mut state = full_spec.create_empty_state(time);
583 decode_values(decoders, stream, path, 1, record.values, &mut state)?;
584 Ok(state)
585}
586
587fn read_last_sealed_record(path: &Path) -> Result<Vec<u8>, StorageError> {
589 const BLOCK: u64 = 8 * 1024;
590 let mut file = File::open(path).map_err(|source| {
591 if source.kind() == std::io::ErrorKind::NotFound {
592 StorageError::MissingChunk {
593 path: path.to_path_buf(),
594 }
595 } else {
596 StorageError::Io {
597 operation: "open latest sealed checkpoint",
598 path: path.to_path_buf(),
599 source,
600 }
601 }
602 })?;
603 let length = file
604 .metadata()
605 .map_err(|source| StorageError::Io {
606 operation: "inspect latest sealed checkpoint",
607 path: path.to_path_buf(),
608 source,
609 })?
610 .len();
611 if length == 0 {
612 return Ok(Vec::new());
613 }
614
615 file.seek(SeekFrom::Start(length - 1))
616 .map_err(|source| StorageError::Io {
617 operation: "seek latest sealed checkpoint",
618 path: path.to_path_buf(),
619 source,
620 })?;
621 let mut final_byte = [0_u8; 1];
622 file.read_exact(&mut final_byte)
623 .map_err(|source| StorageError::Io {
624 operation: "read latest sealed checkpoint",
625 path: path.to_path_buf(),
626 source,
627 })?;
628 if final_byte[0] != b'\n' {
629 return Err(invalid_record(
630 path,
631 1,
632 "sealed chunk does not end with a record newline",
633 ));
634 }
635
636 let record_end = length - 1;
637 let mut cursor = record_end;
638 let mut record_start = 0_u64;
639 while cursor > 0 {
640 let block_start = cursor.saturating_sub(BLOCK);
641 let block_len =
642 usize::try_from(cursor - block_start).map_err(|_| StorageError::ByteCountOverflow {
643 stream: path.display().to_string(),
644 })?;
645 let mut block = vec![0_u8; block_len];
646 file.seek(SeekFrom::Start(block_start))
647 .and_then(|_| file.read_exact(&mut block))
648 .map_err(|source| StorageError::Io {
649 operation: "scan latest sealed record boundary",
650 path: path.to_path_buf(),
651 source,
652 })?;
653 if let Some(position) = block.iter().rposition(|byte| *byte == b'\n') {
654 record_start = block_start + position as u64 + 1;
655 break;
656 }
657 cursor = block_start;
658 }
659
660 let record_len = usize::try_from(record_end - record_start).map_err(|_| {
661 StorageError::ByteCountOverflow {
662 stream: path.display().to_string(),
663 }
664 })?;
665 let mut record = vec![0_u8; record_len];
666 file.seek(SeekFrom::Start(record_start))
667 .and_then(|_| file.read_exact(&mut record))
668 .map_err(|source| StorageError::Io {
669 operation: "read latest sealed record",
670 path: path.to_path_buf(),
671 source,
672 })?;
673 Ok(record)
674}
675
676#[derive(Deserialize)]
678#[serde(deny_unknown_fields)]
679struct BorrowedRecord<'a> {
680 iteration: u64,
681 #[serde(default)]
682 physical_time: Option<f64>,
683 #[serde(borrow)]
684 values: BorrowedValues<'a>,
685}
686
687struct BorrowedValues<'a> {
689 entries: Vec<(String, &'a RawValue)>,
690}
691
692impl<'de: 'a, 'a> Deserialize<'de> for BorrowedValues<'a> {
693 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
695 where
696 D: Deserializer<'de>,
697 {
698 deserializer.deserialize_map(BorrowedValuesVisitor {
699 output: std::marker::PhantomData,
700 })
701 }
702}
703
704struct BorrowedValuesVisitor<'a> {
706 output: std::marker::PhantomData<&'a RawValue>,
708}
709
710impl<'de: 'a, 'a> Visitor<'de> for BorrowedValuesVisitor<'a> {
711 type Value = BorrowedValues<'a>;
712
713 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
715 formatter.write_str("an object of unique field keys and raw JSON values")
716 }
717
718 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
721 where
722 A: MapAccess<'de>,
723 {
724 let mut entries = Vec::with_capacity(map.size_hint().unwrap_or(0));
725 while let Some((key, value)) = map.next_entry::<Cow<'de, str>, &'de RawValue>()? {
726 if entries.iter().any(|(existing, _)| existing == key.as_ref()) {
727 return Err(serde::de::Error::custom(format!(
728 "duplicate payload field `{key}`"
729 )));
730 }
731 let value: &'a RawValue = value;
732 entries.push((key.into_owned(), value));
733 }
734 Ok(BorrowedValues { entries })
735 }
736}
737
738#[derive(Serialize)]
740struct StreamTemplateRef<'a> {
741 fields: &'a [super::jsonl_format::StateFieldMetadata],
742}
743
744fn stream_spec(
746 metadata_path: &Path,
747 stream: &StateStreamMetadata,
748) -> Result<SystemStateSchema, StorageError> {
749 let bytes = serde_json::to_vec(&StreamTemplateRef {
750 fields: &stream.fields,
751 })
752 .map_err(|source| StorageError::Json {
753 operation: "serialize stream schema",
754 path: metadata_path.to_path_buf(),
755 source,
756 })?;
757 SystemStateSchema::parse(metadata_path.to_path_buf(), &bytes).map_err(|source| {
758 StorageError::InvalidMetadata {
759 path: metadata_path.to_path_buf(),
760 reason: format!(
761 "stream `{}` has an invalid state schema: {source}",
762 stream.name
763 ),
764 }
765 })
766}
767
768fn verify_file_size(path: &Path, expected: u64) -> Result<(), StorageError> {
770 let actual = match fs::metadata(path) {
771 Ok(metadata) => metadata.len(),
772 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
773 return Err(StorageError::MissingChunk {
774 path: path.to_path_buf(),
775 });
776 }
777 Err(source) => {
778 return Err(StorageError::Io {
779 operation: "inspect chunk",
780 path: path.to_path_buf(),
781 source,
782 });
783 }
784 };
785 if actual != expected {
786 return Err(StorageError::ChunkSizeMismatch {
787 path: path.to_path_buf(),
788 expected,
789 actual,
790 });
791 }
792 Ok(())
793}
794
795fn validate_iteration(
797 path: &Path,
798 line: u64,
799 iteration: u64,
800 previous: Option<u64>,
801) -> Result<(), StorageError> {
802 if let Some(previous) = previous
803 && iteration <= previous
804 {
805 return Err(invalid_record(
806 path,
807 line,
808 format!("iteration {iteration} is not greater than previous iteration {previous}"),
809 ));
810 }
811 Ok(())
812}
813
814fn decode_values(
816 decoders: &JsonPayloadDecoderRegistry,
817 stream: &StateStreamMetadata,
818 path: &Path,
819 line: u64,
820 mut values: BorrowedValues<'_>,
821 state: &mut crate::system_state::SystemState,
822) -> Result<(), StorageError> {
823 for field in &stream.fields {
824 let Some(position) = values
825 .entries
826 .iter()
827 .position(|(name, _)| name == &field.name)
828 else {
829 return Err(invalid_record(
830 path,
831 line,
832 format!("missing payload field `{}`", field.name),
833 ));
834 };
835 let (_, raw) = values.entries.swap_remove(position);
836 decoders.decode_into(
837 &stream.name,
838 state.simulation_time().iteration(),
839 &field.name,
840 raw.get(),
841 state,
842 )?;
843 }
844 if !values.entries.is_empty() {
845 let mut extra = values
846 .entries
847 .into_iter()
848 .map(|(name, _)| name)
849 .collect::<Vec<_>>();
850 extra.sort_unstable();
851 return Err(invalid_record(
852 path,
853 line,
854 format!("undeclared payload fields: {}", extra.join(", ")),
855 ));
856 }
857 Ok(())
858}
859
860fn validate_chunk_facts(
862 metadata_path: &Path,
863 stream: &StateStreamMetadata,
864 chunk: &ChunkMetadata,
865 records: u64,
866 first_iteration: Option<u64>,
867 last_iteration: Option<u64>,
868) -> Result<(), StorageError> {
869 if records != chunk.records
870 || first_iteration != Some(chunk.first_iteration)
871 || last_iteration != Some(chunk.last_iteration)
872 {
873 return Err(StorageError::InvalidMetadata {
874 path: metadata_path.to_path_buf(),
875 reason: format!(
876 "stream `{}` chunk {} declares {} records at {}..={}, but contains {} records at {:?}..={:?}",
877 stream.name,
878 chunk.ordinal,
879 chunk.records,
880 chunk.first_iteration,
881 chunk.last_iteration,
882 records,
883 first_iteration,
884 last_iteration
885 ),
886 });
887 }
888 Ok(())
889}
890
891fn verify_checksum(
893 metadata_path: &Path,
894 path: &Path,
895 expected: &str,
896 digest: impl AsRef<[u8]>,
897) -> Result<(), StorageError> {
898 let Some(expected_digest) = expected.strip_prefix("sha256:") else {
899 return Err(StorageError::InvalidMetadata {
900 path: metadata_path.to_path_buf(),
901 reason: format!("unsupported chunk checksum algorithm in `{expected}`"),
902 });
903 };
904 let actual_digest = lowercase_hex(digest.as_ref());
905 if actual_digest != expected_digest {
906 return Err(StorageError::ChecksumMismatch {
907 path: path.to_path_buf(),
908 expected: expected.to_owned(),
909 actual: format!("sha256:{actual_digest}"),
910 });
911 }
912 Ok(())
913}
914
915fn lowercase_hex(bytes: &[u8]) -> String {
917 use std::fmt::Write as _;
918
919 let mut encoded = String::with_capacity(bytes.len() * 2);
920 for byte in bytes {
921 write!(&mut encoded, "{byte:02x}").expect("writing to String cannot fail");
922 }
923 encoded
924}
925
926fn invalid_record(path: &Path, line: u64, reason: impl Into<String>) -> StorageError {
928 StorageError::InvalidRecord {
929 path: path.to_path_buf(),
930 line,
931 reason: reason.into(),
932 }
933}