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
328 lines
9.9 KiB
Rust
328 lines
9.9 KiB
Rust
//! Read-only positional access to disks, partitions, and forensic images.
|
|
//!
|
|
//! No type in this module exposes write operations: the source is never modified.
|
|
|
|
use std::fs::File;
|
|
use std::io;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
/// Read-only byte source addressable by absolute 64-bit byte offset.
|
|
pub trait Source: Send + Sync {
|
|
fn size(&self) -> u64;
|
|
/// Reads up to `buf.len()` bytes starting at `offset`. Returns `0` at EOF.
|
|
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)
|
|
}
|
|
}
|
|
|
|
/// Reads exactly `buf.len()` bytes at `offset`, or fails with `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(())
|
|
}
|
|
|
|
/// Reads up to `len` bytes into a `Vec<u8>` (may return fewer bytes near EOF).
|
|
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)
|
|
}
|
|
|
|
/// Image file or raw block device opened strictly read-only.
|
|
pub struct FileSource {
|
|
file: File,
|
|
size: u64,
|
|
/// Raw block devices on Windows/macOS require sector-aligned reads.
|
|
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]);
|
|
}
|
|
// Bounce buffer for sector-aligned reads on raw devices.
|
|
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)
|
|
}
|
|
}
|
|
|
|
/// Sub-window `[start, start + len)` of another `Source` (e.g. a partition).
|
|
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])
|
|
}
|
|
}
|
|
|
|
/// In-memory byte source (for tests and small buffers).
|
|
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`: do not lock out other processes.
|
|
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: read-only size query ioctls with pointers to valid stack variables.
|
|
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))
|
|
}
|
|
|
|
/// Opens a Windows raw device (`\\.\PhysicalDriveN`) read-only and queries its byte size.
|
|
#[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: valid output buffer matching the exact struct layout.
|
|
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)
|
|
}
|
|
|
|
/// Opens a path (raw disk, partition, or forensic/virtual disk image) as a read-only `Source`.
|
|
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)?));
|
|
}
|
|
}
|
|
let file = FileSource::open(path)?;
|
|
// Forensic containers (EWF, AFF4) and virtual disks (QCOW2, VMDK, VHD, VHDX, DMG) are opened transparently.
|
|
let mut head = [0u8; 96];
|
|
let got = file.read_at(0, &mut head).unwrap_or(0);
|
|
if got >= 4 && head.starts_with(b"PK") && crate::aff4::is_aff4(&head[..got]) {
|
|
let a = crate::aff4::Aff4Source::open(path)
|
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
|
return Ok(Arc::new(a));
|
|
}
|
|
if got >= 8 && crate::ewf::is_ewf_header(&head) {
|
|
let e = crate::ewf::EwfSource::open(path)
|
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
|
return Ok(Arc::new(e));
|
|
}
|
|
drop(file);
|
|
if let Some(v) = crate::vdisk::open(path)
|
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?
|
|
{
|
|
return Ok(Arc::new(v));
|
|
}
|
|
Ok(Arc::new(FileSource::open(path)?))
|
|
}
|
|
|
|
/// Content fingerprint of a source disk: SHA-256 of its byte size plus the first and last MiB
|
|
/// (partition tables, headers, and backup GPT). Used to recognize the same physical drive
|
|
/// if it is unplugged and reattached under a different device name (`/dev/sda` -> `/dev/sdb`).
|
|
pub fn fingerprint(src: &dyn Source) -> Option<String> {
|
|
use sha2::{Digest, Sha256};
|
|
const MIB: u64 = 1024 * 1024;
|
|
let size = src.size();
|
|
let mut h = Sha256::new();
|
|
h.update(size.to_le_bytes());
|
|
let mut feed = |off: u64, len: u64| -> bool {
|
|
let mut buf = vec![0u8; len as usize];
|
|
let mut got = 0;
|
|
while got < buf.len() {
|
|
match src.read_at(off + got as u64, &mut buf[got..]) {
|
|
Ok(0) => break,
|
|
Ok(n) => got += n,
|
|
Err(_) => return false,
|
|
}
|
|
}
|
|
h.update(&buf[..got]);
|
|
true
|
|
};
|
|
if !feed(0, size.min(MIB)) {
|
|
return None;
|
|
}
|
|
if size > 2 * MIB && !feed(size - MIB, MIB) {
|
|
return None;
|
|
}
|
|
Some(h.finalize().iter().map(|b| format!("{b:02x}")).collect())
|
|
}
|