Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
124 lines
5.0 KiB
Rust
124 lines
5.0 KiB
Rust
//! Ponta a ponta: RAID 5 de 4 discos (um perdido) com ext4 dentro e um arquivo apagado.
|
|
|
|
mod common;
|
|
use common::*;
|
|
use guara_core::assemble::{assemble_source, Assembly};
|
|
use guara_core::scan::{self, Outcome};
|
|
use guara_core::source::{FileSource, Source};
|
|
use guara_core::*;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
|
|
fn have(t: &str) -> bool {
|
|
Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false)
|
|
}
|
|
|
|
fn data(seed: u64, n: usize) -> Vec<u8> {
|
|
let mut x = seed | 1;
|
|
(0..n)
|
|
.map(|_| {
|
|
x ^= x << 13;
|
|
x ^= x >> 7;
|
|
x ^= x << 17;
|
|
(x >> 24) as u8
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
const CHUNK: usize = 64 * 1024;
|
|
|
|
fn md_super(n: u32, dev: u32, data_off: u64, sectors: u64) -> Vec<u8> {
|
|
let mut b = vec![0u8; 4096];
|
|
b[0..4].copy_from_slice(&0xA92B_4EFCu32.to_le_bytes());
|
|
b[4..8].copy_from_slice(&1u32.to_le_bytes());
|
|
b[16..32].fill(0x5a);
|
|
b[72..76].copy_from_slice(&5u32.to_le_bytes());
|
|
b[76..80].copy_from_slice(&2u32.to_le_bytes());
|
|
b[80..88].copy_from_slice(§ors.to_le_bytes());
|
|
b[88..92].copy_from_slice(&((CHUNK / 512) as u32).to_le_bytes());
|
|
b[92..96].copy_from_slice(&n.to_le_bytes());
|
|
b[128..136].copy_from_slice(&data_off.to_le_bytes());
|
|
b[136..144].copy_from_slice(§ors.to_le_bytes());
|
|
b[160..164].copy_from_slice(&dev.to_le_bytes());
|
|
b[200..208].copy_from_slice(&50u64.to_le_bytes());
|
|
b[220..224].copy_from_slice(&n.to_le_bytes());
|
|
for i in 0..n as usize {
|
|
b[256 + i * 2..258 + i * 2].copy_from_slice(&(i as u16).to_le_bytes());
|
|
}
|
|
b
|
|
}
|
|
|
|
#[test]
|
|
fn deleted_file_on_degraded_raid5() {
|
|
if !(have("mke2fs") && have("debugfs")) {
|
|
eprintln!("SKIP: e2fsprogs ausente");
|
|
return;
|
|
}
|
|
init();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let root = dir.path().join("root");
|
|
std::fs::create_dir_all(root.join("fotos")).unwrap();
|
|
let doc = data(5, 300_000);
|
|
std::fs::write(root.join("fotos/ferias.dat"), &doc).unwrap();
|
|
std::fs::write(root.join("fotos/outra.dat"), data(6, 9000)).unwrap();
|
|
let plain = dir.path().join("plain.img");
|
|
let n = 4usize;
|
|
let dd = n - 1;
|
|
let rows = 40usize;
|
|
let logical_len = rows * dd * CHUNK;
|
|
std::fs::File::create(&plain).unwrap().set_len(logical_len as u64).unwrap();
|
|
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success());
|
|
Command::new("debugfs").arg("-w").arg("-R").arg("rm /fotos/ferias.dat").arg(&plain).output().unwrap();
|
|
let logical = std::fs::read(&plain).unwrap();
|
|
|
|
// RAID 5 left-symmetric
|
|
let data_off = 2048u64; // setores
|
|
let mut disks = vec![vec![0u8; data_off as usize * 512 + rows * CHUNK]; n];
|
|
for row in 0..rows {
|
|
let pd = n - 1 - row % n;
|
|
let mut p = vec![0u8; CHUNK];
|
|
for d in 0..dd {
|
|
let disk = (pd + 1 + d) % n;
|
|
let src = &logical[(row * dd + d) * CHUNK..(row * dd + d + 1) * CHUNK];
|
|
disks[disk][data_off as usize * 512 + row * CHUNK..][..CHUNK].copy_from_slice(src);
|
|
for i in 0..CHUNK {
|
|
p[i] ^= src[i];
|
|
}
|
|
}
|
|
disks[pd][data_off as usize * 512 + row * CHUNK..][..CHUNK].copy_from_slice(&p);
|
|
}
|
|
let mut paths = vec![];
|
|
for (i, d) in disks.iter_mut().enumerate() {
|
|
if i == 2 {
|
|
continue; // disco que morreu
|
|
}
|
|
let sb = md_super(n as u32, i as u32, data_off, (rows * CHUNK / 512) as u64);
|
|
d[4096..8192].copy_from_slice(&sb);
|
|
let p = dir.path().join(format!("disk{i}.img"));
|
|
std::fs::write(&p, &d).unwrap();
|
|
paths.push(p);
|
|
}
|
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&paths[0]).unwrap());
|
|
let asm = Assembly {
|
|
members: paths[1..].iter().map(|p| p.to_string_lossy().to_string()).collect(),
|
|
..Default::default()
|
|
};
|
|
let a = assemble_source(base, &asm).unwrap();
|
|
assert!(a.notes.iter().any(|n| n.contains("degradado")), "{:?}", a.notes);
|
|
assert_eq!(a.extra_parts.len(), 1);
|
|
assert_eq!(a.extra_parts[0].fs, FsKind::Ext);
|
|
let opts = ScanOptions { mode: ScanMode::Deep, extra_parts: a.extra_parts.clone(), covered: a.covered.clone(), ..Default::default() };
|
|
let mut sink = Collect::default();
|
|
let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink);
|
|
assert!(matches!(out, Outcome::Completed));
|
|
let f = sink.found.iter().find(|f| f.name == "ferias.dat").unwrap_or_else(|| panic!("{:?}", sink.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
|
assert_eq!(f.size, 300_000);
|
|
let dest = tempfile::tempdir().unwrap();
|
|
let rep = recover::recover(a.source.as_ref(), &[f], dest.path(), &|| false, &mut |_, _, _| {}).unwrap();
|
|
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
|
let p = &rep.recovered[0].path;
|
|
let p = if Path::new(p).is_absolute() { Path::new(p).to_path_buf() } else { dest.path().join(p) };
|
|
assert!(std::fs::read(p).unwrap() == doc);
|
|
}
|