Files
guara/crates/guara-core/src/image.rs
T
Lucas RayanandClaude Opus 5.5 beb83fef92
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Formatação (cargo fmt) e ajustes do clippy para o CI da 1.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:41:58 -03:00

197 lines
6.0 KiB
Rust

//! Imagem forense de um disco: leitura tolerante a setores ruins, com mapa de
//! erros e hash. Serve para trabalhar sempre sobre a cópia.
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, tamanho) dos trechos ilegíveis, preenchidos com zeros.
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 (EnCase 6): `.E01`, dividido em segmentos.
E01 {
#[serde(default)]
case_number: String,
#[serde(default)]
description: String,
#[serde(default)]
evidence_number: String,
#[serde(default)]
examiner: String,
#[serde(default)]
notes: String,
/// Tamanho máximo de cada segmento em MiB (0 = um arquivo só).
#[serde(default = "default_segment_mib")]
segment_mib: u64,
/// 0 = sem compressão, 1 a 9 = deflate.
#[serde(default = "default_compression")]
compression: u32,
},
/// AFF4 (padrão v1.0, blocos snappy).
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()
};
// O destino pode vir com ou sem a extensão .E01.
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(_) => {
// Refaz setor a setor para isolar os trechos ruins.
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)
}