use crate::io::{LittleEndian, ReadBytesExt};
use crate::{
Checksum, SeqNo, TableId, TreeType, coding::Decode, config::ManifestRecoveryMode, fs::Fs,
version::VersionId, vlog::BlobFileId,
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::path::Path;
const TABLE_ENTRY_PAYLOAD_LEN: u32 = 1 + 4 + 8 + 1 + 16 + 8 + 8;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct TablePlace {
pub level: u8,
pub run: u32,
}
pub fn encode_table_entry_payload(
payload: &mut Vec<u8>,
place: TablePlace,
id: TableId,
checksum: Checksum,
global_seqno: SeqNo,
recency: TableId,
) -> crate::Result<()> {
use crate::io::WriteBytesExt;
payload.write_u8(place.level)?;
payload.write_u32::<LittleEndian>(place.run)?;
payload.write_u64::<LittleEndian>(id)?;
payload.write_u8(0)?; payload.write_u128::<LittleEndian>(checksum.into_u128())?;
payload.write_u64::<LittleEndian>(global_seqno)?;
payload.write_u64::<LittleEndian>(recency)?;
Ok(())
}
const BLOB_ENTRY_PAYLOAD_LEN: u32 = 8 + 1 + 16;
fn decode_table_entry_payload(payload: &[u8]) -> crate::Result<(TablePlace, 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 level = cursor.read_u8()?;
let run = cursor.read_u32::<LittleEndian>()?;
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>()?;
let recency = cursor.read_u64::<LittleEndian>()?;
Ok((
TablePlace { level, run },
RecoveredTable {
id,
checksum,
global_seqno,
recency,
},
))
}
fn place_table(
levels: &mut [Vec<Vec<RecoveredTable>>],
previous: &mut Option<TablePlace>,
place: TablePlace,
table: RecoveredTable,
) -> crate::Result<()> {
const ERR: crate::Error = crate::Error::InvalidHeader("tables record out of place");
let level = levels.get_mut(usize::from(place.level)).ok_or(ERR)?;
if *previous == Some(place) {
level.last_mut().ok_or(ERR)?.push(table);
return Ok(());
}
let follows = match *previous {
None => place.run == 0,
Some(prev) if prev.level == place.level => Some(place.run) == prev.run.checked_add(1),
Some(prev) => prev.level < place.level && place.run == 0,
};
if !follows {
return Err(ERR);
}
level.push(vec![table]);
*previous = Some(place);
Ok(())
}
fn read_tables_section(
bytes: Vec<u8>,
curr_version_id: VersionId,
scratch: &mut Vec<u8>,
) -> crate::Result<Vec<Vec<Vec<RecoveredTable>>>> {
use crate::version::framing::{FRAME_HEADER_LEN, FramedRecordOutcome, read_framed_record};
let section_len = bytes.len() as u64;
let mut reader = crate::io::Cursor::new(bytes);
let level_count = reader
.read_u8()
.map_err(|_| crate::Error::InvalidHeader("tables section level count"))?;
let mut consumed: u64 = 1;
let mut levels = vec![Vec::new(); usize::from(level_count)];
let mut previous = None;
while consumed < section_len {
match read_framed_record(
&mut reader,
section_len - consumed,
Some(TABLE_ENTRY_PAYLOAD_LEN),
scratch,
)? {
FramedRecordOutcome::Ok => {
consumed += FRAME_HEADER_LEN as u64 + scratch.len() as u64;
let (place, table) = decode_table_entry_payload(scratch)?;
place_table(&mut levels, &mut previous, place, table)?;
}
FramedRecordOutcome::TailTruncation => {
return Err(crate::Error::from(crate::io::Error::new(
crate::io::ErrorKind::UnexpectedEof,
"manifest tables record truncated mid-frame",
)));
}
FramedRecordOutcome::ChecksumMismatch { expected, got, .. } => {
return Err(crate::Error::ManifestFrameChecksumMismatch {
section: "tables",
expected,
got,
});
}
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 {expected}"
);
return Err(crate::Error::InvalidHeader(
"manifest tables frame len mismatch",
));
}
}
}
Ok(levels)
}
fn read_blob_files_section(
bytes: Vec<u8>,
curr_version_id: VersionId,
scratch: &mut Vec<u8>,
) -> crate::Result<Vec<(BlobFileId, Checksum)>> {
use crate::version::framing::{FRAME_HEADER_LEN, FramedRecordOutcome, read_framed_record};
const ERR: crate::Error = crate::Error::InvalidHeader("blob_files section");
const FRAMED_ENTRY_LEN: u64 = FRAME_HEADER_LEN as u64 + BLOB_ENTRY_PAYLOAD_LEN as u64;
let section_len = bytes.len() as u64;
let mut reader = crate::io::Cursor::new(bytes);
let count = reader.read_u32::<LittleEndian>().map_err(|_| ERR)?;
if section_len - 4 != u64::from(count) * FRAMED_ENTRY_LEN {
return Err(ERR);
}
let mut consumed: u64 = 4;
let mut blob_file_ids = Vec::with_capacity(count as usize);
for _ in 0..count {
match read_framed_record(
&mut reader,
section_len - consumed,
Some(BLOB_ENTRY_PAYLOAD_LEN),
scratch,
)? {
FramedRecordOutcome::Ok => {
consumed += FRAMED_ENTRY_LEN;
blob_file_ids.push(decode_blob_entry_payload(scratch)?);
}
FramedRecordOutcome::ChecksumMismatch { expected, got, .. } => {
return Err(crate::Error::ManifestFrameChecksumMismatch {
section: "blob_files",
expected,
got,
});
}
FramedRecordOutcome::TailTruncation
| FramedRecordOutcome::BadHeader
| FramedRecordOutcome::LenMismatch { .. } => {
log::error!("manifest blob_files record malformed in version #{curr_version_id}");
return Err(ERR);
}
}
}
blob_file_ids.sort_by_key(|(id, _)| *id);
Ok(blob_file_ids)
}
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)
}
fn parse_dicts_section(mut bytes: &[u8]) -> crate::Result<Vec<crate::file::DictId>> {
const ERR: crate::Error = crate::Error::InvalidHeader("dicts section");
let r = &mut bytes;
let count = r.read_u32::<LittleEndian>().map_err(|_| ERR)? as usize;
if count > r.len() / size_of::<crate::file::DictId>() {
return Err(ERR);
}
let mut ids = Vec::with_capacity(count);
for _ in 0..count {
ids.push(r.read_u32::<LittleEndian>().map_err(|_| ERR)?);
}
if !r.is_empty() {
return Err(ERR);
}
Ok(ids)
}
fn check_format(
archive: &mut crate::manifest_blocks::reader::ManifestArchiveReader,
) -> crate::Result<()> {
let format = archive.read_section("format_version")?;
let format = format
.first()
.copied()
.ok_or(crate::Error::InvalidHeader("format_version"))?;
crate::FormatVersion::try_from(format).map_err(|()| crate::Error::InvalidVersion(format))?;
Ok(())
}
fn check_identity(
archive: &mut crate::manifest_blocks::reader::ManifestArchiveReader,
id: VersionId,
) -> crate::Result<()> {
if named_version(archive)? != id {
return Err(crate::Error::ManifestFooterInvalid(
"manifest snapshot names another version than its file",
));
}
Ok(())
}
fn named_version(
archive: &mut crate::manifest_blocks::reader::ManifestArchiveReader,
) -> crate::Result<VersionId> {
check_format(archive)?;
let named = archive.read_section("version_id")?;
let named = <[u8; 8]>::try_from(named.as_slice()).map_err(|_| {
crate::Error::ManifestFooterInvalid("manifest version_id section is not 8 bytes")
})?;
Ok(u64::from_le_bytes(named))
}
pub fn verify_snapshot(
folder: &Path,
id: VersionId,
fs: &dyn Fs,
encryption: Option<alloc::sync::Arc<dyn crate::encryption::EncryptionProvider>>,
) -> crate::Result<()> {
let mut archive = open_snapshot(folder, id, fs, encryption)?;
check_identity(&mut archive, id)
}
#[cfg(feature = "std")]
pub fn owed_repair_report(
folder: &Path,
fs: &dyn Fs,
encryption: Option<alloc::sync::Arc<dyn crate::encryption::EncryptionProvider>>,
) -> crate::Result<Option<OwedReport>> {
let Some(id) = installed_snapshot(folder, fs)? else {
return Ok(None);
};
let unreadable = |e: crate::Error| {
if e.is_environmental() {
Err(e)
} else {
log::warn!("snapshot v{id} cannot say what repair report it owes: {e}");
Ok(Some(OwedReport::Unreadable(id)))
}
};
let mut archive = match open_snapshot(folder, id, fs, encryption) {
Ok(archive) => archive,
Err(e) => return unreadable(e),
};
let lists_report = archive.section("repair_report").is_some();
match named_version(&mut archive) {
Ok(named) if named == id => {}
Ok(_) => {
return unreadable(crate::Error::ManifestFooterInvalid(
"manifest snapshot names another version than its file",
));
}
Err(e) if e.is_environmental() => return Err(e),
Err(e) => log::warn!("snapshot v{id} does not say which version it is: {e}"),
}
if !lists_report {
return Ok(None);
}
match archive.read_section("repair_report") {
Ok(bytes) => Ok(Some(OwedReport::Readable(bytes))),
Err(e) => unreadable(e),
}
}
#[cfg(feature = "std")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OwedReport {
Readable(Vec<u8>),
Unreadable(VersionId),
}
const PREVIOUS_FORMAT_POINTER: &str = "current";
const PREVIOUS_FORMAT: u8 = 5;
#[must_use]
pub fn snapshot_id_of(name: &str) -> Option<VersionId> {
crate::file::canonical_id(name.strip_prefix('v')?)
}
#[must_use]
pub fn log_id_of(name: &str) -> Option<VersionId> {
crate::file::canonical_id(name.strip_prefix("edits-")?)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ManifestFile {
Snapshot(VersionId),
Temporary(VersionId),
Log(VersionId),
}
#[must_use]
pub fn manifest_file_of(name: &str) -> Option<ManifestFile> {
if let Some(id) = snapshot_id_of(name) {
return Some(ManifestFile::Snapshot(id));
}
if let Some(id) = name
.strip_suffix(super::SNAPSHOT_TEMPORARY_SUFFIX)
.and_then(snapshot_id_of)
{
return Some(ManifestFile::Temporary(id));
}
log_id_of(name).map(ManifestFile::Log)
}
pub fn installed_snapshot(folder: &Path, fs: &dyn Fs) -> crate::Result<Option<VersionId>> {
let entries = match fs.read_dir(folder) {
Ok(entries) => entries,
Err(e) if e.kind() == crate::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
};
if entries
.iter()
.any(|entry| !entry.is_dir && entry.file_name == PREVIOUS_FORMAT_POINTER)
{
return Err(crate::Error::InvalidVersion(PREVIOUS_FORMAT));
}
Ok(entries
.iter()
.filter(|entry| !entry.is_dir)
.filter_map(|entry| snapshot_id_of(&entry.file_name))
.max())
}
pub fn open_snapshot(
folder: &Path,
id: VersionId,
fs: &dyn Fs,
encryption: Option<alloc::sync::Arc<dyn crate::encryption::EncryptionProvider>>,
) -> crate::Result<crate::manifest_blocks::reader::ManifestArchiveReader> {
crate::manifest_blocks::reader::ManifestArchiveReader::open(
&folder.join(alloc::format!("v{id}")),
fs,
alloc::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
encryption,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecoveredTable {
pub id: TableId,
pub checksum: Checksum,
pub global_seqno: SeqNo,
pub recency: TableId,
}
#[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,
pub dicts: Vec<crate::file::DictId>,
pub repair_report: Option<Vec<u8>>,
}
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,
recency: t.recency,
})
.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;
}
if let Some(dicts) = &edit.dicts {
self.dicts.clone_from(dicts);
}
self.curr_version_id = edit.new_version_id;
Ok(())
}
}
pub fn recover(
folder: &Path,
fs: &dyn Fs,
mode: ManifestRecoveryMode,
encryption: Option<alloc::sync::Arc<dyn crate::encryption::EncryptionProvider>>,
) -> crate::Result<Recovery> {
let curr_version_id = installed_snapshot(folder, fs)?.ok_or(
crate::Error::ManifestFooterInvalid("the tree holds no installed manifest snapshot"),
)?;
log::info!(
"Recovering current manifest v{curr_version_id} in {} (mode={mode:?})",
folder.display(),
);
let mut archive = open_snapshot(folder, curr_version_id, fs, encryption)?;
check_identity(&mut archive, curr_version_id)?;
let mut read_scratch: Vec<u8> = Vec::with_capacity(64);
let levels = read_tables_section(
archive.read_section("tables")?,
curr_version_id,
&mut read_scratch,
)?;
let blob_file_ids = read_blob_files_section(
archive.read_section("blob_files")?,
curr_version_id,
&mut read_scratch,
)?;
debug_assert!(blob_file_ids.is_sorted_by_key(|(id, _)| id));
let gc_stats = crate::blob_tree::FragmentationMap::decode_from(&mut crate::io::Cursor::new(
archive.read_section("blob_gc_stats")?,
))?;
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 dicts = if archive.section("dicts").is_some() {
parse_dicts_section(&archive.read_section("dicts")?)?
} else {
Vec::new()
};
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,
dicts,
repair_report: if archive.section("repair_report").is_some() {
Some(archive.read_section("repair_report")?)
} else {
None
},
};
let log_path = super::edit_log::log_path(folder, 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;