use crate::io::{LittleEndian, ReadBytesExt};
use crate::{
Checksum, SeqNo, TableId, TreeType,
coding::Decode,
config::ManifestRecoveryMode,
file::CURRENT_VERSION_FILE,
fs::{Fs, FsOpenOptions},
version::VersionId,
vlog::BlobFileId,
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::path::Path;
const TABLE_ENTRY_PAYLOAD_LEN: u32 = 8 + 1 + 16 + 8;
const BLOB_ENTRY_PAYLOAD_LEN: u32 = 8 + 1 + 16;
fn decode_table_entry_payload(payload: &[u8]) -> crate::Result<RecoveredTable> {
if payload.len() != TABLE_ENTRY_PAYLOAD_LEN as usize {
return Err(crate::Error::InvalidHeader("tables record payload length"));
}
let mut cursor = crate::io::Cursor::new(payload);
let id = cursor.read_u64::<LittleEndian>()?;
let checksum_type = cursor.read_u8()?;
if checksum_type != 0 {
return Err(crate::Error::InvalidTag(("ChecksumType", checksum_type)));
}
let checksum = Checksum::from_raw(cursor.read_u128::<LittleEndian>()?);
let global_seqno = cursor.read_u64::<LittleEndian>()?;
Ok(RecoveredTable {
id,
checksum,
global_seqno,
})
}
fn decode_blob_entry_payload(payload: &[u8]) -> crate::Result<(BlobFileId, Checksum)> {
if payload.len() != BLOB_ENTRY_PAYLOAD_LEN as usize {
return Err(crate::Error::InvalidHeader(
"blob_files record payload length",
));
}
let mut cursor = crate::io::Cursor::new(payload);
let id = cursor.read_u64::<LittleEndian>()?;
let checksum_type = cursor.read_u8()?;
if checksum_type != 0 {
return Err(crate::Error::InvalidTag(("ChecksumType", checksum_type)));
}
let checksum = Checksum::from_raw(cursor.read_u128::<LittleEndian>()?);
Ok((id, checksum))
}
fn parse_restrictions_section(
mut bytes: &[u8],
) -> crate::Result<crate::HashMap<TableId, crate::UserKey>> {
const ERR: crate::Error = crate::Error::InvalidHeader("restrictions section");
let r = &mut bytes;
let count = r.read_u32::<LittleEndian>().map_err(|_| ERR)?;
let mut map = crate::HashMap::default();
for _ in 0..count {
let id = r.read_u64::<LittleEndian>().map_err(|_| ERR)?;
let key_len = r.read_u32::<LittleEndian>().map_err(|_| ERR)? as usize;
if r.len() < key_len {
return Err(ERR);
}
let (head, tail) = r.split_at(key_len);
*r = tail;
if map.insert(id, crate::UserKey::from(head)).is_some() {
return Err(ERR);
}
}
if !r.is_empty() {
return Err(ERR);
}
Ok(map)
}
fn parse_blob_restrictions_section(
mut bytes: &[u8],
) -> crate::Result<crate::HashMap<BlobFileId, u64>> {
const ERR: crate::Error = crate::Error::InvalidHeader("blob_restrictions section");
let r = &mut bytes;
let count = r.read_u32::<LittleEndian>().map_err(|_| ERR)?;
let mut map = crate::HashMap::default();
for _ in 0..count {
let id = r.read_u64::<LittleEndian>().map_err(|_| ERR)?;
let frontier = r.read_u64::<LittleEndian>().map_err(|_| ERR)?;
if map.insert(id, frontier).is_some() {
return Err(ERR);
}
}
if !r.is_empty() {
return Err(ERR);
}
Ok(map)
}
fn parse_retention_floor_section(mut bytes: &[u8]) -> crate::Result<SeqNo> {
const ERR: crate::Error = crate::Error::InvalidHeader("retention_floor section");
let r = &mut bytes;
let floor = r.read_u64::<LittleEndian>().map_err(|_| ERR)?;
if !r.is_empty() {
return Err(ERR);
}
Ok(floor)
}
pub fn get_current_version(
folder: &Path,
fs: &dyn Fs,
encryption: Option<alloc::sync::Arc<dyn crate::encryption::EncryptionProvider>>,
) -> crate::Result<VersionId> {
use crate::io::{LittleEndian, ReadBytesExt};
let path = folder.join(CURRENT_VERSION_FILE);
let mut file = fs.open(&path, &FsOpenOptions::new().read(true))?;
let version_id = file.read_u64::<LittleEndian>()?;
let stored_checksum = file.read_u128::<LittleEndian>()?;
let checksum_type = file.read_u8()?;
if checksum_type != 0 {
return Err(crate::Error::InvalidTag(("ChecksumType", checksum_type)));
}
let manifest_path = folder.join(format!("v{version_id}"));
let archive = crate::manifest_blocks::reader::ManifestArchiveReader::open(
&manifest_path,
fs,
alloc::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
encryption,
)
.map_err(|e| match e {
crate::Error::Io(io) if io.kind() == crate::io::ErrorKind::NotFound => {
crate::Error::ManifestFooterInvalid(
"manifest file referenced by CURRENT does not exist",
)
}
other => other,
})?;
let computed = crate::manifest_blocks::current_digest::compute(version_id, archive.footer())?;
if computed != stored_checksum {
return Err(crate::Error::ChecksumMismatch {
got: Checksum::from_raw(computed),
expected: Checksum::from_raw(stored_checksum),
});
}
Ok(version_id)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecoveredTable {
pub id: TableId,
pub checksum: Checksum,
pub global_seqno: SeqNo,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct RecoveryStats {
pub tables_dropped_to_tail: u32,
pub tables_dropped_to_corruption: u32,
pub tables_truncated_headers: u32,
pub blob_dropped_to_tail: u32,
pub blob_dropped_to_corruption: u32,
}
#[derive(Debug)]
pub struct Recovery {
pub tree_type: TreeType,
pub snapshot_id: VersionId,
pub curr_version_id: VersionId,
pub table_ids: Vec<Vec<Vec<RecoveredTable>>>,
pub blob_file_ids: Vec<(BlobFileId, Checksum)>,
pub gc_stats: crate::blob_tree::FragmentationMap,
pub restrictions: crate::HashMap<TableId, crate::UserKey>,
pub blob_restrictions: crate::HashMap<BlobFileId, u64>,
pub retention_floor: SeqNo,
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "in-tree telemetry assertions only; not part \
of the published API surface today"
)
)]
pub stats: RecoveryStats,
}
impl Recovery {
pub(crate) fn apply_edit(&mut self, edit: &super::edit::VersionEdit) -> crate::Result<()> {
for cl in &edit.changed_levels {
let idx = usize::from(cl.level);
if idx >= self.table_ids.len() {
self.table_ids.resize_with(idx + 1, Vec::new);
}
let new_layout = cl
.runs
.iter()
.map(|run| {
run.iter()
.map(|t| RecoveredTable {
id: t.id,
checksum: Checksum::from_raw(t.checksum),
global_seqno: t.global_seqno,
})
.collect()
})
.collect();
if let Some(slot) = self.table_ids.get_mut(idx) {
*slot = new_layout;
}
}
if !edit.removed_blob_file_ids.is_empty() {
self.blob_file_ids
.retain(|(id, _)| !edit.removed_blob_file_ids.contains(id));
}
for b in &edit.added_blob_files {
let checksum = Checksum::from_raw(b.checksum);
if let Some(entry) = self.blob_file_ids.iter_mut().find(|(id, _)| *id == b.id) {
entry.1 = checksum;
} else {
self.blob_file_ids.push((b.id, checksum));
}
}
if let Some(bytes) = &edit.gc_stats {
self.gc_stats = crate::blob_tree::FragmentationMap::decode_from(&mut &bytes[..])?;
}
self.restrictions = edit
.restrictions
.iter()
.map(|(id, key)| (*id, key.clone()))
.collect();
self.blob_restrictions = edit.blob_restrictions.iter().copied().collect();
if let Some(floor) = edit.retention_floor {
self.retention_floor = floor;
}
self.curr_version_id = edit.new_version_id;
Ok(())
}
}
#[expect(
clippy::too_many_lines,
reason = "manifest recovery is inherently a long sequential read of multiple SFA \
sections; splitting the function would just move the per-mode branching \
into helpers without clarifying the flow"
)]
pub fn recover(
folder: &Path,
fs: &dyn Fs,
mode: ManifestRecoveryMode,
encryption: Option<alloc::sync::Arc<dyn crate::encryption::EncryptionProvider>>,
) -> crate::Result<Recovery> {
const FRAMED_TABLE_ENTRY_LEN: u64 =
crate::version::framing::FRAME_HEADER_LEN as u64 + TABLE_ENTRY_PAYLOAD_LEN as u64;
const FRAMED_BLOB_ENTRY_LEN: u64 =
crate::version::framing::FRAME_HEADER_LEN as u64 + BLOB_ENTRY_PAYLOAD_LEN as u64;
use crate::version::framing::FramedRecordOutcome;
let curr_version_id = get_current_version(folder, fs, encryption.clone())?;
let version_file_path = folder.join(format!("v{curr_version_id}"));
log::info!(
"Recovering current manifest at {} (mode={mode:?})",
version_file_path.display(),
);
let mut archive = crate::manifest_blocks::reader::ManifestArchiveReader::open(
&version_file_path,
fs,
alloc::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
encryption,
)?;
let tolerate_tail = !matches!(mode, ManifestRecoveryMode::AbsoluteConsistency);
let pit_prefix = matches!(mode, ManifestRecoveryMode::PointInTimeRecovery);
let skip_any = matches!(mode, ManifestRecoveryMode::SkipAnyCorruptedRecords);
let mut levels = vec![];
let mut tables_dropped_to_tail: u32 = 0;
let mut tables_dropped_to_corruption: u32 = 0;
let mut tables_truncated_headers: u32 = 0;
let mut read_scratch: Vec<u8> = Vec::with_capacity(64);
{
if archive.section("tables").is_none() {
log::error!(
"tables section not found in version #{curr_version_id} - maybe the file is corrupted?"
);
return Err(crate::Error::Unrecoverable);
}
let section_bytes = archive.read_section("tables")?;
let section_len: u64 = section_bytes.len() as u64;
let mut reader = crate::io::Cursor::new(section_bytes);
let mut tables_bytes_consumed: u64 = 0;
let level_count = match reader.read_u8() {
Ok(n) => {
tables_bytes_consumed += 1;
n
}
Err(e) if tolerate_tail && e.kind() == crate::io::ErrorKind::UnexpectedEof => {
log::warn!(
"tables section truncated before level_count byte in version \
#{curr_version_id}; tail-tolerant mode produces 0 levels"
);
0
}
Err(e) => return Err(e.into()),
};
'levels: for _ in 0..level_count {
let mut level = vec![];
let run_count = match reader.read_u8() {
Ok(n) => {
tables_bytes_consumed += 1;
n
}
Err(e) if tolerate_tail && e.kind() == crate::io::ErrorKind::UnexpectedEof => {
tables_truncated_headers += 1;
levels.push(level);
break 'levels;
}
Err(e) => return Err(e.into()),
};
for _ in 0..run_count {
let mut run = vec![];
let mut corrupted_in_run: u32 = 0;
let table_count = match reader.read_u32::<LittleEndian>() {
Ok(n) => {
tables_bytes_consumed += 4;
n
}
Err(e) if tolerate_tail && e.kind() == crate::io::ErrorKind::UnexpectedEof => {
tables_truncated_headers += 1;
levels.push(level);
break 'levels;
}
Err(e) => return Err(e.into()),
};
let bytes_remaining = section_len.saturating_sub(tables_bytes_consumed);
if u64::from(table_count) * FRAMED_TABLE_ENTRY_LEN > bytes_remaining {
if tolerate_tail {
log::warn!(
"tables: declared table_count={table_count} exceeds \
remaining section payload ({bytes_remaining} bytes, \
~{} entries) in version #{curr_version_id}; \
tail-tolerant mode walks bytes-actually-present and \
stops at the first EOF",
bytes_remaining / FRAMED_TABLE_ENTRY_LEN,
);
} else {
return Err(crate::Error::Unrecoverable);
}
}
for _ in 0..table_count {
let remaining = section_len.saturating_sub(tables_bytes_consumed);
let outcome = crate::version::framing::read_framed_record(
&mut reader,
remaining,
Some(TABLE_ENTRY_PAYLOAD_LEN),
&mut read_scratch,
)?;
match outcome {
FramedRecordOutcome::Ok => {
tables_bytes_consumed += crate::version::framing::FRAME_HEADER_LEN
as u64
+ read_scratch.len() as u64;
match decode_table_entry_payload(&read_scratch) {
Ok(t) => run.push(t),
Err(e) if skip_any => {
log::warn!(
"skip_any: tables record decode failed in version \
#{curr_version_id}: {e:?}; skipping",
);
tables_dropped_to_corruption =
tables_dropped_to_corruption.saturating_add(1);
corrupted_in_run = corrupted_in_run.saturating_add(1);
}
Err(e) if pit_prefix => {
log::warn!(
"pit: tables record decode failed in version \
#{curr_version_id}: {e:?}; accepting consistent \
prefix and dropping the rest of this run + unread \
levels",
);
let recovered = u32::try_from(run.len()).unwrap_or(u32::MAX);
tables_dropped_to_corruption = tables_dropped_to_corruption
.saturating_add(table_count.saturating_sub(recovered));
if !run.is_empty() {
level.push(run);
}
if !level.is_empty() {
levels.push(level);
}
break 'levels;
}
Err(e) => return Err(e),
}
}
FramedRecordOutcome::TailTruncation if tolerate_tail => {
let recovered = u32::try_from(run.len()).unwrap_or(u32::MAX);
let processed = recovered.saturating_add(corrupted_in_run);
tables_dropped_to_tail = tables_dropped_to_tail
.saturating_add(table_count.saturating_sub(processed));
if !run.is_empty() {
level.push(run);
}
if !level.is_empty() {
levels.push(level);
}
break 'levels;
}
FramedRecordOutcome::ChecksumMismatch { bytes_consumed, .. }
if skip_any =>
{
log::warn!(
"skip_any: tables record checksum mismatch \
({bytes_consumed} bytes) in version \
#{curr_version_id}, skipping"
);
tables_bytes_consumed += bytes_consumed;
tables_dropped_to_corruption =
tables_dropped_to_corruption.saturating_add(1);
corrupted_in_run = corrupted_in_run.saturating_add(1);
}
FramedRecordOutcome::ChecksumMismatch { .. } if pit_prefix => {
log::warn!(
"pit: tables record checksum mismatch in version \
#{curr_version_id}; accepting consistent prefix and \
dropping the rest of this run + unread levels"
);
let recovered = u32::try_from(run.len()).unwrap_or(u32::MAX);
tables_dropped_to_corruption = tables_dropped_to_corruption
.saturating_add(table_count.saturating_sub(recovered));
if !run.is_empty() {
level.push(run);
}
if !level.is_empty() {
levels.push(level);
}
break 'levels;
}
FramedRecordOutcome::ChecksumMismatch { expected, got, .. } => {
return Err(crate::Error::ManifestFrameChecksumMismatch {
section: "tables",
expected,
got,
});
}
FramedRecordOutcome::BadHeader if skip_any || pit_prefix => {
log::warn!(
"tables: corrupted framing header in version \
#{curr_version_id}; remaining records in this \
section are unrecoverable"
);
let recovered = u32::try_from(run.len()).unwrap_or(u32::MAX);
let processed = recovered.saturating_add(corrupted_in_run);
tables_dropped_to_corruption = tables_dropped_to_corruption
.saturating_add(table_count.saturating_sub(processed));
if !run.is_empty() {
level.push(run);
}
if !level.is_empty() {
levels.push(level);
}
break 'levels;
}
FramedRecordOutcome::TailTruncation => {
return Err(crate::Error::from(crate::io::Error::new(
crate::io::ErrorKind::UnexpectedEof,
"manifest tables record truncated mid-frame",
)));
}
FramedRecordOutcome::BadHeader => {
log::error!(
"manifest tables frame header rejected in version \
#{curr_version_id}: len exceeds MAX_FRAME_PAYLOAD"
);
return Err(crate::Error::InvalidHeader(
"manifest tables frame header",
));
}
FramedRecordOutcome::LenMismatch { got, expected } => {
log::error!(
"manifest tables frame len mismatch in version \
#{curr_version_id}: declared len={got}, \
expected fixed-size TABLE_ENTRY_PAYLOAD_LEN={expected} \
— schema drift, aborting regardless of recovery mode"
);
return Err(crate::Error::InvalidHeader(
"manifest tables frame len mismatch",
));
}
}
}
if !run.is_empty() {
level.push(run);
}
}
levels.push(level);
}
while levels.len() < usize::from(level_count) {
levels.push(Vec::new());
}
}
if tables_dropped_to_tail > 0
|| tables_dropped_to_corruption > 0
|| tables_truncated_headers > 0
{
log::warn!(
"manifest recovery summary for version #{curr_version_id}: \
{tables_dropped_to_tail} table record(s) dropped to tail-truncation, \
{tables_dropped_to_corruption} dropped to per-record corruption \
(skip_any/pit modes), \
{tables_truncated_headers} level/run header(s) truncated; \
recovered tree may be missing SSTs",
);
}
let mut blob_dropped_to_tail: u32 = 0;
let mut blob_dropped_to_corruption: u32 = 0;
let blob_file_ids = {
if archive.section("blob_files").is_none() {
log::error!(
"blob_files section not found in version #{curr_version_id} - maybe the file is corrupted?"
);
return Err(crate::Error::Unrecoverable);
}
let section_bytes = archive.read_section("blob_files")?;
let section_len: u64 = section_bytes.len() as u64;
let mut reader = crate::io::Cursor::new(section_bytes);
let blob_file_count = match reader.read_u32::<LittleEndian>() {
Ok(n) => n,
Err(e) if tolerate_tail && e.kind() == crate::io::ErrorKind::UnexpectedEof => {
log::warn!(
"blob_files section truncated before count header in version \
#{curr_version_id}; tail-tolerant mode produces 0 blob files"
);
0
}
Err(e) => return Err(e.into()),
};
let blob_section_capacity = section_len.saturating_sub(4) / FRAMED_BLOB_ENTRY_LEN;
if u64::from(blob_file_count) > blob_section_capacity {
if tolerate_tail {
log::warn!(
"blob_files: declared count={blob_file_count} exceeds section \
capacity (~{blob_section_capacity} entries) in version \
#{curr_version_id}; tail-tolerant mode walks \
bytes-actually-present and stops at the first EOF",
);
} else {
return Err(crate::Error::Unrecoverable);
}
}
let cap_hint =
usize::try_from(u64::from(blob_file_count).min(blob_section_capacity)).unwrap_or(0);
let mut blob_file_ids = Vec::with_capacity(cap_hint);
let mut blob_bytes_consumed: u64 = 4; let mut blob_corrupted: u32 = 0;
for _ in 0..blob_file_count {
let remaining = section_len.saturating_sub(blob_bytes_consumed);
let outcome = crate::version::framing::read_framed_record(
&mut reader,
remaining,
Some(BLOB_ENTRY_PAYLOAD_LEN),
&mut read_scratch,
)?;
match outcome {
FramedRecordOutcome::Ok => {
blob_bytes_consumed += crate::version::framing::FRAME_HEADER_LEN as u64
+ read_scratch.len() as u64;
match decode_blob_entry_payload(&read_scratch) {
Ok(entry) => blob_file_ids.push(entry),
Err(e) if skip_any => {
log::warn!(
"skip_any: blob_files record decode failed in version \
#{curr_version_id}: {e:?}; skipping",
);
blob_dropped_to_corruption =
blob_dropped_to_corruption.saturating_add(1);
blob_corrupted = blob_corrupted.saturating_add(1);
}
Err(e) if pit_prefix => {
log::warn!(
"pit: blob_files record decode failed in version \
#{curr_version_id}: {e:?}; accepting consistent prefix \
and dropping the rest of the blob_files section",
);
let recovered = u32::try_from(blob_file_ids.len()).unwrap_or(u32::MAX);
blob_dropped_to_corruption = blob_dropped_to_corruption
.saturating_add(blob_file_count.saturating_sub(recovered));
break;
}
Err(e) => return Err(e),
}
}
FramedRecordOutcome::TailTruncation if tolerate_tail => {
let recovered = u32::try_from(blob_file_ids.len()).unwrap_or(u32::MAX);
let processed = recovered.saturating_add(blob_corrupted);
blob_dropped_to_tail = blob_file_count.saturating_sub(processed);
break;
}
FramedRecordOutcome::ChecksumMismatch { bytes_consumed, .. } if skip_any => {
log::warn!(
"skip_any: blob_files record checksum mismatch \
({bytes_consumed} bytes) in version \
#{curr_version_id}, skipping"
);
blob_bytes_consumed += bytes_consumed;
blob_dropped_to_corruption = blob_dropped_to_corruption.saturating_add(1);
blob_corrupted = blob_corrupted.saturating_add(1);
}
FramedRecordOutcome::ChecksumMismatch { .. } if pit_prefix => {
log::warn!(
"pit: blob_files record checksum mismatch in version \
#{curr_version_id}; accepting consistent prefix and \
dropping the rest of the blob_files section"
);
let recovered = u32::try_from(blob_file_ids.len()).unwrap_or(u32::MAX);
blob_dropped_to_corruption = blob_file_count.saturating_sub(recovered);
break;
}
FramedRecordOutcome::ChecksumMismatch { expected, got, .. } => {
return Err(crate::Error::ManifestFrameChecksumMismatch {
section: "blob_files",
expected,
got,
});
}
FramedRecordOutcome::BadHeader if skip_any || pit_prefix => {
log::warn!(
"blob_files: corrupted framing header in version \
#{curr_version_id}; remaining records unrecoverable"
);
let recovered = u32::try_from(blob_file_ids.len()).unwrap_or(u32::MAX);
let processed = recovered.saturating_add(blob_corrupted);
blob_dropped_to_corruption = blob_dropped_to_corruption
.saturating_add(blob_file_count.saturating_sub(processed));
break;
}
FramedRecordOutcome::TailTruncation => {
return Err(crate::Error::from(crate::io::Error::new(
crate::io::ErrorKind::UnexpectedEof,
"manifest blob_files record truncated mid-frame",
)));
}
FramedRecordOutcome::BadHeader => {
log::error!(
"manifest blob_files frame header rejected in version \
#{curr_version_id}: len exceeds MAX_FRAME_PAYLOAD"
);
return Err(crate::Error::InvalidHeader(
"manifest blob_files frame header",
));
}
FramedRecordOutcome::LenMismatch { got, expected } => {
log::error!(
"manifest blob_files frame len mismatch in version \
#{curr_version_id}: declared len={got}, \
expected fixed-size BLOB_ENTRY_PAYLOAD_LEN={expected} \
— schema drift, aborting regardless of recovery mode"
);
return Err(crate::Error::InvalidHeader(
"manifest blob_files frame len mismatch",
));
}
}
}
blob_file_ids.sort_by_key(|(id, _)| *id);
blob_file_ids
};
if blob_dropped_to_tail > 0 || blob_dropped_to_corruption > 0 {
log::warn!(
"manifest blob_files recovery summary for version #{curr_version_id}: \
{blob_dropped_to_tail} blob-file record(s) dropped to tail-truncation, \
{blob_dropped_to_corruption} dropped to per-record corruption \
(skip_any/pit modes); recovered tree may be missing blob files",
);
}
debug_assert!(blob_file_ids.is_sorted_by_key(|(id, _)| id));
let gc_stats = {
if archive.section("blob_gc_stats").is_none() {
log::error!(
"blob_gc_stats section not found in version #{curr_version_id} - maybe the file is corrupted?"
);
return Err(crate::Error::Unrecoverable);
}
let section_bytes = archive.read_section("blob_gc_stats")?;
let mut reader = crate::io::Cursor::new(section_bytes);
match crate::blob_tree::FragmentationMap::decode_from(&mut reader) {
Ok(m) => m,
Err(crate::Error::Io(e))
if tolerate_tail && e.kind() == crate::io::ErrorKind::UnexpectedEof =>
{
log::warn!(
"blob_gc_stats section truncated in version #{curr_version_id}; \
tail-tolerant mode produces an empty FragmentationMap (GC stats \
will rebuild on the next compaction pass)"
);
crate::blob_tree::FragmentationMap::default()
}
Err(e) => return Err(e),
}
};
let restrictions = if archive.section("restrictions").is_some() {
parse_restrictions_section(&archive.read_section("restrictions")?)?
} else {
crate::HashMap::default()
};
let blob_restrictions = if archive.section("blob_restrictions").is_some() {
parse_blob_restrictions_section(&archive.read_section("blob_restrictions")?)?
} else {
crate::HashMap::default()
};
let retention_floor = if archive.section("retention_floor").is_some() {
parse_retention_floor_section(&archive.read_section("retention_floor")?)?
} else {
0
};
let mut recovery = Recovery {
tree_type: {
if archive.section("tree_type").is_none() {
log::error!(
"tree_type section not found in version #{curr_version_id} - maybe the file is corrupted?"
);
return Err(crate::Error::Unrecoverable);
}
let section_bytes = archive.read_section("tree_type")?;
let byte = section_bytes
.first()
.copied()
.ok_or(crate::Error::InvalidHeader("TreeType"))?;
TreeType::try_from(byte).map_err(|()| crate::Error::InvalidHeader("TreeType"))?
},
snapshot_id: curr_version_id,
curr_version_id,
table_ids: levels,
blob_file_ids,
gc_stats,
restrictions,
blob_restrictions,
retention_floor,
stats: RecoveryStats {
tables_dropped_to_tail,
tables_dropped_to_corruption,
tables_truncated_headers,
blob_dropped_to_tail,
blob_dropped_to_corruption,
},
};
let log_path = folder.join(format!("edits-{curr_version_id}"));
let edits = super::edit_log::replay_log(fs, &log_path, mode)?;
if !edits.is_empty() {
log::info!(
"Replaying {} manifest edit(s) on top of snapshot #{curr_version_id}",
edits.len(),
);
for edit in &edits {
recovery.apply_edit(edit)?;
}
}
Ok(recovery)
}
#[cfg(test)]
#[expect(
clippy::expect_used,
clippy::indexing_slicing,
reason = "test assertions"
)]
mod tests;