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
@@ -0,0 +1,276 @@
|
||||
//! Acesso somente leitura a discos, partições e imagens.
|
||||
//!
|
||||
//! Nenhum tipo deste módulo expõe escrita: a origem nunca é modificada.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Origem de bytes legível por posição absoluta.
|
||||
pub trait Source: Send + Sync {
|
||||
fn size(&self) -> u64;
|
||||
/// Lê até `buf.len()` bytes a partir de `offset`. Retorna 0 no fim.
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize>;
|
||||
}
|
||||
|
||||
impl<T: Source + ?Sized> Source for Arc<T> {
|
||||
fn size(&self) -> u64 {
|
||||
(**self).size()
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
(**self).read_at(offset, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lê exatamente `buf.len()` bytes, ou falha com `UnexpectedEof`.
|
||||
pub fn read_exact_at(src: &dyn Source, mut offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
|
||||
while !buf.is_empty() {
|
||||
let n = src.read_at(offset, buf)?;
|
||||
if n == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"fim da origem",
|
||||
));
|
||||
}
|
||||
offset += n as u64;
|
||||
buf = &mut buf[n..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lê o máximo possível (pode devolver menos que `len` no fim da origem).
|
||||
pub fn read_vec(src: &dyn Source, offset: u64, len: usize) -> io::Result<Vec<u8>> {
|
||||
let mut v = vec![0u8; len];
|
||||
let mut got = 0;
|
||||
while got < len {
|
||||
let n = src.read_at(offset + got as u64, &mut v[got..])?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
got += n;
|
||||
}
|
||||
v.truncate(got);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Arquivo de imagem ou dispositivo de blocos aberto somente para leitura.
|
||||
pub struct FileSource {
|
||||
file: File,
|
||||
size: u64,
|
||||
/// Dispositivos brutos (Windows/macOS) exigem leituras alinhadas ao setor.
|
||||
align: u64,
|
||||
}
|
||||
|
||||
impl FileSource {
|
||||
pub fn open(path: &Path) -> io::Result<Self> {
|
||||
let meta = std::fs::metadata(path)?;
|
||||
let is_device = is_device(&meta);
|
||||
let file = open_readonly(path, is_device)?;
|
||||
let size = if is_device {
|
||||
device_size(&file, path)?
|
||||
} else {
|
||||
meta.len()
|
||||
};
|
||||
let align = if is_device && cfg!(any(windows, target_os = "macos")) {
|
||||
4096
|
||||
} else {
|
||||
1
|
||||
};
|
||||
Ok(Self { file, size, align })
|
||||
}
|
||||
|
||||
fn raw_read(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::FileExt::read_at(&self.file, buf, offset)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::os::windows::fs::FileExt::seek_read(&self.file, buf, offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for FileSource {
|
||||
fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if offset >= self.size || buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let want = buf.len().min((self.size - offset) as usize);
|
||||
if self.align <= 1 {
|
||||
return self.raw_read(offset, &mut buf[..want]);
|
||||
}
|
||||
// Leitura alinhada para dispositivos brutos.
|
||||
let a = self.align;
|
||||
let start = offset / a * a;
|
||||
let end = ((offset + want as u64).div_ceil(a) * a).min(self.size.div_ceil(a) * a);
|
||||
let mut tmp = vec![0u8; (end - start) as usize];
|
||||
let mut got = 0;
|
||||
while got < tmp.len() {
|
||||
let n = self.raw_read(start + got as u64, &mut tmp[got..])?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
got += n;
|
||||
}
|
||||
let skip = (offset - start) as usize;
|
||||
if got <= skip {
|
||||
return Ok(0);
|
||||
}
|
||||
let n = (got - skip).min(want);
|
||||
buf[..n].copy_from_slice(&tmp[skip..skip + n]);
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Janela [start, start+len) de outra origem (uma partição, por exemplo).
|
||||
pub struct SubSource {
|
||||
inner: Arc<dyn Source>,
|
||||
start: u64,
|
||||
len: u64,
|
||||
}
|
||||
|
||||
impl SubSource {
|
||||
pub fn new(inner: Arc<dyn Source>, start: u64, len: u64) -> Self {
|
||||
let len = len.min(inner.size().saturating_sub(start));
|
||||
Self { inner, start, len }
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for SubSource {
|
||||
fn size(&self) -> u64 {
|
||||
self.len
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if offset >= self.len {
|
||||
return Ok(0);
|
||||
}
|
||||
let want = buf.len().min((self.len - offset) as usize);
|
||||
self.inner.read_at(self.start + offset, &mut buf[..want])
|
||||
}
|
||||
}
|
||||
|
||||
/// Origem em memória (testes e dados pequenos).
|
||||
pub struct MemSource(pub Vec<u8>);
|
||||
|
||||
impl Source for MemSource {
|
||||
fn size(&self) -> u64 {
|
||||
self.0.len() as u64
|
||||
}
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let off = offset as usize;
|
||||
if off >= self.0.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let n = buf.len().min(self.0.len() - off);
|
||||
buf[..n].copy_from_slice(&self.0[off..off + n]);
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_device(meta: &std::fs::Metadata) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
let ft = meta.file_type();
|
||||
ft.is_block_device() || ft.is_char_device()
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = meta;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn open_readonly(path: &Path, _device: bool) -> io::Result<File> {
|
||||
std::fs::OpenOptions::new().read(true).open(path)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn open_readonly(path: &Path, _device: bool) -> io::Result<File> {
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
// FILE_SHARE_READ | FILE_SHARE_WRITE: não bloqueia outros processos.
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.share_mode(0x1 | 0x2)
|
||||
.open(path)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn device_size(file: &File, _path: &Path) -> io::Result<u64> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::os::unix::io::AsRawFd;
|
||||
const DKIOCGETBLOCKSIZE: libc::c_ulong = 0x4004_6418;
|
||||
const DKIOCGETBLOCKCOUNT: libc::c_ulong = 0x4008_6419;
|
||||
let fd = file.as_raw_fd();
|
||||
let (mut bs, mut bc) = (0u32, 0u64);
|
||||
// SAFETY: ioctls de leitura com ponteiros para variáveis locais válidas.
|
||||
let ok = unsafe {
|
||||
libc::ioctl(fd, DKIOCGETBLOCKSIZE, &mut bs) == 0
|
||||
&& libc::ioctl(fd, DKIOCGETBLOCKCOUNT, &mut bc) == 0
|
||||
};
|
||||
if ok {
|
||||
return Ok(bs as u64 * bc);
|
||||
}
|
||||
}
|
||||
use std::io::{Seek, SeekFrom};
|
||||
let mut f = file.try_clone()?;
|
||||
f.seek(SeekFrom::End(0))
|
||||
}
|
||||
|
||||
/// Abre um dispositivo Windows (`\\.\PhysicalDriveN`) e obtém o tamanho.
|
||||
#[cfg(windows)]
|
||||
pub fn open_windows_device(path: &Path) -> io::Result<FileSource> {
|
||||
let file = open_readonly(path, true)?;
|
||||
let size = device_size(&file, path)?;
|
||||
Ok(FileSource {
|
||||
file,
|
||||
size,
|
||||
align: 4096,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn device_size(file: &File, _path: &Path) -> io::Result<u64> {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use windows_sys::Win32::System::Ioctl::{GET_LENGTH_INFORMATION, IOCTL_DISK_GET_LENGTH_INFO};
|
||||
use windows_sys::Win32::System::IO::DeviceIoControl;
|
||||
let mut info = GET_LENGTH_INFORMATION { Length: 0 };
|
||||
let mut ret = 0u32;
|
||||
// SAFETY: buffer de saída válido com o tamanho exato da estrutura.
|
||||
let ok = unsafe {
|
||||
DeviceIoControl(
|
||||
file.as_raw_handle() as _,
|
||||
IOCTL_DISK_GET_LENGTH_INFO,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
&mut info as *mut _ as *mut _,
|
||||
std::mem::size_of::<GET_LENGTH_INFORMATION>() as u32,
|
||||
&mut ret,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(info.Length as u64)
|
||||
}
|
||||
|
||||
/// Abre um caminho (imagem ou dispositivo) como origem somente leitura.
|
||||
pub fn open_path(path: &Path) -> io::Result<Arc<dyn Source>> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let s = path.to_string_lossy();
|
||||
if s.starts_with(r"\\.\") {
|
||||
return Ok(Arc::new(open_windows_device(path)?));
|
||||
}
|
||||
}
|
||||
Ok(Arc::new(FileSource::open(path)?))
|
||||
}
|
||||
Reference in new issue
Block a user