Formatação (cargo fmt) e ajustes do clippy para o CI da 1.0.0
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74218e7beb
commit
beb83fef92
49 files changed
+3806
-854
No files matched your search
@@ -11,7 +11,11 @@ fn dir() -> PathBuf {
|
||||
#[test]
|
||||
fn reads_reference_images_and_stored_hash_matches() {
|
||||
let mut seen = 0;
|
||||
for name in ["Base-Linear.aff4", "Base-Allocated.aff4", "Base-Linear-AllHashes.aff4"] {
|
||||
for name in [
|
||||
"Base-Linear.aff4",
|
||||
"Base-Allocated.aff4",
|
||||
"Base-Linear-AllHashes.aff4",
|
||||
] {
|
||||
let p = dir().join(name);
|
||||
if !p.exists() {
|
||||
eprintln!("SKIP {name}");
|
||||
@@ -19,7 +23,11 @@ fn reads_reference_images_and_stored_hash_matches() {
|
||||
}
|
||||
let a = Aff4Source::open(&p).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(a.size(), 268_435_456, "{name}");
|
||||
assert_eq!(a.verify_stream_md5().unwrap(), Some(true), "{name}: hash do fluxo");
|
||||
assert_eq!(
|
||||
a.verify_stream_md5().unwrap(),
|
||||
Some(true),
|
||||
"{name}: hash do fluxo"
|
||||
);
|
||||
// setor 0 legível pelo caminho genérico
|
||||
let s = open_path(&p).unwrap();
|
||||
let head = read_vec(s.as_ref(), 0, 4096).unwrap();
|
||||
@@ -27,7 +35,12 @@ fn reads_reference_images_and_stored_hash_matches() {
|
||||
// leitura em vãos do mapa devolve zeros
|
||||
let far = read_vec(s.as_ref(), 200 << 20, 512).unwrap();
|
||||
assert_eq!(far.len(), 512);
|
||||
eprintln!("{name}: {} {:?}", a.info.get("Examinador").cloned().unwrap_or_default(), guara_core::partition::detect_fs_in(&read_vec(s.as_ref(), 0, 4096).unwrap()).map(|x| x.0));
|
||||
eprintln!(
|
||||
"{name}: {} {:?}",
|
||||
a.info.get("Examinador").cloned().unwrap_or_default(),
|
||||
guara_core::partition::detect_fs_in(&read_vec(s.as_ref(), 0, 4096).unwrap())
|
||||
.map(|x| x.0)
|
||||
);
|
||||
seen += 1;
|
||||
}
|
||||
if seen == 0 {
|
||||
@@ -43,17 +56,36 @@ fn roundtrip_with_snappy_blocks_and_odd_size() {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
if i % 100_000 < 60_000 { 0 } else { (x >> 24) as u8 }
|
||||
if i % 100_000 < 60_000 {
|
||||
0
|
||||
} else {
|
||||
(x >> 24) as u8
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
data.truncate(5_000_001);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p = dir.path().join("t.aff4");
|
||||
let opts = Aff4Options { case_name: "caso".into(), examiner: "Lucas".into(), notes: "teste".into(), ..Default::default() };
|
||||
let rep = write_aff4(&MemSource(data.clone()), &p, &opts, &|| false, &mut |_, _| {}).unwrap();
|
||||
let opts = Aff4Options {
|
||||
case_name: "caso".into(),
|
||||
examiner: "Lucas".into(),
|
||||
notes: "teste".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let rep = write_aff4(
|
||||
&MemSource(data.clone()),
|
||||
&p,
|
||||
&opts,
|
||||
&|| false,
|
||||
&mut |_, _| {},
|
||||
)
|
||||
.unwrap();
|
||||
let a = Aff4Source::open(&p).unwrap();
|
||||
assert_eq!(a.size(), data.len() as u64);
|
||||
assert_eq!(a.info.get("md5").map(String::as_str), Some(rep.md5.as_str()));
|
||||
assert_eq!(
|
||||
a.info.get("md5").map(String::as_str),
|
||||
Some(rep.md5.as_str())
|
||||
);
|
||||
assert_eq!(a.info.get("Examinador").map(String::as_str), Some("Lucas"));
|
||||
assert_eq!(a.verify_stream_md5().unwrap(), Some(true));
|
||||
let got = read_vec(&a, 0, data.len()).unwrap();
|
||||
|
||||
@@ -20,7 +20,10 @@ fn reads_the_live_tree_of_the_reference_image() {
|
||||
eprintln!("{files:#?}");
|
||||
assert!(!files.is_empty());
|
||||
let names: Vec<&str> = files.iter().map(|f| f.0.as_str()).collect();
|
||||
assert!(names.iter().any(|n| n.ends_with("passwords.txt")), "{names:?}");
|
||||
assert!(
|
||||
names.iter().any(|n| n.ends_with("passwords.txt")),
|
||||
"{names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -28,16 +31,31 @@ fn checksum_matches_real_blocks() {
|
||||
let Some(p) = img() else { return };
|
||||
let d = std::fs::read(p).unwrap();
|
||||
let b = &d[0..4096];
|
||||
assert_eq!(fletcher64(&b[8..]), u64::from_le_bytes(b[0..8].try_into().unwrap()));
|
||||
assert_eq!(
|
||||
fletcher64(&b[8..]),
|
||||
u64::from_le_bytes(b[0..8].try_into().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_scan_is_clean_on_pristine_volume() {
|
||||
let Some(p) = img() else { return };
|
||||
init();
|
||||
let c = scan_path(&p, &ScanOptions { mode: ScanMode::Quick, ..Default::default() });
|
||||
let c = scan_path(
|
||||
&p,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Quick,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(c.layout.as_ref().unwrap().partitions[0].fs, FsKind::Apfs);
|
||||
eprintln!("{:?}", c.found.iter().map(|f| (&f.name, &f.dir, f.size)).collect::<Vec<_>>());
|
||||
eprintln!(
|
||||
"{:?}",
|
||||
c.found
|
||||
.iter()
|
||||
.map(|f| (&f.name, &f.dir, f.size))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
// --- manipulação bruta de nós (independente do módulo) ---
|
||||
@@ -65,7 +83,10 @@ fn read_node(b: &[u8]) -> (u16, Vec<Entry>) {
|
||||
for i in 0..n {
|
||||
let o = 56 + toc_off + i * 8;
|
||||
let (ko, kl, vo, vl) = (le16(b, o), le16(b, o + 2), le16(b, o + 4), le16(b, o + 6));
|
||||
out.push((b[key_base + ko..key_base + ko + kl].to_vec(), b[val_end - vo..val_end - vo + vl].to_vec()));
|
||||
out.push((
|
||||
b[key_base + ko..key_base + ko + kl].to_vec(),
|
||||
b[val_end - vo..val_end - vo + vl].to_vec(),
|
||||
));
|
||||
}
|
||||
(flags, out)
|
||||
}
|
||||
@@ -131,7 +152,12 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
||||
c
|
||||
});
|
||||
let vol_oid = le64(&nx, 184);
|
||||
let vol_paddr = cont_entries.iter().filter(|(k, _)| le64(k, 0) == vol_oid).map(|(_, v)| le64(v, 8)).last().unwrap();
|
||||
let vol_paddr = cont_entries
|
||||
.iter()
|
||||
.filter(|(k, _)| le64(k, 0) == vol_oid)
|
||||
.map(|(_, v)| le64(v, 8))
|
||||
.next_back()
|
||||
.unwrap();
|
||||
let vsb = blk(&d, vol_paddr);
|
||||
let (vol_omap_paddr, root_oid) = (le64(&vsb, 128), le64(&vsb, 136));
|
||||
let vol_omap = blk(&d, vol_omap_paddr);
|
||||
@@ -140,23 +166,39 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
||||
let vt_orig = vt.clone();
|
||||
fixed_to_var(&mut vt);
|
||||
let (_, omap_entries) = read_node(&vt);
|
||||
let (root_idx, root_paddr) = omap_entries.iter().enumerate().filter(|(_, (k, _))| le64(k, 0) == root_oid).map(|(i, (_, v))| (i, le64(v, 8))).last().unwrap();
|
||||
let (root_idx, root_paddr) = omap_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (k, _))| le64(k, 0) == root_oid)
|
||||
.map(|(i, (_, v))| (i, le64(v, 8)))
|
||||
.next_back()
|
||||
.unwrap();
|
||||
let root = blk(&d, root_paddr);
|
||||
assert_eq!(le16(&root, 34), 0, "raiz deveria ser folha nesta imagem");
|
||||
let (_, recs) = read_node(&root);
|
||||
// inode do arquivo
|
||||
let victim = b"passwords.txt";
|
||||
let ino = recs.iter().find_map(|(k, v)| {
|
||||
let hdr = le64(k, 0);
|
||||
((hdr >> 60) == 9 && k.len() > 12 && k[12..].starts_with(victim)).then(|| le64(v, 0))
|
||||
}).expect("entrada de diretório");
|
||||
let private_id = recs.iter().find_map(|(k, v)| ((le64(k, 0) >> 60) == 3 && (le64(k, 0) & 0x0FFF_FFFF_FFFF_FFFF) == ino).then(|| le64(v, 8))).unwrap();
|
||||
let ino = recs
|
||||
.iter()
|
||||
.find_map(|(k, v)| {
|
||||
let hdr = le64(k, 0);
|
||||
((hdr >> 60) == 9 && k.len() > 12 && k[12..].starts_with(victim)).then(|| le64(v, 0))
|
||||
})
|
||||
.expect("entrada de diretório");
|
||||
let private_id = recs
|
||||
.iter()
|
||||
.find_map(|(k, v)| {
|
||||
((le64(k, 0) >> 60) == 3 && (le64(k, 0) & 0x0FFF_FFFF_FFFF_FFFF) == ino)
|
||||
.then(|| le64(v, 8))
|
||||
})
|
||||
.unwrap();
|
||||
let kept: Vec<Entry> = recs
|
||||
.iter()
|
||||
.filter(|(k, v)| {
|
||||
let hdr = le64(k, 0);
|
||||
let (id, ty) = (hdr & 0x0FFF_FFFF_FFFF_FFFF, hdr >> 60);
|
||||
let drec_of_victim = ty == 9 && k.len() > 12 && k[12..].starts_with(victim) && le64(v, 0) == ino;
|
||||
let drec_of_victim =
|
||||
ty == 9 && k.len() > 12 && k[12..].starts_with(victim) && le64(v, 0) == ino;
|
||||
!(id == ino && ty != 9 || id == private_id && ty == 8 || drec_of_victim)
|
||||
})
|
||||
.cloned()
|
||||
@@ -164,7 +206,9 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
||||
assert!(kept.len() < recs.len());
|
||||
let newnode = write_node(&root, &kept);
|
||||
// bloco livre para a nova versão
|
||||
let free = (10..d.len() / bs).find(|&n| d[n * bs..(n + 1) * bs].iter().all(|&x| x == 0) && n != vol_tree_paddr as usize).expect("bloco livre");
|
||||
let free = (10..d.len() / bs)
|
||||
.find(|&n| d[n * bs..(n + 1) * bs].iter().all(|&x| x == 0) && n != vol_tree_paddr as usize)
|
||||
.expect("bloco livre");
|
||||
d[free * bs..(free + 1) * bs].copy_from_slice(&newnode);
|
||||
// aponta o mapa de objetos para a nova versão (mesmo xid), editando o valor da entrada
|
||||
let toc_off = le16(&vt_orig, 40);
|
||||
@@ -185,8 +229,26 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
||||
let src = FileSource::open(&p2).unwrap();
|
||||
assert!(!live_paths(&src, 0).iter().any(|f| f.0 == "passwords.txt"));
|
||||
init();
|
||||
let c = scan_path(&p2, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
||||
let f = c.found.iter().find(|f| f.origin == Origin::Apfs && f.name == "passwords.txt").unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::<Vec<_>>()));
|
||||
let c = scan_path(
|
||||
&p2,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Deep,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let f = c
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.origin == Origin::Apfs && f.name == "passwords.txt")
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{:?}",
|
||||
c.found
|
||||
.iter()
|
||||
.map(|f| (&f.name, f.origin))
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
});
|
||||
assert_eq!(f.size, 116);
|
||||
let out = dir.path().join("out");
|
||||
let rep = recover_all(&p2, std::slice::from_ref(f), &out);
|
||||
@@ -194,7 +256,12 @@ fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
||||
let rp = PathBuf::from(&rep.recovered[0].path);
|
||||
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
||||
assert_eq!(got.len(), 116);
|
||||
assert!(String::from_utf8_lossy(&got).chars().all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t'), "{got:?}");
|
||||
assert!(
|
||||
String::from_utf8_lossy(&got)
|
||||
.chars()
|
||||
.all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t'),
|
||||
"{got:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Converte a tabela de entradas fixas (4 bytes) de um nó de omap para o formato variável (8 bytes) em memória.
|
||||
@@ -212,7 +279,10 @@ fn fixed_to_var(b: &mut Vec<u8>) {
|
||||
for i in 0..n {
|
||||
let o = 56 + toc_off + i * 4;
|
||||
let (ko, vo) = (le16(b, o), le16(b, o + 2));
|
||||
entries.push((b[key_base + ko..key_base + ko + 16].to_vec(), b[val_end - vo..val_end - vo + 16].to_vec()));
|
||||
entries.push((
|
||||
b[key_base + ko..key_base + ko + 16].to_vec(),
|
||||
b[val_end - vo..val_end - vo + 16].to_vec(),
|
||||
));
|
||||
}
|
||||
let mut nb = b.clone();
|
||||
nb[32..34].copy_from_slice(&(flags & !4).to_le_bytes());
|
||||
|
||||
@@ -13,10 +13,13 @@ fn dir() -> Option<PathBuf> {
|
||||
d.join("images.conf").exists().then_some(d)
|
||||
}
|
||||
|
||||
fn conf(d: &PathBuf) -> BTreeMap<String, BTreeMap<String, String>> {
|
||||
fn conf(d: &std::path::Path) -> BTreeMap<String, BTreeMap<String, String>> {
|
||||
let mut m: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
|
||||
let mut cur = String::new();
|
||||
for l in std::fs::read_to_string(d.join("images.conf")).unwrap().lines() {
|
||||
for l in std::fs::read_to_string(d.join("images.conf"))
|
||||
.unwrap()
|
||||
.lines()
|
||||
{
|
||||
let l = l.trim();
|
||||
if let Some(n) = l.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
|
||||
cur = n.to_string();
|
||||
@@ -34,7 +37,11 @@ fn check_fs(src: &dyn Source, what: &str) {
|
||||
}
|
||||
let head = read_vec(src, 0, 4096).unwrap();
|
||||
let fs = detect_fs_in(&head);
|
||||
assert!(fs.is_some(), "{what}: setor 0 aberto não é um sistema de arquivos: {:02x?}", &head[..32]);
|
||||
assert!(
|
||||
fs.is_some(),
|
||||
"{what}: setor 0 aberto não é um sistema de arquivos: {:02x?}",
|
||||
&head[..32]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -66,19 +73,23 @@ fn opens_all_reference_images_with_password_and_recovery_key() {
|
||||
}
|
||||
let mut views = vec![];
|
||||
if !pw.is_empty() {
|
||||
let u = bitlocker::unlock(src.clone(), &[Secret::Password(pw.clone())]).unwrap_or_else(|e| panic!("{name} senha: {e}"));
|
||||
let u = bitlocker::unlock(src.clone(), &[Secret::Password(pw.clone())])
|
||||
.unwrap_or_else(|e| panic!("{name} senha: {e}"));
|
||||
check_fs(u.source.as_ref(), &format!("{name} (senha)"));
|
||||
views.push(u);
|
||||
}
|
||||
if !rp.is_empty() {
|
||||
let u = bitlocker::unlock(src.clone(), &[Secret::RecoveryKey(rp.clone())]).unwrap_or_else(|e| panic!("{name} recuperação: {e}"));
|
||||
let u = bitlocker::unlock(src.clone(), &[Secret::RecoveryKey(rp.clone())])
|
||||
.unwrap_or_else(|e| panic!("{name} recuperação: {e}"));
|
||||
check_fs(u.source.as_ref(), &format!("{name} (recuperação)"));
|
||||
views.push(u);
|
||||
}
|
||||
if let Some(g) = c.get("SK_VMK_GUID") {
|
||||
let f = d.join(format!("{}.BEK", g.to_uppercase()));
|
||||
if f.exists() {
|
||||
let u = bitlocker::unlock(src.clone(), &[Secret::KeyFile(std::fs::read(f).unwrap())]).unwrap_or_else(|e| panic!("{name} BEK: {e}"));
|
||||
let u =
|
||||
bitlocker::unlock(src.clone(), &[Secret::KeyFile(std::fs::read(f).unwrap())])
|
||||
.unwrap_or_else(|e| panic!("{name} BEK: {e}"));
|
||||
check_fs(u.source.as_ref(), &format!("{name} (BEK)"));
|
||||
views.push(u);
|
||||
}
|
||||
@@ -86,10 +97,17 @@ fn opens_all_reference_images_with_password_and_recovery_key() {
|
||||
assert!(!views.is_empty(), "{name}: nenhum protetor testável");
|
||||
let first = read_vec(views[0].source.as_ref(), 0, 1 << 20).unwrap();
|
||||
for v in &views[1..] {
|
||||
assert!(read_vec(v.source.as_ref(), 0, 1 << 20).unwrap() == first, "{name}: protetores divergem");
|
||||
assert!(
|
||||
read_vec(v.source.as_ref(), 0, 1 << 20).unwrap() == first,
|
||||
"{name}: protetores divergem"
|
||||
);
|
||||
}
|
||||
opened += 1;
|
||||
eprintln!("ok {name}: {} ({} protetores)", views[0].description, views.len());
|
||||
eprintln!(
|
||||
"ok {name}: {} ({} protetores)",
|
||||
views[0].description,
|
||||
views.len()
|
||||
);
|
||||
}
|
||||
assert!(opened > 0);
|
||||
}
|
||||
@@ -4,13 +4,17 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use guara_core::fs::btrfs::{crc32c, live_paths, lzo1x_decompress};
|
||||
use guara_core::source::{FileSource, Source};
|
||||
use guara_core::source::FileSource;
|
||||
use guara_core::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn have(t: &str) -> bool {
|
||||
Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false)
|
||||
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> {
|
||||
@@ -51,10 +55,22 @@ fn build() -> Option<Img> {
|
||||
std::fs::write(root.join(p), c).unwrap();
|
||||
}
|
||||
let path = dir.path().join("b.img");
|
||||
std::fs::File::create(&path).unwrap().set_len(128 << 20).unwrap();
|
||||
let o = Command::new("mkfs.btrfs").args(["-q", "-f", "--rootdir"]).arg(&root).arg(&path).output().unwrap();
|
||||
std::fs::File::create(&path)
|
||||
.unwrap()
|
||||
.set_len(128 << 20)
|
||||
.unwrap();
|
||||
let o = Command::new("mkfs.btrfs")
|
||||
.args(["-q", "-f", "--rootdir"])
|
||||
.arg(&root)
|
||||
.arg(&path)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
|
||||
Some(Img { _dir: dir, path, files })
|
||||
Some(Img {
|
||||
_dir: dir,
|
||||
path,
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -63,7 +79,10 @@ fn lists_the_live_tree_with_paths_and_sizes() {
|
||||
let src = FileSource::open(&i.path).unwrap();
|
||||
let listed = live_paths(&src, 0);
|
||||
for (p, c) in &i.files {
|
||||
let f = listed.iter().find(|x| x.0 == *p).unwrap_or_else(|| panic!("{p} ausente: {listed:?}"));
|
||||
let f = listed
|
||||
.iter()
|
||||
.find(|x| x.0 == *p)
|
||||
.unwrap_or_else(|| panic!("{p} ausente: {listed:?}"));
|
||||
assert_eq!(f.1, c.len() as u64, "{p}");
|
||||
assert!(!f.2);
|
||||
}
|
||||
@@ -164,10 +183,16 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
|
||||
let root_leaf = (t.root_tree as usize, t.root_tree_logical);
|
||||
let ext_leaf = (t.extent_tree as usize, t.extent_tree_logical);
|
||||
let to_logical = |phys: usize| -> u64 {
|
||||
let c = t.chunks.iter().find(|c| (phys as u64) >= c.2 && (phys as u64) < c.2 + c.1).expect("chunk");
|
||||
let c = t
|
||||
.chunks
|
||||
.iter()
|
||||
.find(|c| (phys as u64) >= c.2 && (phys as u64) < c.2 + c.1)
|
||||
.expect("chunk");
|
||||
c.0 + (phys as u64 - c.2)
|
||||
};
|
||||
let fl = Leaf { raw: img[fs_leaf.0..fs_leaf.0 + nodesize].to_vec() };
|
||||
let fl = Leaf {
|
||||
raw: img[fs_leaf.0..fs_leaf.0 + nodesize].to_vec(),
|
||||
};
|
||||
// inode do arquivo apagado
|
||||
let victim = "relatorio.dat";
|
||||
let mut ino = 0;
|
||||
@@ -194,14 +219,23 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
|
||||
// Novo bloco livre para a nova versão: primeiro bloco do metadata sem folha válida e todo zero.
|
||||
let free_slot = (0..img.len() / nodesize)
|
||||
.map(|n| n * nodesize)
|
||||
.find(|&o| o > fs_leaf.0 && img[o..o + nodesize].iter().all(|&x| x == 0) && t.chunks.iter().any(|c| (o as u64) >= c.2 && (o as u64) < c.2 + c.1) && o < fs_leaf.0 + (8 << 20))
|
||||
.find(|&o| {
|
||||
o > fs_leaf.0
|
||||
&& img[o..o + nodesize].iter().all(|&x| x == 0)
|
||||
&& t.chunks
|
||||
.iter()
|
||||
.any(|c| (o as u64) >= c.2 && (o as u64) < c.2 + c.1)
|
||||
&& o < fs_leaf.0 + (8 << 20)
|
||||
})
|
||||
.expect("bloco livre");
|
||||
// logical = bytenr da folha antiga + (offset físico - offset físico antigo): mesmo chunk
|
||||
let new_bytenr = to_logical(free_slot);
|
||||
let newleaf = fl.without(|o, _| o == ino, new_bytenr);
|
||||
img[free_slot..free_slot + nodesize].copy_from_slice(&newleaf.raw);
|
||||
// Aponta o ROOT_ITEM da árvore 5 para a nova versão (e sobe a geração), na folha da árvore raiz.
|
||||
let mut rl = Leaf { raw: img[root_leaf.0..root_leaf.0 + nodesize].to_vec() };
|
||||
let mut rl = Leaf {
|
||||
raw: img[root_leaf.0..root_leaf.0 + nodesize].to_vec(),
|
||||
};
|
||||
for k in 0..rl.nritems() {
|
||||
let (obj, ty, _, off, _) = rl.item(k);
|
||||
if obj == 5 && ty == 132 {
|
||||
@@ -212,7 +246,9 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
|
||||
rl.seal();
|
||||
img[root_leaf.0..root_leaf.0 + nodesize].copy_from_slice(&rl.raw);
|
||||
// Libera o extent de dados no extent tree (em outro endereço da mesma folha).
|
||||
let el = Leaf { raw: img[ext_leaf.0..ext_leaf.0 + nodesize].to_vec() };
|
||||
let el = Leaf {
|
||||
raw: img[ext_leaf.0..ext_leaf.0 + nodesize].to_vec(),
|
||||
};
|
||||
let newext = el.without(|o, ty| o == data_bytenr && ty == 168, ext_leaf.1);
|
||||
let _ = to_logical;
|
||||
img[ext_leaf.0..ext_leaf.0 + nodesize].copy_from_slice(&newext.raw);
|
||||
@@ -221,15 +257,33 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
|
||||
let p2 = i._dir.path().join("deleted.img");
|
||||
std::fs::write(&p2, &img).unwrap();
|
||||
init();
|
||||
let c = scan_path(&p2, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
||||
let c = scan_path(
|
||||
&p2,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Deep,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let f = c
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.name == victim && f.origin == Origin::Btrfs)
|
||||
.unwrap_or_else(|| panic!("não achou: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::<Vec<_>>()));
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"não achou: {:?}",
|
||||
c.found
|
||||
.iter()
|
||||
.map(|f| (&f.name, f.origin))
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
});
|
||||
assert_eq!(f.size, 150_000);
|
||||
assert_eq!(f.dir.as_deref(), Some("/docs"));
|
||||
assert!(f.confidence >= Confidence::Medium && f.note.as_deref().unwrap_or("").contains("livres"), "{:?}", f.note);
|
||||
assert!(
|
||||
f.confidence >= Confidence::Medium && f.note.as_deref().unwrap_or("").contains("livres"),
|
||||
"{:?}",
|
||||
f.note
|
||||
);
|
||||
let out = i._dir.path().join("out");
|
||||
let rep = recover_all(&p2, std::slice::from_ref(f), &out);
|
||||
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||
@@ -237,7 +291,10 @@ fn deleted_file_survives_in_the_previous_version_of_the_leaf() {
|
||||
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
||||
assert!(got == i.files[0].1);
|
||||
// Os arquivos que continuam existindo não aparecem como apagados.
|
||||
assert!(!c.found.iter().any(|f| f.name == "fica.dat" && f.origin == Origin::Btrfs));
|
||||
assert!(!c
|
||||
.found
|
||||
.iter()
|
||||
.any(|f| f.name == "fica.dat" && f.origin == Origin::Btrfs));
|
||||
let _ = Path::new("");
|
||||
}
|
||||
|
||||
@@ -247,7 +304,10 @@ fn lzo_literals_and_a_match() {
|
||||
let mut s = vec![17 + 20];
|
||||
s.extend((0..20u8).map(|i| b'a' + i));
|
||||
s.extend_from_slice(&[0x11, 0, 0]);
|
||||
assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), (0..20u8).map(|i| b'a' + i).collect::<Vec<_>>());
|
||||
assert_eq!(
|
||||
lzo1x_decompress(&s, 4096).unwrap(),
|
||||
(0..20u8).map(|i| b'a' + i).collect::<Vec<_>>()
|
||||
);
|
||||
// "abcd" + M2 (dist 4, len 4) + fim
|
||||
let s = vec![17 + 4, b'a', b'b', b'c', b'd', 0x6C, 0x00, 0x11, 0, 0];
|
||||
assert_eq!(lzo1x_decompress(&s, 4096).unwrap(), b"abcdabcd");
|
||||
@@ -265,16 +325,35 @@ fn packed_extents_decode_zlib_and_zstd() {
|
||||
let z = e.finish().unwrap();
|
||||
let mut disk = vec![0u8; 4096];
|
||||
disk.extend_from_slice(&z);
|
||||
let p = Packed { offset: 4096, disk_len: z.len() as u64, ram_len: plain.len() as u64, skip: 1000, len: 5000, codec: PackedCodec::Zlib };
|
||||
let p = Packed {
|
||||
offset: 4096,
|
||||
disk_len: z.len() as u64,
|
||||
ram_len: plain.len() as u64,
|
||||
skip: 1000,
|
||||
len: 5000,
|
||||
codec: PackedCodec::Zlib,
|
||||
};
|
||||
assert_eq!(p.read(&MemSource(disk)).unwrap(), &plain[1000..6000]);
|
||||
// zstd, se o utilitário existir
|
||||
if have("zstd") {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let f = dir.path().join("p.bin");
|
||||
std::fs::write(&f, &plain).unwrap();
|
||||
assert!(Command::new("zstd").args(["-q", "-f"]).arg(&f).status().unwrap().success());
|
||||
assert!(Command::new("zstd")
|
||||
.args(["-q", "-f"])
|
||||
.arg(&f)
|
||||
.status()
|
||||
.unwrap()
|
||||
.success());
|
||||
let z = std::fs::read(dir.path().join("p.bin.zst")).unwrap();
|
||||
let p = Packed { offset: 0, disk_len: z.len() as u64, ram_len: plain.len() as u64, skip: 0, len: plain.len() as u64, codec: PackedCodec::Zstd };
|
||||
let p = Packed {
|
||||
offset: 0,
|
||||
disk_len: z.len() as u64,
|
||||
ram_len: plain.len() as u64,
|
||||
skip: 0,
|
||||
len: plain.len() as u64,
|
||||
codec: PackedCodec::Zstd,
|
||||
};
|
||||
assert!(p.read(&MemSource(z)).unwrap() == plain);
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,23 @@ fn photo_n(w: u16, h: u16, seed: u64, shift: i32, nmask: u64) -> Vec<u8> {
|
||||
let noise = ((x >> 40) & nmask) as i32;
|
||||
let fx = xx as f32 / w as f32;
|
||||
let fy = yy as f32 / h as f32;
|
||||
let wave = ((fx * 9.0 + seed as f32 * 0.37).sin() * (fy * 7.0 + 1.3).cos() * 40.0) as i32 + ((fx * 31.0).sin() * 8.0) as i32;
|
||||
let wave = ((fx * 9.0 + seed as f32 * 0.37).sin() * (fy * 7.0 + 1.3).cos() * 40.0)
|
||||
as i32
|
||||
+ ((fx * 31.0).sin() * 8.0) as i32;
|
||||
let base = 110 + shift + wave + (xx * 40 / w as i32);
|
||||
let blob = if ((xx - 300).pow(2) + (yy - 200).pow(2)) < 5000 { 60 } else { 0 };
|
||||
let blob = if ((xx - 300).pow(2) + (yy - 200).pow(2)) < 5000 {
|
||||
60
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let v = (base + blob + noise).clamp(0, 255) as u8;
|
||||
rgb.extend_from_slice(&[v, v.saturating_sub(10), (v as i32 / 2 + noise) as u8]);
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
jpeg_encoder::Encoder::new(&mut out, 92).encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb).unwrap();
|
||||
jpeg_encoder::Encoder::new(&mut out, 92)
|
||||
.encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb)
|
||||
.unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
@@ -71,7 +79,10 @@ fn rebuilds_a_jpeg_split_around_another_one() {
|
||||
// O fragmento correto precisa estar marcado.
|
||||
assert!(idx.get(a2_off));
|
||||
eprintln!("TRUE a2_off={a2_off}");
|
||||
let pool = Pool { index: &idx, used: &[Extent::new(b_off, bb.len() as u64)] };
|
||||
let pool = Pool {
|
||||
index: &idx,
|
||||
used: &[Extent::new(b_off, bb.len() as u64)],
|
||||
};
|
||||
match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) {
|
||||
Analysis::Rebuilt { extents, size } => {
|
||||
assert_eq!(size as usize, a.len(), "tamanho");
|
||||
@@ -79,13 +90,18 @@ fn rebuilds_a_jpeg_split_around_another_one() {
|
||||
assert_eq!(extents[0], Extent::new(a_off, split as u64));
|
||||
assert_eq!(extents[1].offset, a2_off);
|
||||
let mut got = disk[a_off as usize..a_off as usize + split].to_vec();
|
||||
got.extend_from_slice(&disk[a2_off as usize..a2_off as usize + (size as usize - split)]);
|
||||
got.extend_from_slice(
|
||||
&disk[a2_off as usize..a2_off as usize + (size as usize - split)],
|
||||
);
|
||||
assert!(got == a, "conteúdo recomposto difere");
|
||||
}
|
||||
other => panic!("esperava Rebuilt, veio {other:?}"),
|
||||
}
|
||||
// O outro JPEG, íntegro, continua íntegro.
|
||||
let pool2 = Pool { index: &idx, used: &[] };
|
||||
let pool2 = Pool {
|
||||
index: &idx,
|
||||
used: &[],
|
||||
};
|
||||
match analyze_jpeg(&src, b_off, 4 << 20, &pool2, true) {
|
||||
Analysis::Intact { len } => assert_eq!(len as usize, b.len()),
|
||||
other => panic!("B deveria estar íntegro: {other:?}"),
|
||||
@@ -129,7 +145,10 @@ fn rebuilds_three_fragments_among_many_decoys() {
|
||||
.filter(|(i, _)| *i != 2) // o fragmento do meio de A não é de outro arquivo
|
||||
.map(|(_, (o, l))| Extent::new(*o, *l))
|
||||
.collect();
|
||||
let pool = Pool { index: &idx, used: &used };
|
||||
let pool = Pool {
|
||||
index: &idx,
|
||||
used: &used,
|
||||
};
|
||||
match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) {
|
||||
Analysis::Rebuilt { extents, size } => {
|
||||
assert_eq!(size as usize, n);
|
||||
@@ -167,17 +186,36 @@ fn deep_scan_recovers_fragmented_jpeg_end_to_end() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let img = dir.path().join("d.img");
|
||||
std::fs::write(&img, &disk).unwrap();
|
||||
let c = common::scan_path(&img, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
||||
let c = common::scan_path(
|
||||
&img,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Deep,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let jpgs: Vec<_> = c.found.iter().filter(|f| f.ext == "jpg").collect();
|
||||
assert_eq!(jpgs.len(), 2, "{:?}", jpgs.iter().map(|f| (f.size, f.first_offset(), f.confidence)).collect::<Vec<_>>());
|
||||
let fa = jpgs.iter().find(|f| f.first_offset() == Some(a_off as u64)).unwrap();
|
||||
assert_eq!(
|
||||
jpgs.len(),
|
||||
2,
|
||||
"{:?}",
|
||||
jpgs.iter()
|
||||
.map(|f| (f.size, f.first_offset(), f.confidence))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
let fa = jpgs
|
||||
.iter()
|
||||
.find(|f| f.first_offset() == Some(a_off as u64))
|
||||
.unwrap();
|
||||
assert_eq!(fa.size as usize, n, "{:?}", fa.note);
|
||||
let out = dir.path().join("out");
|
||||
let rep = common::recover_all(&img, std::slice::from_ref(*fa), &out);
|
||||
let p = std::path::PathBuf::from(&rep.recovered[0].path);
|
||||
let got = std::fs::read(if p.is_absolute() { p } else { out.join(p) }).unwrap();
|
||||
assert!(got == a, "JPEG recuperado difere do original");
|
||||
let fb = jpgs.iter().find(|f| f.first_offset() != Some(a_off as u64)).unwrap();
|
||||
let fb = jpgs
|
||||
.iter()
|
||||
.find(|f| f.first_offset() != Some(a_off as u64))
|
||||
.unwrap();
|
||||
assert_eq!(fb.size as usize, photo_n(1600, 1200, 77, 30, 3).len());
|
||||
let _ = (b_len, recover::RecoverReport::default());
|
||||
}
|
||||
@@ -11,13 +11,26 @@ use std::process::{Command, Stdio};
|
||||
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)
|
||||
Command::new("sh")
|
||||
.args(["-c", &format!("command -v {t}")])
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn run_stdin(c: &mut Command, input: &str) -> bool {
|
||||
use std::io::Write;
|
||||
let mut ch = c.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn().unwrap();
|
||||
ch.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();
|
||||
let mut ch = c
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
ch.stdin
|
||||
.take()
|
||||
.unwrap()
|
||||
.write_all(input.as_bytes())
|
||||
.unwrap();
|
||||
ch.wait().unwrap().success()
|
||||
}
|
||||
|
||||
@@ -47,30 +60,94 @@ fn deleted_file_inside_luks2_is_recovered_with_password() {
|
||||
std::fs::write(root.join("docs/segredo.dat"), &secret_doc).unwrap();
|
||||
std::fs::write(root.join("docs/fica.dat"), data(12, 5000)).unwrap();
|
||||
let plain = dir.path().join("plain.img");
|
||||
std::fs::File::create(&plain).unwrap().set_len(8 << 20).unwrap();
|
||||
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success());
|
||||
std::fs::File::create(&plain)
|
||||
.unwrap()
|
||||
.set_len(8 << 20)
|
||||
.unwrap();
|
||||
assert!(Command::new("mke2fs")
|
||||
.args(["-q", "-F", "-t", "ext4", "-d"])
|
||||
.arg(&root)
|
||||
.arg(&plain)
|
||||
.status()
|
||||
.unwrap()
|
||||
.success());
|
||||
let dbg = |c: &str| {
|
||||
Command::new("debugfs").arg("-w").arg("-R").arg(c).arg(&plain).output().unwrap()
|
||||
Command::new("debugfs")
|
||||
.arg("-w")
|
||||
.arg("-R")
|
||||
.arg(c)
|
||||
.arg(&plain)
|
||||
.output()
|
||||
.unwrap()
|
||||
};
|
||||
dbg("rm /docs/segredo.dat");
|
||||
|
||||
let img = dir.path().join("luks.img");
|
||||
std::fs::File::create(&img).unwrap().set_len(32 << 20).unwrap();
|
||||
std::fs::File::create(&img)
|
||||
.unwrap()
|
||||
.set_len(32 << 20)
|
||||
.unwrap();
|
||||
let pw = "correct horse";
|
||||
let mut fmt = Command::new("cryptsetup");
|
||||
fmt.args(["luksFormat", "--batch-mode", "--type", "luks2", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000", "--sector-size", "512", "--key-file", "-"]).arg(&img);
|
||||
fmt.args([
|
||||
"luksFormat",
|
||||
"--batch-mode",
|
||||
"--type",
|
||||
"luks2",
|
||||
"--pbkdf",
|
||||
"pbkdf2",
|
||||
"--pbkdf-force-iterations",
|
||||
"1000",
|
||||
"--sector-size",
|
||||
"512",
|
||||
"--key-file",
|
||||
"-",
|
||||
])
|
||||
.arg(&img);
|
||||
assert!(run_stdin(&mut fmt, pw));
|
||||
let vkf = dir.path().join("vk");
|
||||
let mut d = Command::new("cryptsetup");
|
||||
d.args(["luksDump", "--dump-volume-key", "--batch-mode", "--key-file", "-", "--volume-key-file"]).arg(&vkf).arg(&img);
|
||||
d.args([
|
||||
"luksDump",
|
||||
"--dump-volume-key",
|
||||
"--batch-mode",
|
||||
"--key-file",
|
||||
"-",
|
||||
"--volume-key-file",
|
||||
])
|
||||
.arg(&vkf)
|
||||
.arg(&img);
|
||||
assert!(run_stdin(&mut d, pw));
|
||||
let out = Command::new("cryptsetup").arg("luksDump").arg(&img).output().unwrap();
|
||||
let out = Command::new("cryptsetup")
|
||||
.arg("luksDump")
|
||||
.arg(&img)
|
||||
.output()
|
||||
.unwrap();
|
||||
let t = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
let off: u64 = t.lines().skip_while(|l| !l.starts_with("Data segments")).find(|l| l.contains("offset:")).unwrap().split(':').nth(1).unwrap().split_whitespace().next().unwrap().parse().unwrap();
|
||||
let off: u64 = t
|
||||
.lines()
|
||||
.skip_while(|l| !l.starts_with("Data segments"))
|
||||
.find(|l| l.contains("offset:"))
|
||||
.unwrap()
|
||||
.split(':')
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert!(Command::new("python3")
|
||||
.arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/support/luks_encrypt.py"))
|
||||
.arg(&img).arg(&plain).arg(&vkf).arg(off.to_string()).arg("aes-xts-plain64").arg("512")
|
||||
.status().unwrap().success());
|
||||
.arg(&img)
|
||||
.arg(&plain)
|
||||
.arg(&vkf)
|
||||
.arg(off.to_string())
|
||||
.arg("aes-xts-plain64")
|
||||
.arg("512")
|
||||
.status()
|
||||
.unwrap()
|
||||
.success());
|
||||
|
||||
let base: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
|
||||
// Sem senha: o volume segue fechado e a varredura não inventa arquivos.
|
||||
@@ -78,19 +155,52 @@ fn deleted_file_inside_luks2_is_recovered_with_password() {
|
||||
assert_eq!(locked.unlocked.len(), 0);
|
||||
assert_eq!(locked.locked.len(), 1);
|
||||
|
||||
let asm = Assembly { secrets: vec![SecretInput::Password { value: pw.into() }], ..Default::default() };
|
||||
let asm = Assembly {
|
||||
secrets: vec![SecretInput::Password { value: pw.into() }],
|
||||
..Default::default()
|
||||
};
|
||||
let open = assemble_source(base, &asm).unwrap();
|
||||
assert_eq!(open.unlocked.len(), 1);
|
||||
let mut sink = Collect::default();
|
||||
let outc = scan::run(open.source.as_ref(), &ScanOptions { mode: ScanMode::Deep, ..Default::default() }, None, &[], &|| false, &mut sink);
|
||||
let outc = scan::run(
|
||||
open.source.as_ref(),
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Deep,
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
&[],
|
||||
&|| false,
|
||||
&mut sink,
|
||||
);
|
||||
assert!(matches!(outc, Outcome::Completed));
|
||||
let f = sink.found.iter().find(|f| f.name == "segredo.dat").unwrap_or_else(|| panic!("não achou: {:?}", sink.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||
let f = sink
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.name == "segredo.dat")
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"não achou: {:?}",
|
||||
sink.found.iter().map(|f| &f.name).collect::<Vec<_>>()
|
||||
)
|
||||
});
|
||||
assert_eq!(f.size, 120_000);
|
||||
let dest = tempfile::tempdir().unwrap();
|
||||
let refs = vec![f];
|
||||
let rep = recover::recover(open.source.as_ref(), &refs, dest.path(), &|| false, &mut |_, _, _| {}).unwrap();
|
||||
let rep = recover::recover(
|
||||
open.source.as_ref(),
|
||||
&refs,
|
||||
dest.path(),
|
||||
&|| false,
|
||||
&mut |_, _, _| {},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(rep.failed.is_empty());
|
||||
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) };
|
||||
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() == secret_doc);
|
||||
}
|
||||
@@ -11,7 +11,11 @@ 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)
|
||||
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> {
|
||||
@@ -67,9 +71,24 @@ fn deleted_file_on_degraded_raid5() {
|
||||
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();
|
||||
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
|
||||
@@ -101,24 +120,56 @@ fn deleted_file_on_degraded_raid5() {
|
||||
}
|
||||
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(),
|
||||
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!(
|
||||
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 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<_>>()));
|
||||
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();
|
||||
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) };
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -133,7 +184,11 @@ fn raid5_without_metadata_is_detected_and_scanned() {
|
||||
for d in 0..10 {
|
||||
std::fs::create_dir_all(root.join(format!("d{d}"))).unwrap();
|
||||
for f in 0..30 {
|
||||
std::fs::write(root.join(format!("d{d}/f{f}.txt")), vec![b'x'; 500 + f * 50]).unwrap();
|
||||
std::fs::write(
|
||||
root.join(format!("d{d}/f{f}.txt")),
|
||||
vec![b'x'; 500 + f * 50],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
let doc = data(9, 200_000);
|
||||
@@ -142,9 +197,24 @@ fn raid5_without_metadata_is_detected_and_scanned() {
|
||||
let (n, chunk) = (3usize, 64 * 1024usize);
|
||||
let dd = n - 1;
|
||||
let rows = 160usize;
|
||||
std::fs::File::create(&plain).unwrap().set_len((rows * dd * chunk) 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 /d3/perdido.dat").arg(&plain).output().unwrap();
|
||||
std::fs::File::create(&plain)
|
||||
.unwrap()
|
||||
.set_len((rows * dd * chunk) 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 /d3/perdido.dat")
|
||||
.arg(&plain)
|
||||
.output()
|
||||
.unwrap();
|
||||
let logical = std::fs::read(&plain).unwrap();
|
||||
// left-asymmetric
|
||||
let mut disks = vec![vec![0u8; rows * chunk]; n];
|
||||
@@ -169,21 +239,48 @@ fn raid5_without_metadata_is_detected_and_scanned() {
|
||||
}
|
||||
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(),
|
||||
members: paths[1..]
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect(),
|
||||
raid: Some(guara_core::assemble::RaidSpec::default()),
|
||||
..Default::default()
|
||||
};
|
||||
let a = assemble_source(base, &asm).unwrap();
|
||||
assert!(a.skip_primary);
|
||||
assert!(a.notes.iter().any(|n| n.contains("descobertos")), "{:?}", a.notes);
|
||||
let opts = ScanOptions { mode: ScanMode::Deep, extra_parts: a.extra_parts.clone(), skip_primary: true, ..Default::default() };
|
||||
assert!(
|
||||
a.notes.iter().any(|n| n.contains("descobertos")),
|
||||
"{:?}",
|
||||
a.notes
|
||||
);
|
||||
let opts = ScanOptions {
|
||||
mode: ScanMode::Deep,
|
||||
extra_parts: a.extra_parts.clone(),
|
||||
skip_primary: true,
|
||||
..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 == "perdido.dat").expect("perdido.dat");
|
||||
let f = sink
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.name == "perdido.dat")
|
||||
.expect("perdido.dat");
|
||||
let dest = tempfile::tempdir().unwrap();
|
||||
let rep = recover::recover(a.source.as_ref(), &[f], dest.path(), &|| false, &mut |_, _, _| {}).unwrap();
|
||||
let rep = recover::recover(
|
||||
a.source.as_ref(),
|
||||
&[f],
|
||||
dest.path(),
|
||||
&|| false,
|
||||
&mut |_, _, _| {},
|
||||
)
|
||||
.unwrap();
|
||||
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) };
|
||||
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);
|
||||
}
|
||||
@@ -3,7 +3,9 @@ use guara_core::source::{open_path, read_vec, Source};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn fx(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ewf").join(name)
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/ewf")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn raw() -> Vec<u8> {
|
||||
@@ -32,8 +34,14 @@ fn stored_hash_matches() {
|
||||
}
|
||||
let e = EwfSource::open(&fx("s_e6.E01")).unwrap();
|
||||
assert_eq!(e.info.version, 1);
|
||||
assert_eq!(e.info.fields.get("Examinador").map(String::as_str), Some("examiner"));
|
||||
assert_eq!(e.info.fields.get("Número do caso").map(String::as_str), Some("case1"));
|
||||
assert_eq!(
|
||||
e.info.fields.get("Examinador").map(String::as_str),
|
||||
Some("examiner")
|
||||
);
|
||||
assert_eq!(
|
||||
e.info.fields.get("Número do caso").map(String::as_str),
|
||||
Some("case1")
|
||||
);
|
||||
assert_eq!(EwfSource::open(&fx("s_x7.Ex01")).unwrap().info.version, 2);
|
||||
}
|
||||
|
||||
@@ -58,14 +66,20 @@ fn write_then_read_roundtrip_with_segments() {
|
||||
..Default::default()
|
||||
};
|
||||
let rep = write_e01(&src, &base, &opts, &|| false, &mut |_, _| {}).unwrap();
|
||||
assert!(dir.path().join("img.E02").exists(), "esperava mais de um segmento");
|
||||
assert!(
|
||||
dir.path().join("img.E02").exists(),
|
||||
"esperava mais de um segmento"
|
||||
);
|
||||
let r = open_path(&dir.path().join("img.E01")).unwrap();
|
||||
assert_eq!(r.size(), data.len() as u64);
|
||||
assert!(read_vec(r.as_ref(), 0, data.len()).unwrap() == data);
|
||||
let e = EwfSource::open(&dir.path().join("img.E01")).unwrap();
|
||||
assert_eq!(e.info.md5.as_deref(), Some(rep.md5.as_str()));
|
||||
assert_eq!(e.verify().unwrap(), Some(true));
|
||||
assert_eq!(e.info.fields.get("Examinador").map(String::as_str), Some("Lucas"));
|
||||
assert_eq!(
|
||||
e.info.fields.get("Examinador").map(String::as_str),
|
||||
Some("Lucas")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -74,7 +88,17 @@ fn compressed_output_is_smaller_and_odd_sizes_work() {
|
||||
let src = guara_core::source::MemSource(data.clone());
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let base = dir.path().join("z");
|
||||
write_e01(&src, &base, &E01Options { segment_size: 0, ..Default::default() }, &|| false, &mut |_, _| {}).unwrap();
|
||||
write_e01(
|
||||
&src,
|
||||
&base,
|
||||
&E01Options {
|
||||
segment_size: 0,
|
||||
..Default::default()
|
||||
},
|
||||
&|| false,
|
||||
&mut |_, _| {},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(std::fs::metadata(dir.path().join("z.E01")).unwrap().len() < 10_000);
|
||||
let r = open_path(&dir.path().join("z.E01")).unwrap();
|
||||
assert_eq!(r.size(), 100_352);
|
||||
@@ -85,8 +109,22 @@ fn compressed_output_is_smaller_and_odd_sizes_work() {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn emit_for_external_verification() {
|
||||
let Ok(base) = std::env::var("GUARA_E01_OUT") else { return };
|
||||
let Ok(base) = std::env::var("GUARA_E01_OUT") else {
|
||||
return;
|
||||
};
|
||||
let src = guara_core::source::MemSource(raw());
|
||||
let opts = E01Options { segment_size: 100_000, case_number: "c1".into(), examiner: "ex".into(), ..Default::default() };
|
||||
write_e01(&src, std::path::Path::new(&base), &opts, &|| false, &mut |_, _| {}).unwrap();
|
||||
let opts = E01Options {
|
||||
segment_size: 100_000,
|
||||
case_number: "c1".into(),
|
||||
examiner: "ex".into(),
|
||||
..Default::default()
|
||||
};
|
||||
write_e01(
|
||||
&src,
|
||||
std::path::Path::new(&base),
|
||||
&opts,
|
||||
&|| false,
|
||||
&mut |_, _| {},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
+100
-19
@@ -17,7 +17,13 @@ fn have(tool: &str) -> bool {
|
||||
fn debugfs(img: &Path, cmds: &[&str]) -> String {
|
||||
let mut out = String::new();
|
||||
for c in cmds {
|
||||
let o = Command::new("debugfs").arg("-w").arg("-R").arg(c).arg(img).output().unwrap();
|
||||
let o = Command::new("debugfs")
|
||||
.arg("-w")
|
||||
.arg("-R")
|
||||
.arg(c)
|
||||
.arg(img)
|
||||
.output()
|
||||
.unwrap();
|
||||
out.push_str(&String::from_utf8_lossy(&o.stdout));
|
||||
out.push_str(&String::from_utf8_lossy(&o.stderr));
|
||||
}
|
||||
@@ -26,7 +32,10 @@ fn debugfs(img: &Path, cmds: &[&str]) -> String {
|
||||
|
||||
fn inode_of(img: &Path, path: &str) -> u64 {
|
||||
let o = debugfs(img, &[&format!("stat {path}")]);
|
||||
let l = o.lines().find(|l| l.starts_with("Inode:")).unwrap_or_else(|| panic!("{o}"));
|
||||
let l = o
|
||||
.lines()
|
||||
.find(|l| l.starts_with("Inode:"))
|
||||
.unwrap_or_else(|| panic!("{o}"));
|
||||
l.split_whitespace().nth(1).unwrap().parse().unwrap()
|
||||
}
|
||||
|
||||
@@ -76,13 +85,24 @@ fn build(opts: &[&str]) -> Option<Img> {
|
||||
.arg(&img)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(st.status.success(), "{}", String::from_utf8_lossy(&st.stderr));
|
||||
Some(Img { _dir: dir, img, files })
|
||||
assert!(
|
||||
st.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&st.stderr)
|
||||
);
|
||||
Some(Img {
|
||||
_dir: dir,
|
||||
img,
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
fn deep(img: &Path) -> common::Collect {
|
||||
init();
|
||||
let opts = ScanOptions { mode: ScanMode::Deep, ..ScanOptions::default() };
|
||||
let opts = ScanOptions {
|
||||
mode: ScanMode::Deep,
|
||||
..ScanOptions::default()
|
||||
};
|
||||
scan_path(img, &opts)
|
||||
}
|
||||
|
||||
@@ -91,30 +111,60 @@ fn recovered(img: &Path, f: &Found) -> Vec<u8> {
|
||||
let rep = recover_all(img, std::slice::from_ref(f), d.path());
|
||||
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||
let p = &rep.recovered[0].path;
|
||||
let p = if Path::new(p).is_absolute() { PathBuf::from(p) } else { d.path().join(p) };
|
||||
let p = if Path::new(p).is_absolute() {
|
||||
PathBuf::from(p)
|
||||
} else {
|
||||
d.path().join(p)
|
||||
};
|
||||
std::fs::read(p).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_by_debugfs_keeps_extents_and_name() {
|
||||
let Some(i) = build(&["-t", "ext4"]) else { return };
|
||||
let Some(i) = build(&["-t", "ext4"]) else {
|
||||
return;
|
||||
};
|
||||
debugfs(&i.img, &["rm /docs/relatorio.dat", "rm /fotos/viagem.dat"]);
|
||||
let c = deep(&i.img);
|
||||
for (p, data) in i.files.iter().filter(|f| f.0 == "docs/relatorio.dat" || f.0 == "fotos/viagem.dat") {
|
||||
for (p, data) in i
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| f.0 == "docs/relatorio.dat" || f.0 == "fotos/viagem.dat")
|
||||
{
|
||||
let name = p.rsplit('/').next().unwrap();
|
||||
let f = c.found.iter().find(|f| f.name == name && f.origin == Origin::Ext).unwrap_or_else(|| panic!("{name} não encontrado: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::<Vec<_>>()));
|
||||
let f = c
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.name == name && f.origin == Origin::Ext)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{name} não encontrado: {:?}",
|
||||
c.found
|
||||
.iter()
|
||||
.map(|f| (&f.name, f.origin))
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
});
|
||||
assert_eq!(f.size, data.len() as u64);
|
||||
assert_eq!(f.dir.as_deref(), Some(&format!("/{}", p.rsplit_once('/').unwrap().0)[..]));
|
||||
assert_eq!(
|
||||
f.dir.as_deref(),
|
||||
Some(&format!("/{}", p.rsplit_once('/').unwrap().0)[..])
|
||||
);
|
||||
assert!(recovered(&i.img, f) == *data, "{name}");
|
||||
}
|
||||
// O arquivo que não foi apagado não aparece.
|
||||
assert!(!c.found.iter().any(|f| f.name == "fica.dat" && f.origin == Origin::Ext));
|
||||
assert!(!c
|
||||
.found
|
||||
.iter()
|
||||
.any(|f| f.name == "fica.dat" && f.origin == Origin::Ext));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_inode_recovered_by_remnant_extents() {
|
||||
// Simula o que o kernel faz ao apagar: tamanho zerado e contagem de extents zerada.
|
||||
let Some(i) = build(&["-t", "ext4"]) else { return };
|
||||
let Some(i) = build(&["-t", "ext4"]) else {
|
||||
return;
|
||||
};
|
||||
let ino = inode_of(&i.img, "/docs/sub/contrato.dat");
|
||||
debugfs(
|
||||
&i.img,
|
||||
@@ -125,7 +175,11 @@ fn truncated_inode_recovered_by_remnant_extents() {
|
||||
],
|
||||
);
|
||||
let c = deep(&i.img);
|
||||
let f = c.found.iter().find(|f| f.name == "contrato.dat").expect("contrato.dat");
|
||||
let f = c
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.name == "contrato.dat")
|
||||
.expect("contrato.dat");
|
||||
assert_eq!(f.origin, Origin::Ext);
|
||||
// Tamanho exato é perdido: recupera todos os blocos (9000 bytes arredondados para 3 blocos de 4K).
|
||||
assert_eq!(f.size, 12288);
|
||||
@@ -138,7 +192,9 @@ fn journal_copy_restores_exact_size() {
|
||||
if !have("debugfs") {
|
||||
return;
|
||||
}
|
||||
let Some(i) = build(&["-t", "ext4", "-O", "has_journal"]) else { return };
|
||||
let Some(i) = build(&["-t", "ext4", "-O", "has_journal"]) else {
|
||||
return;
|
||||
};
|
||||
// Bloco da tabela de inodes que contém o inode do arquivo.
|
||||
let ino = inode_of(&i.img, "/fotos/viagem.dat");
|
||||
let o = debugfs(&i.img, &["stats"]);
|
||||
@@ -157,9 +213,23 @@ fn journal_copy_restores_exact_size() {
|
||||
let bin = i.img.with_extension("blk");
|
||||
std::fs::write(&bin, old).unwrap();
|
||||
let script = i.img.with_extension("cmds");
|
||||
std::fs::write(&script, format!("jo\njw -b {blk} {}\njc\njc\n", bin.display())).unwrap();
|
||||
let o = Command::new("debugfs").arg("-w").arg("-f").arg(&script).arg(&i.img).output().unwrap();
|
||||
let j = format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr));
|
||||
std::fs::write(
|
||||
&script,
|
||||
format!("jo\njw -b {blk} {}\njc\njc\n", bin.display()),
|
||||
)
|
||||
.unwrap();
|
||||
let o = Command::new("debugfs")
|
||||
.arg("-w")
|
||||
.arg("-f")
|
||||
.arg(&script)
|
||||
.arg(&i.img)
|
||||
.output()
|
||||
.unwrap();
|
||||
let j = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&o.stdout),
|
||||
String::from_utf8_lossy(&o.stderr)
|
||||
);
|
||||
if j.contains("Unknown request") || j.contains("not supported") {
|
||||
eprintln!("SKIP: debugfs sem suporte a escrita no diário: {j}");
|
||||
return;
|
||||
@@ -174,7 +244,16 @@ fn journal_copy_restores_exact_size() {
|
||||
],
|
||||
);
|
||||
let c = deep(&i.img);
|
||||
let f = c.found.iter().find(|f| f.name == "viagem.dat").unwrap_or_else(|| panic!("sem viagem.dat: {:?}", c.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||
let f = c
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.name == "viagem.dat")
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"sem viagem.dat: {:?}",
|
||||
c.found.iter().map(|f| &f.name).collect::<Vec<_>>()
|
||||
)
|
||||
});
|
||||
assert_eq!(f.size, 700_000, "{:?}", f.note);
|
||||
assert!(recovered(&i.img, f) == i.files[2].1);
|
||||
}
|
||||
@@ -183,7 +262,9 @@ fn journal_copy_restores_exact_size() {
|
||||
fn ext2_blockmap_deleted_is_not_invented() {
|
||||
// ext2 zera os ponteiros ao apagar; sem diário não há o que recuperar por metadados,
|
||||
// mas a varredura não pode falhar nem listar o arquivo que continua existindo.
|
||||
let Some(i) = build(&["-t", "ext2"]) else { return };
|
||||
let Some(i) = build(&["-t", "ext2"]) else {
|
||||
return;
|
||||
};
|
||||
let c = deep(&i.img);
|
||||
assert!(!c.found.iter().any(|f| f.origin == Origin::Ext));
|
||||
}
|
||||
@@ -7,7 +7,8 @@ use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn opens_reference_image_and_matches_known_hash() {
|
||||
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/fvault2-images/small");
|
||||
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../target/testdata/fvault2-images/small");
|
||||
if !p.exists() {
|
||||
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||
return;
|
||||
@@ -31,5 +32,8 @@ fn opens_reference_image_and_matches_known_hash() {
|
||||
h.update(&buf[..n]);
|
||||
off += n as u64;
|
||||
}
|
||||
assert_eq!(h.finalize().sha256, "2c662e36c0f7e2f5583e6a939bbcbdc660805692d0fccaa45ad4052beb3b8e18");
|
||||
assert_eq!(
|
||||
h.finalize().sha256,
|
||||
"2c662e36c0f7e2f5583e6a939bbcbdc660805692d0fccaa45ad4052beb3b8e18"
|
||||
);
|
||||
}
|
||||
@@ -16,31 +16,78 @@ fn check(path: &str, ext: &str, min_conf: Confidence) -> bool {
|
||||
// lixo depois do fim real (outro arquivo no disco)
|
||||
data.extend((0..5000u32).map(|i| (i.wrapping_mul(2654435761) >> 7) as u8 | 0x81));
|
||||
let src = MemSource(data.clone());
|
||||
let h = detect(&src, 0, &data[..128], 1 << 30).unwrap_or_else(|| panic!("{path}: não reconhecido"));
|
||||
let h =
|
||||
detect(&src, 0, &data[..128], 1 << 30).unwrap_or_else(|| panic!("{path}: não reconhecido"));
|
||||
assert_eq!(h.len as usize, real, "{path}: tamanho");
|
||||
assert_eq!(h.ext, ext, "{path}: extensão");
|
||||
assert!(h.confidence >= min_conf, "{path}: confiança {:?}", h.confidence);
|
||||
assert!(
|
||||
h.confidence >= min_conf,
|
||||
"{path}: confiança {:?}",
|
||||
h.confidence
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
fn first(dir: &str, ext: &str) -> Option<String> {
|
||||
let out = std::process::Command::new("find").args([dir, "-iname", &format!("*.{ext}"), "-type", "f", "-size", "+2k", "-size", "-20M"]).output().ok()?;
|
||||
String::from_utf8_lossy(&out.stdout).lines().next().map(String::from)
|
||||
let out = std::process::Command::new("find")
|
||||
.args([
|
||||
dir,
|
||||
"-iname",
|
||||
&format!("*.{ext}"),
|
||||
"-type",
|
||||
"f",
|
||||
"-size",
|
||||
"+2k",
|
||||
"-size",
|
||||
"-20M",
|
||||
])
|
||||
.output()
|
||||
.ok()?;
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.next()
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_files_have_exact_lengths() {
|
||||
let mut n = 0;
|
||||
let cases: Vec<(Option<String>, &str, Confidence)> = vec![
|
||||
(first("/usr/share/cinnamon/sounds", "ogg"), "ogg", Confidence::High),
|
||||
(
|
||||
first("/usr/share/cinnamon/sounds", "ogg"),
|
||||
"ogg",
|
||||
Confidence::High,
|
||||
),
|
||||
(first("/usr/share", "gz"), "gz", Confidence::High),
|
||||
(first("/usr/share/doc", "bz2"), "bz2", Confidence::High),
|
||||
(first("/usr/lib/firmware", "zst"), "zst", Confidence::Medium),
|
||||
(first("/usr/share/texlive/texmf-dist/tex/latex/media9/players", "swf"), "swf", Confidence::Medium),
|
||||
(first("/usr/share/code/resources/app", "rtf"), "rtf", Confidence::High),
|
||||
(first("/usr/share/app-install/icons", "tiff"), "tif", Confidence::Medium),
|
||||
(
|
||||
first(
|
||||
"/usr/share/texlive/texmf-dist/tex/latex/media9/players",
|
||||
"swf",
|
||||
),
|
||||
"swf",
|
||||
Confidence::Medium,
|
||||
),
|
||||
(
|
||||
first("/usr/share/code/resources/app", "rtf"),
|
||||
"rtf",
|
||||
Confidence::High,
|
||||
),
|
||||
(
|
||||
first("/usr/share/app-install/icons", "tiff"),
|
||||
"tif",
|
||||
Confidence::Medium,
|
||||
),
|
||||
(Some("/usr/bin/ls".to_string()), "elf", Confidence::High),
|
||||
(first("/home/lucas/Documents/Desenvolvimento/pop-hub/public", "ico"), "ico", Confidence::High),
|
||||
(
|
||||
first(
|
||||
"/home/lucas/Documents/Desenvolvimento/pop-hub/public",
|
||||
"ico",
|
||||
),
|
||||
"ico",
|
||||
Confidence::High,
|
||||
),
|
||||
];
|
||||
for (path, ext, conf) in cases {
|
||||
if let Some(p) = path {
|
||||
@@ -55,9 +102,24 @@ fn real_files_have_exact_lengths() {
|
||||
#[test]
|
||||
fn webm_rar_deb_with_unknown_locations() {
|
||||
for (dir, ext, out, conf) in [
|
||||
("/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/Nextcloud/apps/firstrunwizard/img", "webm", "webm", Confidence::High),
|
||||
("/home/lucas/Documents/Pessoal/dev/power_vision/.trabalho/libbsd", "deb", "deb", Confidence::Medium),
|
||||
("/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/war_cloud/lucasrguerra/Backups", "rar", "rar", Confidence::High),
|
||||
(
|
||||
"/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/Nextcloud/apps/firstrunwizard/img",
|
||||
"webm",
|
||||
"webm",
|
||||
Confidence::High,
|
||||
),
|
||||
(
|
||||
"/home/lucas/Documents/Pessoal/dev/power_vision/.trabalho/libbsd",
|
||||
"deb",
|
||||
"deb",
|
||||
Confidence::Medium,
|
||||
),
|
||||
(
|
||||
"/media/lucas/d9d2ba04-f925-4b9a-97d7-92c2e3bfdb78/war_cloud/lucasrguerra/Backups",
|
||||
"rar",
|
||||
"rar",
|
||||
Confidence::High,
|
||||
),
|
||||
] {
|
||||
if let Some(p) = first(dir, ext) {
|
||||
check(&p, out, conf);
|
||||
|
||||
@@ -5,7 +5,8 @@ use common::*;
|
||||
use guara_core::*;
|
||||
|
||||
fn image() -> Option<Vec<u8>> {
|
||||
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/dfvfs/hfsplus.raw");
|
||||
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../target/testdata/dfvfs/hfsplus.raw");
|
||||
p.exists().then(|| std::fs::read(p).unwrap())
|
||||
}
|
||||
|
||||
@@ -14,7 +15,13 @@ fn scan_bytes(bytes: &[u8]) -> Collect {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p = dir.path().join("h.img");
|
||||
std::fs::write(&p, bytes).unwrap();
|
||||
scan_path(&p, &ScanOptions { mode: ScanMode::Quick, ..Default::default() })
|
||||
scan_path(
|
||||
&p,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Quick,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -25,7 +32,11 @@ fn pristine_volume_has_no_deleted_files_and_is_recognized() {
|
||||
};
|
||||
let c = scan_bytes(&img);
|
||||
assert_eq!(c.layout.as_ref().unwrap().partitions[0].fs, FsKind::HfsPlus);
|
||||
assert!(c.found.is_empty(), "{:?}", c.found.iter().map(|f| &f.name).collect::<Vec<_>>());
|
||||
assert!(
|
||||
c.found.is_empty(),
|
||||
"{:?}",
|
||||
c.found.iter().map(|f| &f.name).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -33,13 +44,17 @@ fn record_left_in_leaf_slack_is_recovered() {
|
||||
let Some(mut img) = image() else { return };
|
||||
let bs = 4096usize;
|
||||
let node = 186 * bs + bs; // catálogo começa no bloco 186; nó 1 é a única folha
|
||||
// Remove os 5 últimos registros como o driver faz ao apagar no fim do nó: só diminui a contagem.
|
||||
// Remove os 5 últimos registros como o driver faz ao apagar no fim do nó: só diminui a contagem.
|
||||
img[node + 10..node + 12].copy_from_slice(&21u16.to_be_bytes());
|
||||
// Libera o bloco de dados (bitmap de alocação no bloco 1).
|
||||
let blk = 278usize;
|
||||
img[bs + blk / 8] &= !(0x80 >> (blk % 8));
|
||||
let c = scan_bytes(&img);
|
||||
let f = c.found.iter().find(|f| f.name == "fseventsd-uuid").unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||
let f = c
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.name == "fseventsd-uuid")
|
||||
.unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||
assert_eq!(f.origin, Origin::Hfs);
|
||||
assert_eq!(f.size, 36);
|
||||
assert_eq!(f.dir.as_deref(), Some("/.fseventsd"));
|
||||
|
||||
@@ -11,14 +11,19 @@ fn photo(w: u16, h: u16) -> Vec<u8> {
|
||||
x ^= x << 17;
|
||||
let noise = ((x >> 40) & 31) as i32;
|
||||
let base = 90 + (xx * 90 / w as i32) + (yy * 60 / h as i32);
|
||||
let blob = if ((xx - 200).pow(2) + (yy - 150).pow(2)) < 3600 { 70 } else { 0 };
|
||||
let blob = if ((xx - 200).pow(2) + (yy - 150).pow(2)) < 3600 {
|
||||
70
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let v = (base + blob + noise).clamp(0, 255) as u8;
|
||||
rgb.extend_from_slice(&[v, v.saturating_sub(20), (v as i32 / 2 + noise) as u8]);
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
let enc = jpeg_encoder::Encoder::new(&mut out, 92);
|
||||
enc.encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb).unwrap();
|
||||
enc.encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb)
|
||||
.unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
@@ -32,7 +37,10 @@ fn decodes_whole_image_and_ends_at_eoi() {
|
||||
assert_eq!(d.dc.len() as u32, h.total_mcus());
|
||||
// O que sobra depois do último MCU é o EOI.
|
||||
let tail = &j[d.state.pos.min(j.len())..];
|
||||
assert!(tail.windows(2).any(|w| w == [0xFF, 0xD9]), "sem EOI depois do último MCU");
|
||||
assert!(
|
||||
tail.windows(2).any(|w| w == [0xFF, 0xD9]),
|
||||
"sem EOI depois do último MCU"
|
||||
);
|
||||
assert!(first_discontinuity(&h, &d.dc).is_none());
|
||||
}
|
||||
|
||||
@@ -59,7 +67,12 @@ fn detects_break_and_resumes_with_the_right_continuation() {
|
||||
// Decodificação segue o lixo por um tempo; mas a quebra aparece pelo erro ou pela descontinuidade.
|
||||
let mcu_break = first_discontinuity(&h, &d.dc);
|
||||
// MCUs bons: os decodificados ao fim do cluster `cut`.
|
||||
let good = d.snaps.iter().filter(|s| s.pos <= cut).last().expect("snapshot antes da quebra").clone();
|
||||
let good = d
|
||||
.snaps
|
||||
.iter()
|
||||
.rfind(|s| s.pos <= cut)
|
||||
.expect("snapshot antes da quebra")
|
||||
.clone();
|
||||
// Retomada com a continuação verdadeira reproduz a decodificação inteira.
|
||||
let mut joined = j[..cut].to_vec();
|
||||
joined.extend_from_slice(&j[cut..]);
|
||||
|
||||
+105
-15
@@ -25,8 +25,17 @@ fn py_ok() -> bool {
|
||||
|
||||
fn run_stdin(cmd: &mut Command, input: &str) -> bool {
|
||||
use std::io::Write;
|
||||
let mut ch = cmd.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn().unwrap();
|
||||
ch.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();
|
||||
let mut ch = cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
ch.stdin
|
||||
.take()
|
||||
.unwrap()
|
||||
.write_all(input.as_bytes())
|
||||
.unwrap();
|
||||
ch.wait().unwrap().success()
|
||||
}
|
||||
|
||||
@@ -51,11 +60,26 @@ struct Case<'a> {
|
||||
}
|
||||
|
||||
fn payload_offset(img: &Path, ver2: bool) -> u64 {
|
||||
let o = Command::new("cryptsetup").arg("luksDump").arg(img).output().unwrap();
|
||||
let o = Command::new("cryptsetup")
|
||||
.arg("luksDump")
|
||||
.arg(img)
|
||||
.output()
|
||||
.unwrap();
|
||||
let t = String::from_utf8_lossy(&o.stdout).to_string();
|
||||
if ver2 {
|
||||
let l = t.lines().skip_while(|l| !l.starts_with("Data segments")).find(|l| l.contains("offset:")).unwrap();
|
||||
l.split(':').nth(1).unwrap().split_whitespace().next().unwrap().parse().unwrap()
|
||||
let l = t
|
||||
.lines()
|
||||
.skip_while(|l| !l.starts_with("Data segments"))
|
||||
.find(|l| l.contains("offset:"))
|
||||
.unwrap();
|
||||
l.split(':')
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap()
|
||||
} else {
|
||||
let l = t.lines().find(|l| l.starts_with("Payload offset")).unwrap();
|
||||
l.split(':').nth(1).unwrap().trim().parse::<u64>().unwrap() * 512
|
||||
@@ -72,7 +96,10 @@ fn run_case(c: &Case) {
|
||||
let plain = dir.path().join("plain.bin");
|
||||
let data = plain_data(3 * 1024 * 1024 + 4096);
|
||||
std::fs::write(&plain, &data).unwrap();
|
||||
std::fs::File::create(&img).unwrap().set_len(24 << 20).unwrap();
|
||||
std::fs::File::create(&img)
|
||||
.unwrap()
|
||||
.set_len(24 << 20)
|
||||
.unwrap();
|
||||
let pw = "senha com acentuação ç";
|
||||
let mut fmt = Command::new("cryptsetup");
|
||||
fmt.args(["luksFormat", "--batch-mode", "--key-file", "-"]);
|
||||
@@ -81,9 +108,20 @@ fn run_case(c: &Case) {
|
||||
assert!(run_stdin(&mut fmt, pw), "luksFormat falhou ({})", c.name);
|
||||
let vkf = dir.path().join("vk.bin");
|
||||
let mut dump = Command::new("cryptsetup");
|
||||
dump.args(["luksDump", "--dump-volume-key", "--batch-mode", "--key-file", "-", "--volume-key-file"]);
|
||||
dump.args([
|
||||
"luksDump",
|
||||
"--dump-volume-key",
|
||||
"--batch-mode",
|
||||
"--key-file",
|
||||
"-",
|
||||
"--volume-key-file",
|
||||
]);
|
||||
dump.arg(&vkf).arg(&img);
|
||||
assert!(run_stdin(&mut dump, pw), "dump da chave falhou ({})", c.name);
|
||||
assert!(
|
||||
run_stdin(&mut dump, pw),
|
||||
"dump da chave falhou ({})",
|
||||
c.name
|
||||
);
|
||||
let off = payload_offset(&img, c.ver2);
|
||||
let st = Command::new("python3")
|
||||
.arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/support/luks_encrypt.py"))
|
||||
@@ -104,21 +142,36 @@ fn run_case(c: &Case) {
|
||||
Err(e) => panic!("{}: erro inesperado {e}", c.name),
|
||||
Ok(_) => panic!("{}: abriu com senha errada", c.name),
|
||||
}
|
||||
let u = luks::unlock(src.clone(), &[Secret::Password("x".into()), Secret::Password(pw.into())])
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", c.name));
|
||||
let u = luks::unlock(
|
||||
src.clone(),
|
||||
&[Secret::Password("x".into()), Secret::Password(pw.into())],
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", c.name));
|
||||
let got = read_vec(u.source.as_ref(), 0, data.len()).unwrap();
|
||||
assert!(got == data, "{}: conteúdo difere", c.name);
|
||||
// Também abre com a chave do volume.
|
||||
let vk = std::fs::read(&vkf).unwrap();
|
||||
let u2 = luks::unlock(src, &[Secret::VolumeKey(vk)]).unwrap();
|
||||
assert_eq!(read_vec(u2.source.as_ref(), 1000, 5000).unwrap(), &data[1000..6000]);
|
||||
assert_eq!(
|
||||
read_vec(u2.source.as_ref(), 1000, 5000).unwrap(),
|
||||
&data[1000..6000]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn luks1_xts() {
|
||||
run_case(&Case {
|
||||
name: "luks1-xts",
|
||||
format_args: vec!["--type", "luks1", "--pbkdf-force-iterations", "1000", "-c", "aes-xts-plain64", "-s", "512"],
|
||||
format_args: vec![
|
||||
"--type",
|
||||
"luks1",
|
||||
"--pbkdf-force-iterations",
|
||||
"1000",
|
||||
"-c",
|
||||
"aes-xts-plain64",
|
||||
"-s",
|
||||
"512",
|
||||
],
|
||||
cipher: "aes-xts-plain64",
|
||||
sector: 512,
|
||||
ver2: false,
|
||||
@@ -129,7 +182,18 @@ fn luks1_xts() {
|
||||
fn luks1_cbc_essiv_sha1() {
|
||||
run_case(&Case {
|
||||
name: "luks1-cbc-essiv",
|
||||
format_args: vec!["--type", "luks1", "--pbkdf-force-iterations", "1000", "-c", "aes-cbc-essiv:sha256", "-s", "256", "-h", "sha1"],
|
||||
format_args: vec![
|
||||
"--type",
|
||||
"luks1",
|
||||
"--pbkdf-force-iterations",
|
||||
"1000",
|
||||
"-c",
|
||||
"aes-cbc-essiv:sha256",
|
||||
"-s",
|
||||
"256",
|
||||
"-h",
|
||||
"sha1",
|
||||
],
|
||||
cipher: "aes-cbc-essiv:sha256",
|
||||
sector: 512,
|
||||
ver2: false,
|
||||
@@ -140,7 +204,20 @@ fn luks1_cbc_essiv_sha1() {
|
||||
fn luks2_argon2id_xts() {
|
||||
run_case(&Case {
|
||||
name: "luks2-argon2id",
|
||||
format_args: vec!["--type", "luks2", "--pbkdf", "argon2id", "--pbkdf-force-iterations", "4", "--pbkdf-memory", "64", "-c", "aes-xts-plain64", "-s", "512"],
|
||||
format_args: vec![
|
||||
"--type",
|
||||
"luks2",
|
||||
"--pbkdf",
|
||||
"argon2id",
|
||||
"--pbkdf-force-iterations",
|
||||
"4",
|
||||
"--pbkdf-memory",
|
||||
"64",
|
||||
"-c",
|
||||
"aes-xts-plain64",
|
||||
"-s",
|
||||
"512",
|
||||
],
|
||||
cipher: "aes-xts-plain64",
|
||||
sector: 4096,
|
||||
ver2: true,
|
||||
@@ -151,7 +228,20 @@ fn luks2_argon2id_xts() {
|
||||
fn luks2_pbkdf2_sector512_aes256() {
|
||||
run_case(&Case {
|
||||
name: "luks2-pbkdf2",
|
||||
format_args: vec!["--type", "luks2", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000", "-c", "aes-xts-plain64", "-s", "256", "--sector-size", "512"],
|
||||
format_args: vec![
|
||||
"--type",
|
||||
"luks2",
|
||||
"--pbkdf",
|
||||
"pbkdf2",
|
||||
"--pbkdf-force-iterations",
|
||||
"1000",
|
||||
"-c",
|
||||
"aes-xts-plain64",
|
||||
"-s",
|
||||
"256",
|
||||
"--sector-size",
|
||||
"512",
|
||||
],
|
||||
cipher: "aes-xts-plain64",
|
||||
sector: 512,
|
||||
ver2: true,
|
||||
|
||||
@@ -7,7 +7,8 @@ use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn assembles_real_lvm_image() {
|
||||
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/dfvfs/lvm.raw");
|
||||
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../target/testdata/dfvfs/lvm.raw");
|
||||
if !p.exists() {
|
||||
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||
return;
|
||||
|
||||
@@ -39,11 +39,20 @@ const SPS: &str = "67f4000d90d9b28283f602d41818190000030001000003003c8f142996";
|
||||
const PPS: &str = "68ebec448440";
|
||||
|
||||
fn hex(s: &str) -> Vec<u8> {
|
||||
(0..s.len() / 2).map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap()).collect()
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Cabeçalho de fatia H.264 conforme o SPS acima (log2_max_frame_num e log2_max_poc_lsb lidos do SPS).
|
||||
fn slice(idr: bool, frame_num: u32, poc_lsb: u32, payload: usize, seed: u8, sps: &mguara::Sps) -> Vec<u8> {
|
||||
fn slice(
|
||||
idr: bool,
|
||||
frame_num: u32,
|
||||
poc_lsb: u32,
|
||||
payload: usize,
|
||||
seed: u8,
|
||||
sps: &mguara::Sps,
|
||||
) -> Vec<u8> {
|
||||
let mut b = BitW::new();
|
||||
b.ue(0); // first_mb_in_slice
|
||||
b.ue(if idr { 7 } else { 5 }); // slice_type
|
||||
@@ -95,7 +104,14 @@ fn synthetic_opt(gops: usize, inband: bool) -> (Vec<u8>, Vec<usize>, Vec<bool>)
|
||||
if i == 0 {
|
||||
frame_num = 0;
|
||||
}
|
||||
let s = slice(i == 0, frame_num % 16, p % 64, 600 + i * 90 + g * 7, (g * 7 + i) as u8, &sps);
|
||||
let s = slice(
|
||||
i == 0,
|
||||
frame_num % 16,
|
||||
p % 64,
|
||||
600 + i * 90 + g * 7,
|
||||
(g * 7 + i) as u8,
|
||||
&sps,
|
||||
);
|
||||
nal(&mut mdat, &s);
|
||||
if p % 4 == 0 || i == 1 || i == 4 {
|
||||
frame_num += 1;
|
||||
@@ -105,7 +121,10 @@ fn synthetic_opt(gops: usize, inband: bool) -> (Vec<u8>, Vec<usize>, Vec<bool>)
|
||||
}
|
||||
}
|
||||
let mut f = vec![];
|
||||
f.extend_from_slice(&[0, 0, 0, 24, b'f', b't', b'y', b'p', b'i', b's', b'o', b'm', 0, 0, 2, 0, b'i', b's', b'o', b'm', b'a', b'v', b'c', b'1']);
|
||||
f.extend_from_slice(&[
|
||||
0, 0, 0, 24, b'f', b't', b'y', b'p', b'i', b's', b'o', b'm', 0, 0, 2, 0, b'i', b's', b'o',
|
||||
b'm', b'a', b'v', b'c', b'1',
|
||||
]);
|
||||
f.extend_from_slice(&[0, 0, 0, 8, b'f', b'r', b'e', b'e']);
|
||||
f.extend_from_slice(&((8 + mdat.len()) as u32).to_be_bytes());
|
||||
f.extend_from_slice(b"mdat");
|
||||
@@ -133,10 +152,14 @@ fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() {
|
||||
let r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).expect("reconstrução");
|
||||
assert_eq!(r.frames, sizes.len());
|
||||
assert_eq!((r.width, r.height), (320, 240));
|
||||
assert_eq!(r.payload.1 as usize, real_len, "o fim do vídeo foi detectado");
|
||||
assert_eq!(
|
||||
r.payload.1 as usize, real_len,
|
||||
"o fim do vídeo foi detectado"
|
||||
);
|
||||
let out = assemble(&file, &r);
|
||||
let size = out.len() as u64;
|
||||
let mut mp4 = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), size).expect("moov válido");
|
||||
let mut mp4 =
|
||||
mp4::Mp4Reader::read_header(std::io::Cursor::new(out), size).expect("moov válido");
|
||||
let track_id = *mp4.tracks().keys().next().unwrap();
|
||||
let t = &mp4.tracks()[&track_id];
|
||||
assert_eq!(t.sample_count() as usize, sizes.len());
|
||||
@@ -164,7 +187,9 @@ fn rebuilds_synthetic_stream_with_b_frames_and_garbage_tail() {
|
||||
#[test]
|
||||
fn rebuilds_real_h264_file_like_the_original_index() {
|
||||
let Ok(path) = std::env::var("GUARA_MP4_SAMPLE") else {
|
||||
eprintln!("SKIP: defina GUARA_MP4_SAMPLE com um MP4 H.264 para comparar com o índice original");
|
||||
eprintln!(
|
||||
"SKIP: defina GUARA_MP4_SAMPLE com um MP4 H.264 para comparar com o índice original"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let orig = std::fs::read(&path).unwrap();
|
||||
@@ -189,7 +214,10 @@ fn rebuilds_real_h264_file_like_the_original_index() {
|
||||
let mut b = mp4::Mp4Reader::read_header(std::io::Cursor::new(out), rsize).unwrap();
|
||||
let ta = *a.tracks().keys().next().unwrap();
|
||||
let tb = *b.tracks().keys().next().unwrap();
|
||||
assert_eq!(a.tracks()[&ta].sample_count(), b.tracks()[&tb].sample_count());
|
||||
assert_eq!(
|
||||
a.tracks()[&ta].sample_count(),
|
||||
b.tracks()[&tb].sample_count()
|
||||
);
|
||||
for i in 1..=a.tracks()[&ta].sample_count() {
|
||||
let x = a.read_sample(ta, i).unwrap().unwrap();
|
||||
let y = b.read_sample(tb, i).unwrap().unwrap();
|
||||
@@ -243,8 +271,12 @@ fn rebuilds_real_video_with_interleaved_audio() {
|
||||
// O tamanho do mdat declarado vai muito além dos 40 MB que temos (arquivo cortado).
|
||||
let src = MemSource(file.clone());
|
||||
// Sem SPS/PPS no mdat, é preciso um vídeo de referência da mesma câmera (aqui, o próprio original).
|
||||
let cfg = mp4rebuild::config_from_mp4(&MemSource(head.clone()), 0, head.len() as u64).expect("avcC do original");
|
||||
assert!(mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).is_none(), "sem referência não há como montar o avcC");
|
||||
let cfg = mp4rebuild::config_from_mp4(&MemSource(head.clone()), 0, head.len() as u64)
|
||||
.expect("avcC do original");
|
||||
assert!(
|
||||
mp4rebuild::rebuild(&src, 0, file.len() as u64, &[]).is_none(),
|
||||
"sem referência não há como montar o avcC"
|
||||
);
|
||||
let r = mp4rebuild::rebuild(&src, 0, file.len() as u64, &[cfg]).expect("reconstrução");
|
||||
let out = assemble(&file, &r);
|
||||
let osize = out.len() as u64;
|
||||
@@ -253,10 +285,26 @@ fn rebuilds_real_video_with_interleaved_audio() {
|
||||
// Original (índice completo) para comparar as primeiras amostras.
|
||||
let f = std::fs::File::open(&path).unwrap();
|
||||
let mut a = mp4::Mp4Reader::read_header(std::io::BufReader::new(f), total).unwrap();
|
||||
let vid = a.tracks().values().find(|t| t.track_type().map(|x| x == mp4::TrackType::Video).unwrap_or(false)).unwrap().track_id();
|
||||
eprintln!("reconstruído: {} quadros, {} bytes pulados (áudio), {}x{}", r.frames, r.skipped_bytes, r.width, r.height);
|
||||
let sizes_a: Vec<usize> = (1..=60).map(|i| a.read_sample(vid, i).unwrap().unwrap().bytes.len()).collect();
|
||||
let sizes_b: Vec<usize> = (1..=60).map(|i| b.read_sample(tb, i).unwrap().unwrap().bytes.len()).collect();
|
||||
let vid = a
|
||||
.tracks()
|
||||
.values()
|
||||
.find(|t| {
|
||||
t.track_type()
|
||||
.map(|x| x == mp4::TrackType::Video)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.unwrap()
|
||||
.track_id();
|
||||
eprintln!(
|
||||
"reconstruído: {} quadros, {} bytes pulados (áudio), {}x{}",
|
||||
r.frames, r.skipped_bytes, r.width, r.height
|
||||
);
|
||||
let sizes_a: Vec<usize> = (1..=60)
|
||||
.map(|i| a.read_sample(vid, i).unwrap().unwrap().bytes.len())
|
||||
.collect();
|
||||
let sizes_b: Vec<usize> = (1..=60)
|
||||
.map(|i| b.read_sample(tb, i).unwrap().unwrap().bytes.len())
|
||||
.collect();
|
||||
eprintln!("ORIG {:?}\nMINE {:?}", &sizes_a[40..60], &sizes_b[40..60]);
|
||||
let check = (r.frames as u32 - 1).min(400);
|
||||
for i in 1..=check {
|
||||
@@ -275,7 +323,8 @@ fn deep_scan_rebuilds_a_video_without_moov_using_a_neighbour_as_reference() {
|
||||
common::init();
|
||||
// Arquivo íntegro (com moov) da "mesma câmera" e um segundo que perdeu o moov e não traz SPS/PPS.
|
||||
let (good_raw, _, _) = synthetic_opt(8, true);
|
||||
let r = mp4rebuild::rebuild(&MemSource(good_raw.clone()), 0, good_raw.len() as u64, &[]).unwrap();
|
||||
let r =
|
||||
mp4rebuild::rebuild(&MemSource(good_raw.clone()), 0, good_raw.len() as u64, &[]).unwrap();
|
||||
let good = assemble(&good_raw, &r);
|
||||
let (broken, sizes, _) = synthetic_opt(10, false);
|
||||
let mut disk = vec![0u8; 8192];
|
||||
@@ -288,12 +337,32 @@ fn deep_scan_rebuilds_a_video_without_moov_using_a_neighbour_as_reference() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let img = dir.path().join("v.img");
|
||||
std::fs::write(&img, &disk).unwrap();
|
||||
let c = common::scan_path(&img, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
||||
let c = common::scan_path(
|
||||
&img,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Deep,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let vids: Vec<_> = c.found.iter().filter(|f| f.ext == "mp4").collect();
|
||||
assert_eq!(vids.len(), 2, "{:?}", vids.iter().map(|f| (f.first_offset(), f.size, f.confidence, &f.note)).collect::<Vec<_>>());
|
||||
let fb = vids.iter().find(|f| matches!(f.data, guara_core::DataLoc::Parts { .. })).expect("vídeo quebrado");
|
||||
assert_eq!(
|
||||
vids.len(),
|
||||
2,
|
||||
"{:?}",
|
||||
vids.iter()
|
||||
.map(|f| (f.first_offset(), f.size, f.confidence, &f.note))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
let fb = vids
|
||||
.iter()
|
||||
.find(|f| matches!(f.data, guara_core::DataLoc::Parts { .. }))
|
||||
.expect("vídeo quebrado");
|
||||
let _ = broken_off;
|
||||
assert!(fb.note.as_deref().unwrap_or("").contains("reconstruído"), "{:?}", fb.note);
|
||||
assert!(
|
||||
fb.note.as_deref().unwrap_or("").contains("reconstruído"),
|
||||
"{:?}",
|
||||
fb.note
|
||||
);
|
||||
let _ = good_off;
|
||||
let out = dir.path().join("out");
|
||||
let rep = common::recover_all(&img, std::slice::from_ref(*fb), &out);
|
||||
|
||||
@@ -59,7 +59,9 @@ fn encode_parity(level: Level, layout: Layout, n: usize, logical: &[u8]) -> Vec<
|
||||
let start = if level == Level::R6 { qd + 1 } else { pd + 1 };
|
||||
(0..dd).map(|d| (start + d) % n).collect()
|
||||
} else {
|
||||
(0..n).filter(|&i| i != pd && !(level == Level::R6 && i == qd)).collect()
|
||||
(0..n)
|
||||
.filter(|&i| i != pd && !(level == Level::R6 && i == qd))
|
||||
.collect()
|
||||
};
|
||||
let mut p = vec![0u8; C];
|
||||
let mut q = vec![0u8; C];
|
||||
@@ -83,7 +85,10 @@ fn members(imgs: &[Vec<u8>], missing: &[usize]) -> Vec<Option<Member>> {
|
||||
imgs.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
(!missing.contains(&i)).then(|| Member { src: Arc::new(MemSource(v.clone())), data_offset: 0 })
|
||||
(!missing.contains(&i)).then(|| Member {
|
||||
src: Arc::new(MemSource(v.clone())),
|
||||
data_offset: 0,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -92,10 +97,19 @@ fn check(level: Level, layout: Layout, n: usize, missing: &[usize]) {
|
||||
let dd = if level == Level::R6 { n - 2 } else { n - 1 };
|
||||
let logical = data(C * dd * 23);
|
||||
let imgs = encode_parity(level, layout, n, &logical);
|
||||
let r = RaidSource::new(RaidParams::simple(level, C as u64, layout, members(&imgs, missing))).unwrap();
|
||||
let r = RaidSource::new(RaidParams::simple(
|
||||
level,
|
||||
C as u64,
|
||||
layout,
|
||||
members(&imgs, missing),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(r.size(), logical.len() as u64);
|
||||
let got = read_vec(&r, 0, logical.len()).unwrap();
|
||||
assert!(got == logical, "{level:?} {layout:?} n={n} faltando {missing:?}");
|
||||
assert!(
|
||||
got == logical,
|
||||
"{level:?} {layout:?} n={n} faltando {missing:?}"
|
||||
);
|
||||
// leitura desalinhada
|
||||
let part = read_vec(&r, 5000, 30000).unwrap();
|
||||
assert_eq!(part, &logical[5000..35000]);
|
||||
@@ -136,15 +150,33 @@ fn raid0_raid1_linear_and_raid10_near() {
|
||||
for (c, ch) in logical.chunks(C).enumerate() {
|
||||
imgs[c % 3].extend_from_slice(ch);
|
||||
}
|
||||
let r = RaidSource::new(RaidParams::simple(Level::R0, C as u64, Layout::LeftSymmetric, members(&imgs, &[]))).unwrap();
|
||||
let r = RaidSource::new(RaidParams::simple(
|
||||
Level::R0,
|
||||
C as u64,
|
||||
Layout::LeftSymmetric,
|
||||
members(&imgs, &[]),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||
// RAID 1, um membro caído
|
||||
let m = vec![logical.clone(), logical.clone()];
|
||||
let r = RaidSource::new(RaidParams::simple(Level::R1, C as u64, Layout::LeftSymmetric, members(&m, &[0]))).unwrap();
|
||||
let r = RaidSource::new(RaidParams::simple(
|
||||
Level::R1,
|
||||
C as u64,
|
||||
Layout::LeftSymmetric,
|
||||
members(&m, &[0]),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||
// Linear
|
||||
let (a, b) = logical.split_at(C * 10);
|
||||
let r = RaidSource::new(RaidParams::simple(Level::Linear, C as u64, Layout::LeftSymmetric, members(&[a.to_vec(), b.to_vec()], &[]))).unwrap();
|
||||
let r = RaidSource::new(RaidParams::simple(
|
||||
Level::Linear,
|
||||
C as u64,
|
||||
Layout::LeftSymmetric,
|
||||
members(&[a.to_vec(), b.to_vec()], &[]),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||
// RAID 10 near=2 com 4 discos: slots em sequência.
|
||||
let n = 4;
|
||||
@@ -156,7 +188,12 @@ fn raid0_raid1_linear_and_raid10_near() {
|
||||
imgs[disk][row * C..(row + 1) * C].copy_from_slice(ch);
|
||||
}
|
||||
}
|
||||
let mut p = RaidParams::simple(Level::R10, C as u64, Layout::LeftSymmetric, members(&imgs, &[1]));
|
||||
let mut p = RaidParams::simple(
|
||||
Level::R10,
|
||||
C as u64,
|
||||
Layout::LeftSymmetric,
|
||||
members(&imgs, &[1]),
|
||||
);
|
||||
p.near = 2;
|
||||
p.far = 1;
|
||||
let r = RaidSource::new(p).unwrap();
|
||||
@@ -164,7 +201,18 @@ fn raid0_raid1_linear_and_raid10_near() {
|
||||
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||
}
|
||||
|
||||
fn md_v1_super(uuid: u8, level: u32, layout: u32, raid_disks: u32, dev_number: u32, role: u16, data_off_sectors: u64, data_sectors: u64, events: u64) -> Vec<u8> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn md_v1_super(
|
||||
uuid: u8,
|
||||
level: u32,
|
||||
layout: u32,
|
||||
raid_disks: u32,
|
||||
dev_number: u32,
|
||||
role: u16,
|
||||
data_off_sectors: u64,
|
||||
data_sectors: u64,
|
||||
events: 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());
|
||||
@@ -199,7 +247,17 @@ fn assembles_from_md_v12_superblocks_with_a_missing_disk() {
|
||||
continue; // disco perdido
|
||||
}
|
||||
let mut full = vec![0u8; 4096 + (data_off as usize) * 512 + im.len()];
|
||||
let sb = md_v1_super(7, 5, 2, n as u32, i as u32, i as u16, data_off, (im.len() / 512) as u64, 100);
|
||||
let sb = md_v1_super(
|
||||
7,
|
||||
5,
|
||||
2,
|
||||
n as u32,
|
||||
i as u32,
|
||||
i as u16,
|
||||
data_off,
|
||||
(im.len() / 512) as u64,
|
||||
100,
|
||||
);
|
||||
full[4096..8192].copy_from_slice(&sb);
|
||||
full[4096 + data_off as usize * 512..][..im.len()].copy_from_slice(im);
|
||||
// o superbloco 1.2 fica a 4 KiB; os dados começam em data_offset a partir do início
|
||||
|
||||
@@ -7,7 +7,11 @@ 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)
|
||||
Command::new("sh")
|
||||
.args(["-c", &format!("command -v {t}")])
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn ext4_image(mib: u64) -> Option<Vec<u8>> {
|
||||
@@ -19,18 +23,33 @@ fn ext4_image(mib: u64) -> Option<Vec<u8>> {
|
||||
for d in 0..12 {
|
||||
std::fs::create_dir_all(root.join(format!("pasta{d}/sub"))).unwrap();
|
||||
for f in 0..40 {
|
||||
std::fs::write(root.join(format!("pasta{d}/arq{f}.txt")), vec![b'a' + (f % 26) as u8; 300 + f * 37]).unwrap();
|
||||
std::fs::write(
|
||||
root.join(format!("pasta{d}/arq{f}.txt")),
|
||||
vec![b'a' + (f % 26) as u8; 300 + f * 37],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
let img = dir.path().join("e.img");
|
||||
std::fs::File::create(&img).unwrap().set_len(mib << 20).unwrap();
|
||||
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-b", "1024", "-d"]).arg(&root).arg(&img).status().unwrap().success());
|
||||
std::fs::File::create(&img)
|
||||
.unwrap()
|
||||
.set_len(mib << 20)
|
||||
.unwrap();
|
||||
assert!(Command::new("mke2fs")
|
||||
.args(["-q", "-F", "-t", "ext4", "-b", "1024", "-d"])
|
||||
.arg(&root)
|
||||
.arg(&img)
|
||||
.status()
|
||||
.unwrap()
|
||||
.success());
|
||||
Some(std::fs::read(img).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_raid5_chunk_layout_and_shuffled_order() {
|
||||
let Some(logical) = ext4_image(24) else { return };
|
||||
let Some(logical) = ext4_image(24) else {
|
||||
return;
|
||||
};
|
||||
let (n, chunk) = (3usize, 32768usize);
|
||||
let dd = n - 1;
|
||||
let rows = logical.len() / (chunk * dd);
|
||||
@@ -51,11 +70,17 @@ fn finds_raid5_chunk_layout_and_shuffled_order() {
|
||||
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
|
||||
}
|
||||
// Entrega os discos embaralhados: fornecidos = [2, 0, 1] do array real.
|
||||
let given: Vec<Arc<dyn Source>> = [2usize, 0, 1].iter().map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>).collect();
|
||||
let given: Vec<Arc<dyn Source>> = [2usize, 0, 1]
|
||||
.iter()
|
||||
.map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>)
|
||||
.collect();
|
||||
let found = raid_detect::detect(&given, &|| false);
|
||||
assert!(!found.is_empty());
|
||||
for c in found.iter().take(6) {
|
||||
eprintln!("CAND {:?} {} {:?} {:?} {:.2}", c.level, c.chunk, c.layout, c.order, c.score);
|
||||
eprintln!(
|
||||
"CAND {:?} {} {:?} {:?} {:.2}",
|
||||
c.level, c.chunk, c.layout, c.order, c.score
|
||||
);
|
||||
}
|
||||
let best = &found[0];
|
||||
assert_eq!(best.level, Level::R5);
|
||||
@@ -63,19 +88,30 @@ fn finds_raid5_chunk_layout_and_shuffled_order() {
|
||||
// Layouts e ordens diferentes podem ser equivalentes; o que importa é o conteúdo montado.
|
||||
let r = guara_core::raid::RaidSource::new(best.params(&given)).unwrap();
|
||||
let got = guara_core::source::read_vec(&r, 0, logical.len()).unwrap();
|
||||
assert!(got == logical, "montagem escolhida ({:?}, {:?}, {:?}) não reproduz o volume", best.layout, best.order, best.level);
|
||||
assert!(
|
||||
got == logical,
|
||||
"montagem escolhida ({:?}, {:?}, {:?}) não reproduz o volume",
|
||||
best.layout,
|
||||
best.order,
|
||||
best.level
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_raid0() {
|
||||
let Some(logical) = ext4_image(24) else { return };
|
||||
let Some(logical) = ext4_image(24) else {
|
||||
return;
|
||||
};
|
||||
let (n, chunk) = (2usize, 65536usize);
|
||||
let rows = logical.len() / (chunk * n);
|
||||
let mut disks = vec![vec![]; n];
|
||||
for c in 0..rows * n {
|
||||
disks[c % n].extend_from_slice(&logical[c * chunk..(c + 1) * chunk]);
|
||||
}
|
||||
let given: Vec<Arc<dyn Source>> = [1usize, 0].iter().map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>).collect();
|
||||
let given: Vec<Arc<dyn Source>> = [1usize, 0]
|
||||
.iter()
|
||||
.map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>)
|
||||
.collect();
|
||||
let found = raid_detect::detect(&given, &|| false);
|
||||
let best = &found[0];
|
||||
assert_eq!(best.level, Level::R0);
|
||||
|
||||
+102
-16
@@ -8,7 +8,11 @@ use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn have(t: &str) -> bool {
|
||||
Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false)
|
||||
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> {
|
||||
@@ -36,7 +40,11 @@ fn be64(b: &[u8], o: usize) -> u64 {
|
||||
fn unpack(r: &[u8]) -> (u64, u64, u64) {
|
||||
let hi = be64(r, 0);
|
||||
let lo = be64(r, 8);
|
||||
((hi >> 9) & ((1 << 54) - 1), ((hi & 0x1FF) << 43) | (lo >> 21), lo & 0x1F_FFFF)
|
||||
(
|
||||
(hi >> 9) & ((1 << 54) - 1),
|
||||
((hi & 0x1FF) << 43) | (lo >> 21),
|
||||
lo & 0x1F_FFFF,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -55,7 +63,11 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
|
||||
std::fs::write(src.join(format!("a_longa_{i}.txt")), format!("arquivo {i}")).unwrap();
|
||||
}
|
||||
let mut proto = String::from("/dev/null\n0 0\nd--755 0 0\ndocs d--755 0 0\n");
|
||||
let mut names: Vec<_> = std::fs::read_dir(&src).unwrap().flatten().map(|e| e.file_name().to_string_lossy().to_string()).collect();
|
||||
let mut names: Vec<_> = std::fs::read_dir(&src)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
names.sort();
|
||||
for n in &names {
|
||||
proto.push_str(&format!(" {n} ---644 0 0 {}\n", src.join(n).display()));
|
||||
@@ -64,8 +76,16 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
|
||||
let pf = dir.path().join("proto");
|
||||
std::fs::write(&pf, proto).unwrap();
|
||||
let img = dir.path().join("x.img");
|
||||
std::fs::File::create(&img).unwrap().set_len(320 << 20).unwrap();
|
||||
let o = Command::new("mkfs.xfs").args(["-q", "-f", "-p"]).arg(&pf).arg(&img).output().unwrap();
|
||||
std::fs::File::create(&img)
|
||||
.unwrap()
|
||||
.set_len(320 << 20)
|
||||
.unwrap();
|
||||
let o = Command::new("mkfs.xfs")
|
||||
.args(["-q", "-f", "-p"])
|
||||
.arg(&pf)
|
||||
.arg(&img)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr));
|
||||
|
||||
let db = |cmds: &[&str]| -> String {
|
||||
@@ -78,11 +98,39 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
|
||||
String::from_utf8_lossy(&c.output().unwrap().stdout).to_string()
|
||||
};
|
||||
let nc = db(&["blockget -n", "ncheck"]);
|
||||
let ino: u64 = nc.lines().find(|l| l.ends_with("docs/relatorio.dat")).unwrap().split_whitespace().next().unwrap().parse().unwrap();
|
||||
let ino: u64 = nc
|
||||
.lines()
|
||||
.find(|l| l.ends_with("docs/relatorio.dat"))
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
let conv = db(&[&format!("convert ino {ino} byte")]);
|
||||
let ino_off = usize::from_str_radix(conv.split("0x").nth(1).unwrap().split_whitespace().next().unwrap().trim_end_matches(')'), 16).unwrap();
|
||||
let ino_off = usize::from_str_radix(
|
||||
conv.split("0x")
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap()
|
||||
.trim_end_matches(')'),
|
||||
16,
|
||||
)
|
||||
.unwrap();
|
||||
let sbv = db(&["sb 0", "p agblocks agblklog inopblog"]);
|
||||
let num = |k: &str| -> u64 { sbv.lines().find(|l| l.starts_with(k)).unwrap().split('=').nth(1).unwrap().trim().parse().unwrap() };
|
||||
let num = |k: &str| -> u64 {
|
||||
sbv.lines()
|
||||
.find(|l| l.starts_with(k))
|
||||
.unwrap()
|
||||
.split('=')
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap()
|
||||
};
|
||||
let (agblocks, agblklog, inopblog) = (num("agblocks"), num("agblklog"), num("inopblog"));
|
||||
|
||||
let mut b = std::fs::read(&img).unwrap();
|
||||
@@ -90,8 +138,10 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
|
||||
// extents do arquivo (no inode, formato 3)
|
||||
assert_eq!(b[ino_off + 5], 2);
|
||||
let nex = be32(&b[ino_off..], 76) as usize;
|
||||
let exts: Vec<_> = (0..nex).map(|i| unpack(&b[ino_off + 176 + i * 16..])).collect();
|
||||
assert_eq!(exts.iter().map(|e| e.2).sum::<u64>() * 4096 >= 150_000, true);
|
||||
let exts: Vec<_> = (0..nex)
|
||||
.map(|i| unpack(&b[ino_off + 176 + i * 16..]))
|
||||
.collect();
|
||||
assert!(exts.iter().map(|e| e.2).sum::<u64>() * 4096 >= 150_000);
|
||||
// 1) libera o inode como o kernel: modo 0, links 0, tamanho 0, 0 extents
|
||||
b[ino_off + 2..ino_off + 4].fill(0);
|
||||
b[ino_off + 16..ino_off + 20].fill(0);
|
||||
@@ -125,7 +175,14 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
|
||||
assert_eq!(blevel, 1);
|
||||
let bb = ag_off + broot * bs;
|
||||
let nrec = be16(&b[bb..], 6) as usize;
|
||||
let mut recs: Vec<(u32, u32)> = (0..nrec).map(|i| (be32(&b[bb + 56 + i * 8..], 0), be32(&b[bb + 56 + i * 8..], 4))).collect();
|
||||
let mut recs: Vec<(u32, u32)> = (0..nrec)
|
||||
.map(|i| {
|
||||
(
|
||||
be32(&b[bb + 56 + i * 8..], 0),
|
||||
be32(&b[bb + 56 + i * 8..], 4),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for e in &exts {
|
||||
recs.push(((e.1 & ((1 << agblklog) - 1)) as u32, e.2 as u32));
|
||||
}
|
||||
@@ -144,18 +201,38 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
|
||||
b[bb + 6..bb + 8].copy_from_slice(&(merged.len() as u16).to_be_bytes());
|
||||
// 4) entrada de diretório liberada: perde os 4 primeiros bytes do inode (viram marca + tamanho)
|
||||
let name = b"relatorio.dat";
|
||||
let pos = (0..b.len() - 20).find(|&p| &b[p + 9..p + 9 + name.len()] == name && b[p + 8] as usize == name.len() && be64(&b[p..], 0) == ino).expect("entrada do diretório");
|
||||
let pos = (0..b.len() - 20)
|
||||
.find(|&p| {
|
||||
&b[p + 9..p + 9 + name.len()] == name
|
||||
&& b[p + 8] as usize == name.len()
|
||||
&& be64(&b[p..], 0) == ino
|
||||
})
|
||||
.expect("entrada do diretório");
|
||||
let size = (8 + 1 + name.len() + 1 + 2usize).div_ceil(8) * 8;
|
||||
b[pos..pos + 2].copy_from_slice(&[0xFF, 0xFF]);
|
||||
b[pos + 2..pos + 4].copy_from_slice(&(size as u16).to_be_bytes());
|
||||
std::fs::write(&img, &b).unwrap();
|
||||
|
||||
let c = scan_path(&img, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
||||
let c = scan_path(
|
||||
&img,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Deep,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let f = c
|
||||
.found
|
||||
.iter()
|
||||
.find(|f| f.origin == Origin::Xfs)
|
||||
.unwrap_or_else(|| panic!("nada do XFS: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::<Vec<_>>()));
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"nada do XFS: {:?}",
|
||||
c.found
|
||||
.iter()
|
||||
.map(|f| (&f.name, f.origin))
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
});
|
||||
assert_eq!(f.name, "relatorio.dat");
|
||||
assert_eq!(f.dir.as_deref(), Some("/docs"));
|
||||
assert!(f.confidence >= Confidence::Medium, "{:?}", f.note);
|
||||
@@ -168,7 +245,10 @@ fn deleted_file_is_recovered_from_free_inode_and_stale_directory_entry() {
|
||||
assert!(got.starts_with(&doc), "conteúdo inicial difere");
|
||||
assert_eq!(got.len() % 4096, 0);
|
||||
// Só um arquivo apagado.
|
||||
assert_eq!(c.found.iter().filter(|f| f.origin == Origin::Xfs).count(), 1);
|
||||
assert_eq!(
|
||||
c.found.iter().filter(|f| f.origin == Origin::Xfs).count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -178,7 +258,13 @@ fn reference_image_is_recognized() {
|
||||
return;
|
||||
}
|
||||
init();
|
||||
let c = scan_path(&p, &ScanOptions { mode: ScanMode::Quick, ..Default::default() });
|
||||
let c = scan_path(
|
||||
&p,
|
||||
&ScanOptions {
|
||||
mode: ScanMode::Quick,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let l = c.layout.unwrap();
|
||||
assert_eq!(l.partitions[0].fs, FsKind::Xfs);
|
||||
}
|
||||
Reference in new issue
Block a user