Files
guara/crates/guara-core/src/image.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

197 lines
6.1 KiB
Rust

//! Forensic disk imaging: fault-tolerant reading across bad sectors with an error
//! map and integrity hashes so recovery always runs against a copy.
use crate::error::{Error, Result};
use crate::hashing::MultiHasher;
use crate::source::Source;
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ImageReport {
pub bytes: u64,
pub sha256: String,
#[serde(default)]
pub md5: String,
#[serde(default)]
pub sha1: String,
/// `(offset, length)` of unreadable byte ranges that were zero-filled in the output image.
pub bad_ranges: Vec<(u64, u64)>,
pub cancelled: bool,
}
const BLOCK: usize = 1 << 20;
const SECTOR: usize = 512;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum ImageFormat {
#[default]
Raw,
/// Expert Witness Format (EnCase 6): segmented `.E01`.
E01 {
#[serde(default)]
case_number: String,
#[serde(default)]
description: String,
#[serde(default)]
evidence_number: String,
#[serde(default)]
examiner: String,
#[serde(default)]
notes: String,
/// Maximum size of each segment in MiB (0 = single segment).
#[serde(default = "default_segment_mib")]
segment_mib: u64,
/// 0 = uncompressed, 1..=9 = deflate compression level.
#[serde(default = "default_compression")]
compression: u32,
},
/// AFF4 (v1.0 standard, snappy-compressed chunks).
Aff4 {
#[serde(default)]
case_number: String,
#[serde(default)]
examiner: String,
#[serde(default)]
evidence_number: String,
#[serde(default)]
notes: String,
},
}
fn default_segment_mib() -> u64 {
1500
}
fn default_compression() -> u32 {
1
}
pub fn create_image_as(
src: &dyn Source,
dest: &Path,
format: &ImageFormat,
cancelled: &(dyn Fn() -> bool + Sync),
progress: &mut dyn FnMut(u64, u64),
) -> Result<ImageReport> {
match format {
ImageFormat::Raw => create_image(src, dest, cancelled, progress),
ImageFormat::Aff4 {
case_number,
examiner,
evidence_number,
notes,
} => {
let opts = crate::aff4::Aff4Options {
case_name: case_number.clone(),
examiner: examiner.clone(),
notes: notes.clone(),
evidence_number: evidence_number.clone(),
..Default::default()
};
let dest = if dest
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("aff4"))
{
dest.to_path_buf()
} else {
dest.with_extension("aff4")
};
crate::aff4::write_aff4(src, &dest, &opts, cancelled, progress)
}
ImageFormat::E01 {
case_number,
description,
evidence_number,
examiner,
notes,
segment_mib,
compression,
} => {
let opts = crate::ewf::E01Options {
case_number: case_number.clone(),
description: description.clone(),
evidence_number: evidence_number.clone(),
examiner: examiner.clone(),
notes: notes.clone(),
segment_size: segment_mib * 1024 * 1024,
compression: *compression,
..Default::default()
};
// Destination path may be passed with or without the `.E01` extension.
let base = match dest.extension().and_then(|e| e.to_str()) {
Some(e) if e.eq_ignore_ascii_case("e01") => dest.with_extension(""),
_ => dest.to_path_buf(),
};
crate::ewf::write_e01(src, &base, &opts, cancelled, progress)
}
}
}
pub fn create_image(
src: &dyn Source,
dest: &Path,
cancelled: &(dyn Fn() -> bool + Sync),
progress: &mut dyn FnMut(u64, u64),
) -> Result<ImageReport> {
let mut out = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(dest)
.map_err(|e| {
Error::msg(format!(
"não foi possível criar {}: {e} (o arquivo não pode existir)",
dest.display()
))
})?;
crate::owner::give_to_user(dest);
let size = src.size();
let mut rep = ImageReport::default();
let mut hasher = MultiHasher::new();
let mut buf = vec![0u8; BLOCK];
let mut pos = 0u64;
while pos < size {
if cancelled() {
rep.cancelled = true;
break;
}
let want = (size - pos).min(BLOCK as u64) as usize;
match read_full(src, pos, &mut buf[..want]) {
Ok(()) => {}
Err(_) => {
// Retry sector by sector to isolate the exact bad sectors.
let mut o = 0;
while o < want {
let n = SECTOR.min(want - o);
if read_full(src, pos + o as u64, &mut buf[o..o + n]).is_err() {
buf[o..o + n].fill(0);
match rep.bad_ranges.last_mut() {
Some(l) if l.0 + l.1 == pos + o as u64 => l.1 += n as u64,
_ => rep.bad_ranges.push((pos + o as u64, n as u64)),
}
}
o += n;
}
}
}
out.write_all(&buf[..want])?;
hasher.update(&buf[..want]);
pos += want as u64;
progress(pos, size);
}
out.flush()?;
rep.bytes = pos;
let h = hasher.finalize();
rep.sha256 = h.sha256;
rep.md5 = h.md5;
rep.sha1 = h.sha1;
let map = dest.with_extension("map.json");
crate::owner::write_owned(&map, serde_json::to_vec_pretty(&rep)?)?;
Ok(rep)
}
fn read_full(src: &dyn Source, off: u64, buf: &mut [u8]) -> std::io::Result<()> {
crate::source::read_exact_at(src, off, buf)
}