#![forbid(unsafe_code)]
use tempfile::TempDir;
use crate::core::candidate::{pick_cheapest, raw_candidate, zero_candidate};
use crate::core::extent::ChunkId;
use crate::entropy::palette::PaletteEncoder;
use crate::entropy::periodic::PeriodicEncoder;
use crate::entropy::sparse::SparseEncoder;
use crate::fsck::{FsckOptions, fsck};
use crate::rans::residual::RansEncoder;
use crate::store::transaction::CrashHooks;
use crate::store::{ExtentUpdate, Store, StoreConfig};
fn create_store(dir: &TempDir) -> Store {
let cfg = StoreConfig {
segment_size: 1024 * 1024,
..Default::default()
};
Store::create(dir.path(), &cfg, [0x22; 16]).unwrap()
}
fn encode_chunks(content: &[u8], store: &Store) -> Vec<ExtentUpdate> {
let limits = store.limits();
let policy = store.policy();
let chunk_class = limits.chunk_class as usize;
let mut updates = Vec::new();
let mut off = 0usize;
while off < content.len() {
let end = (off + chunk_class).min(content.len());
let chunk = &content[off..end];
let cid = ChunkId::of(chunk);
let ctx = crate::core::candidate::CandidateContext {
limits,
policy,
content_id: cid,
bases: &[],
dedup: None,
};
let mut cands = Vec::new();
if let Some(z) = zero_candidate(chunk, cid, limits) {
cands.push(z);
}
for enc in [
Box::new(SparseEncoder) as Box<dyn crate::core::candidate::Encoder>,
Box::new(PaletteEncoder),
Box::new(PeriodicEncoder),
Box::new(RansEncoder),
] {
cands.extend(enc.encode(chunk, &ctx));
}
if let Some(r) = raw_candidate(chunk, cid, limits) {
cands.push(r);
}
let best = pick_cheapest(&cands, policy).expect("at least raw");
updates.push(ExtentUpdate {
offset: off as u64,
descriptor: best.representation.clone(),
content_id: cid,
objects: best.objects.clone(),
});
off = end;
}
updates
}
fn write_file(store: &mut Store, ino: u64, content: &[u8]) {
let updates = encode_chunks(content, store);
store
.commit_file_extents(
ino,
updates,
Some(content.len() as u64),
&CrashHooks::none(),
)
.unwrap();
}
fn build_store(dir: &TempDir) -> Store {
let mut store = create_store(dir);
let inode = crate::store::inode::Inode::new_file(1000, 1000, 0o644);
let mut tx = store.begin_tx().unwrap();
Store::put_inode_in_tx(&mut tx, 3, &inode).unwrap();
tx.commit(&CrashHooks::none()).unwrap();
let mut content = Vec::new();
content.extend_from_slice(b"fsck-fsck-fsck-".repeat(300).as_slice());
content.extend_from_slice(&[0u8; 8192]);
for i in 0..9000u32 {
content.push((i % 23) as u8);
}
write_file(&mut store, 3, &content);
store
}
#[test]
fn clean_store_passes_fsck() {
let dir = TempDir::new().unwrap();
let store = build_store(&dir);
let content = store.read_file(3, 0, 1024).unwrap();
assert!(!content.is_empty());
drop(store);
let report = fsck(dir.path(), &FsckOptions::default()).unwrap();
assert!(
report.is_clean(),
"clean store must pass fsck:\n{}",
report.render()
);
assert!(report.segments_scanned >= 1);
assert!(report.records_scanned >= 1);
assert!(report.inodes_verified >= 1);
assert!(report.live_objects >= 2);
}
#[test]
fn torn_tail_is_warning_and_repairable() {
let dir = TempDir::new().unwrap();
let store = build_store(&dir);
drop(store);
let segments = crate::store::segment::list_segments(dir.path()).unwrap();
let last = *segments.last().unwrap();
let path = crate::store::segment::segment_path(dir.path(), last);
let rec = crate::format::record::encode(
crate::format::version::RecordTag::Data,
0,
None,
&[0x77u8; 200],
);
{
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
use std::io::Write;
f.write_all(&rec).unwrap();
f.sync_all().unwrap();
}
let f = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
let len = f.metadata().unwrap().len();
f.set_len(len - 37).unwrap();
drop(f);
let report = fsck(dir.path(), &FsckOptions::default()).unwrap();
assert!(report.warning_count() >= 1);
assert!(
report
.issues
.iter()
.any(|i| i.message.contains("torn tail")),
"expected torn-tail warning:\n{}",
report.render()
);
let opts = FsckOptions {
repair_torn_tails: true,
..Default::default()
};
let report = fsck(dir.path(), &opts).unwrap();
assert!(!report.repaired.is_empty());
let store = Store::open(dir.path(), &StoreConfig::default()).unwrap();
let content = store.read_file(3, 0, 60).unwrap();
assert_eq!(content, b"fsck-fsck-fsck-".repeat(4));
}
#[test]
fn mid_file_corruption_is_an_error() {
let dir = TempDir::new().unwrap();
let store = build_store(&dir);
drop(store);
let path = crate::store::segment::segment_path(dir.path(), 0);
let mut f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
use std::io::{Seek, SeekFrom};
f.seek(SeekFrom::Start(crate::format::record::HEADER_SIZE + 3))
.unwrap();
let mut b = [0u8; 1];
use std::io::Read;
f.read_exact(&mut b).unwrap();
b[0] ^= 0xFF;
f.seek(SeekFrom::Start(crate::format::record::HEADER_SIZE + 3))
.unwrap();
use std::io::Write;
f.write_all(&b).unwrap();
drop(f);
let report = fsck(dir.path(), &FsckOptions::default());
assert!(report.is_err() || !report.unwrap().is_clean());
}
#[test]
fn deleted_root_object_is_detected() {
let dir = TempDir::new().unwrap();
let store = build_store(&dir);
drop(store);
for seq in crate::store::segment::list_segments(dir.path()).unwrap() {
crate::store::segment::delete_segment(dir.path(), seq).unwrap();
}
let report = fsck(dir.path(), &FsckOptions::default());
assert!(report.is_err() || !report.unwrap().is_clean());
}
#[test]
fn corrupt_superblock_slot_is_ignored_with_warning() {
let dir = TempDir::new().unwrap();
let store = build_store(&dir);
drop(store);
let sb_path = dir.path().join("superblock");
let mut f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&sb_path)
.unwrap();
use std::io::{Seek, SeekFrom};
f.seek(SeekFrom::Start(10)).unwrap();
let mut b = [0u8; 1];
use std::io::Read;
f.read_exact(&mut b).unwrap();
b[0] ^= 0xFF;
f.seek(SeekFrom::Start(10)).unwrap();
use std::io::Write;
f.write_all(&b).unwrap();
drop(f);
let report = fsck(dir.path(), &FsckOptions::default()).unwrap();
assert!(
report.superblock_slots_valid == 2 || report.warning_count() >= 1,
"expected a warning for the corrupt slot:\n{}",
report.render()
);
let store = Store::open(dir.path(), &StoreConfig::default()).unwrap();
let content = store.read_file(3, 0, 60).unwrap();
assert_eq!(content, b"fsck-fsck-fsck-".repeat(4));
}
#[test]
fn overwritten_data_is_reported_as_unreachable() {
let dir = TempDir::new().unwrap();
let mut store = build_store(&dir);
for i in 0..5 {
let content = format!("version-{i}:{}", "y".repeat(2000)).into_bytes();
write_file(&mut store, 3, &content);
}
drop(store);
let report = fsck(dir.path(), &FsckOptions::default()).unwrap();
assert!(
report.leaked_objects > 0,
"overwrites must produce unreachable objects:\n{}",
report.render()
);
}
#[test]
fn verify_materialized_full_chain() {
let dir = TempDir::new().unwrap();
let store = build_store(&dir);
drop(store);
let opts = FsckOptions {
verify_materialized: true,
..Default::default()
};
let report = fsck(dir.path(), &opts).unwrap();
assert!(
report.is_clean(),
"materialized chain must pass:\n{}",
report.render()
);
}
#[test]
fn fsck_refuses_mounted_store() {
let dir = TempDir::new().unwrap();
let _store = build_store(&dir); let res = crate::fsck::ensure_unmounted(dir.path());
assert!(
res.is_err(),
"fsck must refuse a store whose mount lock is held"
);
}