feat: Guará 0.1.0 — recuperação de arquivos (núcleo, CLI e interface gráfica)
Núcleo em Rust somente leitura: NTFS, FAT, exFAT, partições MBR/GPT e perdidas, carving por estrutura, fusão sem duplicatas, confiança por arquivo, imagem forense, pausar/retomar. CLI `guara`, UI web/Tauri, CI e releases por tag. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
765cb0d4a2
57 files changed
+8621
No files matched your search
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# Gera imagens de disco de teste (FAT32, exFAT, NTFS, ext4) com arquivos
|
||||
# conhecidos, apaga alguns e grava os SHA-256 esperados em <saida>/expected-<fs>.txt.
|
||||
#
|
||||
# Uso: scripts/make-fixtures.sh <diretório-de-saída> [fat32 exfat ntfs ext4]
|
||||
# Requer (Linux): mtools, dosfstools, exfatprogs/exfat-fuse, ntfs-3g, e2fsprogs,
|
||||
# imagemagick, zip, python3 e FUSE para exFAT/NTFS.
|
||||
set -euo pipefail
|
||||
|
||||
OUT="$(realpath -m "${1:?uso: $0 <saida> [fs...]}")"
|
||||
shift || true
|
||||
FSES=("${@:-fat32 exfat ntfs ext4}")
|
||||
[ $# -eq 0 ] && FSES=(fat32 exfat ntfs ext4)
|
||||
mkdir -p "$OUT"
|
||||
SIZE_MB=96
|
||||
|
||||
CONTENT="$OUT/content"
|
||||
rm -rf "$CONTENT"; mkdir -p "$CONTENT/Documentos/Trabalho" "$CONTENT/Fotos"
|
||||
|
||||
# --- conteúdo de teste (mistura de formatos com e sem assinatura) ---
|
||||
convert -size 1400x900 plasma:fractal -quality 92 "$CONTENT/Fotos/praia.jpg"
|
||||
convert -size 900x700 plasma:red-blue "$CONTENT/Fotos/montanha.png"
|
||||
convert -size 600x400 plasma:green-yellow "$CONTENT/Fotos/mantida.jpg"
|
||||
head -c 700000 /dev/urandom | base64 > "$CONTENT/Documentos/Trabalho/notas.txt"
|
||||
head -c 3000000 /dev/urandom > "$CONTENT/Documentos/dados.bin"
|
||||
python3 - "$CONTENT" <<'PY'
|
||||
import sys, sqlite3, os
|
||||
d = sys.argv[1]
|
||||
pdf = b"%PDF-1.4\n" + b"".join(b"%% linha %d preenchimento %s\n" % (i, os.urandom(40).hex().encode()) for i in range(6000)) + b"trailer\n<< /Root 1 0 R >>\n%%EOF\n"
|
||||
open(d + "/Documentos/Trabalho/relatorio.pdf", "wb").write(pdf)
|
||||
db = d + "/Documentos/contatos.db"
|
||||
c = sqlite3.connect(db)
|
||||
c.execute("create table t(id integer primary key, v text)")
|
||||
c.executemany("insert into t(v) values (?)", [(os.urandom(64).hex(),) for _ in range(4000)])
|
||||
c.commit(); c.close()
|
||||
PY
|
||||
mkdir -p "$OUT/zip" && ( cd "$OUT/zip" && rm -rf * && mkdir -p word _rels && echo '<?xml version="1.0"?><Types/>' > '[Content_Types].xml' \
|
||||
&& head -c 400000 /dev/urandom | base64 > word/document.xml && zip -q -r "$CONTENT/Documentos/Trabalho/tese.docx" . )
|
||||
rm -rf "$OUT/zip"
|
||||
|
||||
# Arquivos que serão APAGADOS (caminhos relativos a $CONTENT)
|
||||
DELETE=(Fotos/praia.jpg Fotos/montanha.png Documentos/Trabalho/notas.txt Documentos/Trabalho/relatorio.pdf Documentos/Trabalho/tese.docx Documentos/contatos.db Documentos/dados.bin)
|
||||
|
||||
write_expected() { # $1=fs
|
||||
: > "$OUT/expected-$1.txt"
|
||||
for f in "${DELETE[@]}"; do
|
||||
echo "$(sha256sum "$CONTENT/$f" | cut -d' ' -f1) $f" >> "$OUT/expected-$1.txt"
|
||||
done
|
||||
}
|
||||
|
||||
mnt_cleanup() { fusermount -u "$1" 2>/dev/null || true; rmdir "$1" 2>/dev/null || true; }
|
||||
|
||||
for fs in "${FSES[@]}"; do
|
||||
img="$OUT/$fs.img"
|
||||
rm -f "$img"; truncate -s ${SIZE_MB}M "$img"
|
||||
case $fs in
|
||||
fat32)
|
||||
mkfs.vfat -F 32 -n GUARATEST "$img" >/dev/null
|
||||
mcopy -i "$img" -s -m "$CONTENT"/* ::
|
||||
for f in "${DELETE[@]}"; do mdel -i "$img" "::/$f"; done
|
||||
;;
|
||||
exfat)
|
||||
# Sem FUSE sem privilégio, só o sistema vazio é criado aqui; o teste em Rust
|
||||
# injeta as entradas (veja crates/guara-core/tests/exfat.rs).
|
||||
mkfs.exfat -L GUARATEST "$img" >/dev/null
|
||||
;;
|
||||
ntfs)
|
||||
# ntfscp só grava na raiz; "apagar" = limpar o bit "em uso" do registro da MFT,
|
||||
# exatamente o que o NTFS faz (clusters e nome permanecem no disco).
|
||||
mkntfs -F -Q -L GUARATEST "$img" >/dev/null 2>&1
|
||||
for f in $(cd "$CONTENT" && find . -type f | sed 's|^\./||'); do
|
||||
ntfscp -f "$img" "$CONTENT/$f" "/$(basename "$f")" >/dev/null 2>&1
|
||||
done
|
||||
python3 "$(dirname "$0")/ntfs-mark-deleted.py" "$img" "${DELETE[@]}"
|
||||
;;
|
||||
ext4)
|
||||
mkfs.ext4 -q -F -L GUARATEST -d "$CONTENT" "$img"
|
||||
for f in "${DELETE[@]}"; do debugfs -w -R "rm /$f" "$img" >/dev/null 2>&1; done
|
||||
;;
|
||||
esac
|
||||
write_expected "$fs"
|
||||
echo "ok: $img"
|
||||
done
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Marca registros da MFT como apagados (limpa o bit 'em uso') para fixtures de teste.
|
||||
|
||||
Uso: ntfs-mark-deleted.py imagem.img caminho/arquivo1 caminho/arquivo2 ...
|
||||
Só o nome base é usado (ntfscp grava na raiz).
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
img, names = sys.argv[1], {os.path.basename(n) for n in sys.argv[2:]}
|
||||
f = open(img, "r+b")
|
||||
boot = f.read(512)
|
||||
bps = struct.unpack_from("<H", boot, 11)[0]
|
||||
spc = boot[13]
|
||||
mft_lcn = struct.unpack_from("<Q", boot, 48)[0]
|
||||
rs = struct.unpack_from("<b", boot, 64)[0]
|
||||
rec = rs * bps * spc if rs > 0 else 1 << -rs
|
||||
f.seek(mft_lcn * bps * spc)
|
||||
data = f.read(rec * 4096) # MFT inicial é contígua em imagens pequenas
|
||||
count = 0
|
||||
for i in range(len(data) // rec):
|
||||
r = bytearray(data[i * rec:(i + 1) * rec])
|
||||
if r[:4] != b"FILE" or not (r[22] & 1):
|
||||
continue
|
||||
p = struct.unpack_from("<H", r, 20)[0]
|
||||
while p + 8 <= len(r):
|
||||
t, ln = struct.unpack_from("<II", r, p)
|
||||
if t == 0xFFFFFFFF or ln == 0:
|
||||
break
|
||||
if t == 0x30 and r[p + 8] == 0:
|
||||
vo = p + struct.unpack_from("<H", r, p + 20)[0]
|
||||
nl = r[vo + 0x40]
|
||||
name = bytes(r[vo + 0x42:vo + 0x42 + nl * 2]).decode("utf-16le")
|
||||
if name in names and r[vo + 0x41] != 2:
|
||||
f.seek(mft_lcn * bps * spc + i * rec + 22)
|
||||
f.write(bytes([r[22] & ~1 & 0xFF]))
|
||||
count += 1
|
||||
break
|
||||
p += ln
|
||||
f.flush()
|
||||
print(f"marcados como apagados: {count}", file=sys.stderr)
|
||||
Reference in new issue
Block a user