Teste de XFS com exclusão feita pelo kernel
scripts/make-xfs-real-deletion.sh (precisa de root) monta uma imagem XFS por loop, grava arquivos, apaga alguns e desmonta, deixando o hash de cada um em expected.tsv. O teste confere que cada arquivo apagado volta pelo nome, na pasta certa e com o conteúdo original, e que os que ficaram não aparecem como apagados. O CI do Linux gera a imagem com sudo e exige o teste. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2d210fa240
commit
239f96263d
3 files changed
+158
-1
No files matched your search
@@ -46,7 +46,10 @@ jobs:
|
|||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y mtools dosfstools exfatprogs ntfs-3g e2fsprogs imagemagick zip
|
sudo apt-get install -y mtools dosfstools exfatprogs ntfs-3g e2fsprogs xfsprogs imagemagick zip
|
||||||
|
- name: Imagem XFS com exclusão feita pelo kernel (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: sudo scripts/make-xfs-real-deletion.sh
|
||||||
- name: Testes (com imagens reais FAT32/NTFS/ext4 no Linux)
|
- name: Testes (com imagens reais FAT32/NTFS/ext4 no Linux)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -268,3 +268,100 @@ fn reference_image_is_recognized() {
|
|||||||
let l = c.layout.unwrap();
|
let l = c.layout.unwrap();
|
||||||
assert_eq!(l.partitions[0].fs, FsKind::Xfs);
|
assert_eq!(l.partitions[0].fs, FsKind::Xfs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exclusão feita pelo kernel (scripts/make-xfs-real-deletion.sh, precisa de root): cada arquivo
|
||||||
|
/// apagado precisa voltar pelo nome, na pasta certa, com o conteúdo original no começo; os que
|
||||||
|
/// ficaram não podem aparecer como apagados. Pulado quando a imagem não foi gerada.
|
||||||
|
#[test]
|
||||||
|
fn real_kernel_deletion_is_recovered() {
|
||||||
|
let dir =
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/xfs-real-delete");
|
||||||
|
let (img, tsv) = (dir.join("disk.img"), dir.join("expected.tsv"));
|
||||||
|
if !(img.exists() && tsv.exists()) {
|
||||||
|
assert!(
|
||||||
|
std::env::var("GUARA_REQUIRE_FIXTURES").is_err(),
|
||||||
|
"faltou a imagem: sudo scripts/make-xfs-real-deletion.sh"
|
||||||
|
);
|
||||||
|
eprintln!("SKIP: rode sudo scripts/make-xfs-real-deletion.sh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
let rows: Vec<(String, u64, String, bool)> = std::fs::read_to_string(&tsv)
|
||||||
|
.unwrap()
|
||||||
|
.lines()
|
||||||
|
.map(|l| {
|
||||||
|
let c: Vec<&str> = l.split('\t').collect();
|
||||||
|
(
|
||||||
|
c[0].to_string(),
|
||||||
|
c[1].parse().unwrap(),
|
||||||
|
c[2].to_string(),
|
||||||
|
c[3] == "deleted",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let c = scan_path(
|
||||||
|
&img,
|
||||||
|
&ScanOptions {
|
||||||
|
mode: ScanMode::Deep,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let xfs: Vec<&Found> = c.found.iter().filter(|f| f.origin == Origin::Xfs).collect();
|
||||||
|
let listing = || {
|
||||||
|
xfs.iter()
|
||||||
|
.map(|f| {
|
||||||
|
format!(
|
||||||
|
"{}/{} ({} bytes, {:?})",
|
||||||
|
f.dir.as_deref().unwrap_or(""),
|
||||||
|
f.name,
|
||||||
|
f.size,
|
||||||
|
f.confidence
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
let out = tempfile::tempdir().unwrap();
|
||||||
|
for (rel, size, sha, deleted) in &rows {
|
||||||
|
let (dirname, name) = match rel.rsplit_once('/') {
|
||||||
|
Some((d, n)) => (format!("/{d}"), n),
|
||||||
|
None => (String::new(), rel.as_str()),
|
||||||
|
};
|
||||||
|
let hit = xfs
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == name && f.dir.as_deref().unwrap_or("") == dirname);
|
||||||
|
if !*deleted {
|
||||||
|
assert!(
|
||||||
|
hit.is_none(),
|
||||||
|
"{rel} não foi apagado, mas apareceu como recuperável: {:?}",
|
||||||
|
listing()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let f = hit.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"{rel} apagado pelo kernel não foi encontrado; achados: {:?}",
|
||||||
|
listing()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let rep = recover_all(&img, std::slice::from_ref(*f), &out.path().join(name));
|
||||||
|
assert!(rep.failed.is_empty(), "{rel}: {:?}", rep.failed);
|
||||||
|
let rp = PathBuf::from(&rep.recovered[0].path);
|
||||||
|
let got = std::fs::read(if rp.is_absolute() {
|
||||||
|
rp
|
||||||
|
} else {
|
||||||
|
out.path().join(name).join(rp)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
// o tamanho exato se perde na exclusão: o arquivo vem com os blocos inteiros
|
||||||
|
assert!(
|
||||||
|
got.len() as u64 >= *size,
|
||||||
|
"{rel}: veio menor ({} < {size})",
|
||||||
|
got.len()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sha256(&got[..*size as usize]),
|
||||||
|
*sha,
|
||||||
|
"{rel}: o conteúdo inicial difere do original"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Cria uma imagem XFS com exclusão REAL (feita pelo kernel), para validar a recuperação de XFS
|
||||||
|
# sem simulação. Precisa de root, porque monta a imagem por loop: sudo scripts/make-xfs-real-deletion.sh
|
||||||
|
#
|
||||||
|
# Saída em target/testdata/xfs-real-delete/: disk.img e expected.tsv (nome, tamanho, sha256 e se
|
||||||
|
# o arquivo foi apagado). O teste em crates/guara-core/tests/xfs.rs usa esses dois arquivos e é
|
||||||
|
# pulado quando eles não existem.
|
||||||
|
set -euo pipefail
|
||||||
|
[ "$(id -u)" = 0 ] || { echo "rode com sudo: sudo $0" >&2; exit 1; }
|
||||||
|
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
out="${1:-$root/target/testdata/xfs-real-delete}"
|
||||||
|
mkdir -p "$out"
|
||||||
|
img="$out/disk.img"
|
||||||
|
rm -f "$img"
|
||||||
|
truncate -s 64M "$img"
|
||||||
|
mkfs.xfs -q -f "$img"
|
||||||
|
mnt="$(mktemp -d)"
|
||||||
|
cleanup() { mountpoint -q "$mnt" && umount "$mnt"; rmdir "$mnt" 2>/dev/null || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
mount -o loop "$img" "$mnt"
|
||||||
|
python3 - "$mnt" "$out/expected.tsv" <<'PY'
|
||||||
|
import hashlib, os, sys
|
||||||
|
mnt, tsv = sys.argv[1], sys.argv[2]
|
||||||
|
def data(seed, n):
|
||||||
|
x, out = seed | 1, bytearray()
|
||||||
|
for _ in range(n):
|
||||||
|
x ^= (x << 13) & 0xFFFFFFFFFFFFFFFF; x ^= x >> 7; x ^= (x << 17) & 0xFFFFFFFFFFFFFFFF
|
||||||
|
out.append((x >> 24) & 0xFF)
|
||||||
|
return bytes(out)
|
||||||
|
files = [ # (caminho, tamanho, semente, apagar?)
|
||||||
|
("docs/relatorio.dat", 150000, 5, True),
|
||||||
|
("docs/mantido.dat", 90000, 6, False),
|
||||||
|
("fotos/foto1.bin", 300000, 7, True),
|
||||||
|
("fotos/foto2.bin", 40000, 8, False),
|
||||||
|
("raiz-apagada.txt", 20000, 9, True),
|
||||||
|
]
|
||||||
|
rows = []
|
||||||
|
for rel, n, seed, gone in files:
|
||||||
|
p = os.path.join(mnt, rel)
|
||||||
|
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||||
|
d = data(seed, n)
|
||||||
|
with open(p, "wb") as f:
|
||||||
|
f.write(d)
|
||||||
|
rows.append((rel, n, hashlib.sha256(d).hexdigest(), "deleted" if gone else "live"))
|
||||||
|
os.sync()
|
||||||
|
for rel, n, seed, gone in files:
|
||||||
|
if gone:
|
||||||
|
os.remove(os.path.join(mnt, rel))
|
||||||
|
os.sync()
|
||||||
|
with open(tsv, "w") as f:
|
||||||
|
for r in rows:
|
||||||
|
f.write("\t".join(map(str, r)) + "\n")
|
||||||
|
PY
|
||||||
|
sync
|
||||||
|
umount "$mnt"
|
||||||
|
[ -n "${SUDO_UID:-}" ] && chown -R "$SUDO_UID:${SUDO_GID:-$SUDO_UID}" "$out"
|
||||||
|
echo "ok: $img"
|
||||||
Reference in new issue
Block a user