Files
guara/crates/guara-core/src/carve.rs
T
Lucas RayanandClaude Sonnet 5 6328220c60
CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s
chore(i18n): finish translating code comments and scripts to English
Completes the English-only standardization for code, comments, scripts
and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en,
untouched). Covers the remaining crates/, apps/gui/ui/ and
apps/gui/src-tauri/ comments, .gitea/ workflow and template comments,
.githooks/ hooks, and the scripts/ source comments for the files
already renamed in a prior commit (check.sh, generate-logo.py,
install-hooks.sh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:43:02 -03:00

141 lines
4.5 KiB
Rust

//! Signature- and structure-based file carving over byte ranges.
use crate::signatures::{category_for_ext, detect, might_start, Hit};
use crate::source::Source;
use crate::types::Category;
pub const SECTOR: u64 = 512;
const CHUNK: usize = 8 * 1024 * 1024;
const LOOKAHEAD: usize = 4096;
const MIN_LEN: u64 = 256;
pub struct CarveCfg<'a> {
pub max_size: u64,
pub categories: &'a [Category],
}
/// Scans `[start, end)` starting from `*next`. Calls `on_hit(offset, hit)` and returns
/// `true` if the range completed (`false` if cancelled).
/// `advance(done_bytes)` reports progress within the range.
pub fn carve_range(
src: &dyn Source,
start: u64,
end: u64,
next: &mut u64,
cfg: &CarveCfg,
cancelled: &(dyn Fn() -> bool + Sync),
ev: &mut dyn CarveEvents,
) -> CarveEnd {
let end = end.min(src.size());
let mut pos = (*next).max(start);
// Align to sector boundary.
pos = pos.div_ceil(SECTOR) * SECTOR;
let mut buf = vec![0u8; CHUNK + LOOKAHEAD];
let mut skip_until = 0u64;
let mut fails = 0u32;
while pos < end {
if cancelled() {
*next = pos;
return CarveEnd::Cancelled;
}
let want = ((end - pos) as usize).min(CHUNK + LOOKAHEAD);
let got = match read_full(src, pos, &mut buf[..want]) {
Ok(n) => {
fails = 0;
n
}
Err(_) => {
// Multiple consecutive read failures = device disconnected: stop with resume point saved.
fails += 1;
if fails >= 4 {
*next = pos - (fails as u64 - 1) * CHUNK as u64;
return CarveEnd::IoLost;
}
// Isolated bad sector: skip a full block and continue (forensic imaging handles bad sectors better).
pos += CHUNK as u64;
ev.advance(CHUNK as u64);
*next = pos;
continue;
}
};
if got == 0 {
break;
}
let scan_len = got.min(CHUNK).min((end - pos) as usize);
ev.chunk(pos, &buf[..scan_len]);
let mut i = 0usize;
while i < scan_len {
let abs = pos + i as u64;
if abs < skip_until {
i += SECTOR as usize;
continue;
}
let b0 = buf[i];
if might_start(b0) {
let hend = (i + 128).min(got);
let head = &buf[i..hend];
if let Some(hit) = detect(src, abs, head, cfg.max_size) {
let allowed = cfg.categories.is_empty()
|| cfg.categories.contains(&category_for_ext(hit.ext));
if hit.len >= MIN_LEN && allowed {
let action = ev.hit(abs, &hit);
match action {
HitAction::Skip if hit.confidence != crate::types::Confidence::Low => {
skip_until = abs + hit.len;
}
HitAction::SkipLen(n) => skip_until = abs + n,
_ => {}
}
}
}
}
i += SECTOR as usize;
}
let step = scan_len as u64;
pos += step.div_ceil(SECTOR) * SECTOR;
ev.advance(step);
*next = pos;
}
*next = end;
CarveEnd::Done
}
/// Outcome of carving a byte range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarveEnd {
Done,
Cancelled,
/// Multiple consecutive reads failed: the device was likely disconnected.
IoLost,
}
/// Receiver for file-carving events.
pub trait CarveEvents {
fn advance(&mut self, bytes: u64);
/// Called for each chunk read before signature scanning (used to index candidate defrag clusters).
fn chunk(&mut self, _abs: u64, _data: &[u8]) {}
fn hit(&mut self, offset: u64, hit: &Hit) -> HitAction;
}
/// Action to take after a signature hit.
pub enum HitAction {
/// Skip past the recognized file content (default behavior).
Skip,
/// Skip only this many bytes (when the remainder of the file is unreliable).
SkipLen(u64),
/// Continue sector by sector (hit discarded).
Continue,
}
fn read_full(src: &dyn Source, off: u64, buf: &mut [u8]) -> std::io::Result<usize> {
let mut got = 0;
while got < buf.len() {
let n = src.read_at(off + got as u64, &mut buf[got..])?;
if n == 0 {
break;
}
got += n;
}
Ok(got)
}