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
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
141 lines
4.5 KiB
Rust
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)
|
|
}
|