use super::*;
use crate::{
fs::MemFs, manifest_blocks::writer::ManifestArchiveWriter, runtime_config::RuntimeConfig,
};
use std::sync::Arc;
fn fresh_fs() -> MemFs {
let fs = MemFs::new();
fs.create_dir_all(Path::new("/m")).unwrap();
fs
}
#[test]
fn validate_block_header_fits_rejects_understated_exact_slot() {
use crate::coding::Encode;
use crate::table::block::{BlockType, Header};
let header = Header {
data_length: 4,
uncompressed_length: 4,
..Header::test_dummy(BlockType::ManifestFooter)
};
let mut buf = header.encode_into_vec();
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&[0xAB, 0xCD]);
assert!(
validate_block_header_fits(&buf, HeaderContext::SectionExact).is_err(),
"an understated header in an exact-fit section slot must be rejected",
);
assert!(
validate_block_header_fits(&buf, HeaderContext::FooterExact).is_err(),
"an understated header in an exact-fit footer slot must be rejected",
);
assert!(
validate_block_header_fits(&buf, HeaderContext::FooterPadded).is_err(),
"padded slot must reject non-zero bytes past the declared block size",
);
let mut zero_padded = header.encode_into_vec();
zero_padded.extend_from_slice(&[0u8; 4]); zero_padded.extend_from_slice(&[0u8; 2]); assert!(
validate_block_header_fits(&zero_padded, HeaderContext::FooterPadded).is_ok(),
"padded slot accepts a smaller declared size with genuine zero padding",
);
}
fn write_manifest(fs: &MemFs, path: &Path, runtime: RuntimeConfig, sections: &[(&str, &[u8])]) {
let mut w = ManifestArchiveWriter::create(
path,
fs,
Arc::new(runtime),
None,
crate::fs::SyncMode::Normal,
)
.unwrap();
for (name, data) in sections {
w.start(name).unwrap();
use std::io::Write;
w.write_all(data).unwrap();
}
w.finish().unwrap();
}
#[test]
fn reader_opens_clean_manifest_via_tail() {
let fs = fresh_fs();
let path = Path::new("/m/clean");
write_manifest(
&fs,
path,
RuntimeConfig::default(),
&[("format_version", &[5]), ("tree_type", &[0])],
);
let reader = ManifestArchiveReader::open(
path,
&fs,
std::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
None,
)
.unwrap();
assert_eq!(reader.source(), FooterSource::Tail);
assert!(reader.section("format_version").is_some());
assert!(reader.section("tree_type").is_some());
assert!(reader.section("nonexistent").is_none());
}
#[test]
fn reader_reads_section_bytes_verbatim() {
let fs = fresh_fs();
let path = Path::new("/m/roundtrip");
write_manifest(
&fs,
path,
RuntimeConfig::default(),
&[
("format_version", &[5]),
("comparator_name", b"u64-big-endian"),
],
);
let mut reader = ManifestArchiveReader::open(
path,
&fs,
std::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
None,
)
.unwrap();
assert_eq!(reader.read_section("format_version").unwrap(), vec![5]);
assert_eq!(
reader.read_section("comparator_name").unwrap(),
b"u64-big-endian".to_vec(),
);
}
#[test]
fn reader_falls_back_to_head_mirror_when_tail_corrupt() {
let fs = fresh_fs();
let path = Path::new("/m/tail_corrupt");
write_manifest(
&fs,
path,
RuntimeConfig::default(), &[("format_version", &[5])],
);
let mut file = fs
.open(path, &FsOpenOptions::new().write(true).read(true))
.unwrap();
let size = file.metadata().unwrap().len;
file.seek(SeekFrom::Start(size - 4)).unwrap();
use std::io::Write;
file.write_all(&[0xFF, 0xFF, 0xFF, 0xFF]).unwrap();
file.sync_all().unwrap();
drop(file);
let reader = ManifestArchiveReader::open(
path,
&fs,
std::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
None,
)
.unwrap();
assert_eq!(
reader.source(),
FooterSource::Head,
"reader should have fallen back to the head mirror"
);
assert!(reader.section("format_version").is_some());
}
#[test]
fn reader_fails_when_tail_corrupt_and_no_mirror() {
let fs = fresh_fs();
let path = Path::new("/m/tail_corrupt_no_mirror");
let runtime = RuntimeConfig {
manifest_footer_mirror: false,
..RuntimeConfig::default()
};
write_manifest(&fs, path, runtime, &[("format_version", &[5])]);
let mut file = fs
.open(path, &FsOpenOptions::new().write(true).read(true))
.unwrap();
let size = file.metadata().unwrap().len;
file.seek(SeekFrom::Start(size - 4)).unwrap();
use std::io::Write;
file.write_all(&[0xFF, 0xFF, 0xFF, 0xFF]).unwrap();
file.sync_all().unwrap();
drop(file);
let err = ManifestArchiveReader::open(
path,
&fs,
std::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
None,
)
.expect_err("must reject");
assert!(matches!(err, crate::Error::ManifestFooterInvalid(_)));
}
#[test]
fn reader_rejects_request_for_missing_section() {
let fs = fresh_fs();
let path = Path::new("/m/missing");
write_manifest(&fs, path, RuntimeConfig::default(), &[("a", &[1])]);
let mut reader = ManifestArchiveReader::open(
path,
&fs,
std::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
None,
)
.unwrap();
let err = reader
.read_section("does_not_exist")
.expect_err("missing section must error");
assert!(matches!(err, crate::Error::ManifestSectionInvalid(_)));
}
#[test]
fn reader_isolates_corruption_to_one_section_other_sections_readable() {
let fs = fresh_fs();
let path = Path::new("/m/isolated");
write_manifest(
&fs,
path,
RuntimeConfig::default(),
&[
("a", &[1, 2, 3, 4]),
("b", &[5, 6, 7, 8]),
("c", &[9, 10, 11, 12]),
],
);
let b_offset = {
let reader =
ManifestArchiveReader::open(path, &fs, Arc::new(RuntimeConfig::default()), None)
.unwrap();
let entry = reader.section("b").expect("b section is in TOC");
entry.block_offset
};
let payload_off =
b_offset + Header::header_len(crate::table::block::BlockType::Manifest) as u64;
{
let mut file = fs
.open(path, &FsOpenOptions::new().write(true).read(true))
.unwrap();
file.seek(SeekFrom::Start(payload_off)).unwrap();
let mut byte = [0u8; 1];
file.read_exact(&mut byte).unwrap();
file.seek(SeekFrom::Start(payload_off)).unwrap();
file.write_all(&[byte[0] ^ 0xFF]).unwrap();
file.sync_all().unwrap();
}
let mut reader =
ManifestArchiveReader::open(path, &fs, Arc::new(RuntimeConfig::default()), None).unwrap();
let a_bytes = reader.read_section("a").unwrap();
assert_eq!(a_bytes, vec![1, 2, 3, 4], "section a survives");
let c_bytes = reader.read_section("c").unwrap();
assert_eq!(c_bytes, vec![9, 10, 11, 12], "section c survives");
let b_err = reader
.read_section("b")
.expect_err("section b decoded but should have failed XXH3");
log::debug!("section b corruption surfaced as: {b_err:?}");
}
#[test]
fn reader_rejects_files_smaller_than_head_reservation() {
let fs = fresh_fs();
let path = Path::new("/m/too_small");
let mut file = fs
.open(path, &FsOpenOptions::new().write(true).create_new(true))
.unwrap();
use std::io::Write;
file.write_all(&[0u8; 100]).unwrap(); file.sync_all().unwrap();
drop(file);
let err = ManifestArchiveReader::open(
path,
&fs,
std::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
None,
)
.expect_err("must reject");
assert!(matches!(err, crate::Error::Unrecoverable));
}
#[cfg(feature = "encryption")]
#[test]
fn reader_falls_back_to_head_mirror_for_encrypted_manifest() {
use crate::encryption::{Aes256GcmProvider, EncryptionProvider};
let fs = fresh_fs();
let path = Path::new("/m/enc_tail_corrupt");
let key = [42u8; 32];
let enc: Arc<dyn EncryptionProvider> = Arc::new(Aes256GcmProvider::new(&key));
let mut w = ManifestArchiveWriter::create(
path,
&fs,
Arc::new(RuntimeConfig::default()),
Some(Arc::clone(&enc)),
crate::fs::SyncMode::Normal,
)
.unwrap();
w.start("format_version").unwrap();
use std::io::Write;
w.write_all(&[5u8]).unwrap();
w.finish().unwrap();
let mut file = fs
.open(path, &FsOpenOptions::new().write(true).read(true))
.unwrap();
let size = file.metadata().unwrap().len;
file.seek(SeekFrom::Start(size - 4)).unwrap();
file.write_all(&[0xFF, 0xFF, 0xFF, 0xFF]).unwrap();
file.sync_all().unwrap();
drop(file);
let reader =
ManifestArchiveReader::open(path, &fs, Arc::new(RuntimeConfig::default()), Some(enc))
.expect("encrypted head-mirror fallback must decrypt cleanly");
assert_eq!(
reader.source(),
FooterSource::Head,
"reader should have fallen back to the head mirror"
);
assert!(reader.section("format_version").is_some());
}
#[test]
fn reader_recovers_from_head_when_tail_hint_missing() {
let fs = fresh_fs();
let path = Path::new("/m/head_only");
write_manifest(
&fs,
path,
RuntimeConfig::default(),
&[("format_version", &[5])],
);
let file = fs
.open(path, &FsOpenOptions::new().write(true).read(true))
.unwrap();
file.set_len(HEAD_FOOTER_RESERVED_SIZE).unwrap();
file.sync_all().unwrap();
drop(file);
let reader = ManifestArchiveReader::open(path, &fs, Arc::new(RuntimeConfig::default()), None)
.expect("head-only manifest must recover via head fallback");
assert_eq!(
reader.source(),
FooterSource::Head,
"reader should have fallen back to the head mirror"
);
assert!(reader.section("format_version").is_some());
}
#[cfg(feature = "page_ecc")]
#[test]
fn reader_reads_section_when_manifest_ecc_enabled_returns_verbatim_bytes() {
let fs = fresh_fs();
let path = Path::new("/m/ecc_plain");
let runtime = RuntimeConfig {
page_ecc: true,
..RuntimeConfig::default()
};
write_manifest(
&fs,
path,
runtime.clone(),
&[("format_version", &[5]), ("tree_type", &[0])],
);
let mut reader = ManifestArchiveReader::open(path, &fs, Arc::new(runtime), None).unwrap();
assert_eq!(reader.read_section("format_version").unwrap(), vec![5]);
assert_eq!(reader.read_section("tree_type").unwrap(), vec![0]);
}
#[cfg(all(feature = "page_ecc", feature = "encryption"))]
#[test]
fn reader_reads_encrypted_section_when_manifest_ecc_enabled_returns_verbatim_bytes() {
use crate::encryption::{Aes256GcmProvider, EncryptionProvider};
let fs = fresh_fs();
let path = Path::new("/m/ecc_enc");
let enc: Arc<dyn EncryptionProvider> = Arc::new(Aes256GcmProvider::new(&[7u8; 32]));
let runtime = RuntimeConfig {
page_ecc: true,
..RuntimeConfig::default()
};
let mut w = ManifestArchiveWriter::create(
path,
&fs,
Arc::new(runtime.clone()),
Some(Arc::clone(&enc)),
crate::fs::SyncMode::Normal,
)
.unwrap();
w.start("format_version").unwrap();
use std::io::Write;
w.write_all(&[5u8]).unwrap();
w.finish().unwrap();
let mut reader = ManifestArchiveReader::open(path, &fs, Arc::new(runtime), Some(enc)).unwrap();
assert_eq!(reader.read_section("format_version").unwrap(), vec![5]);
}