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

252 lines
8.0 KiB
Rust

//! Virtual disk images and containers: QCOW2, VMDK, VHD, VHDX, and DMG (UDIF).
//!
//! Each format is opened read-only as a logical block device and plugs transparently into
//! the standard pipeline (partitions, filesystems, encryption) via [`Source`]. Differencing
//! disks (VHD/VHDX parent locators, QCOW2 backing files, VMDK parent chains) resolve reads
//! up the backing chain to the base image.
//!
mod dmg;
mod qcow2;
mod vhd;
mod vhdx;
mod vmdk;
use crate::error::{Error, Result};
use crate::source::{FileSource, Source};
use std::io;
use std::path::Path;
pub(crate) fn be16(b: &[u8], o: usize) -> u16 {
u16::from_be_bytes(b[o..o + 2].try_into().unwrap())
}
pub(crate) fn be32(b: &[u8], o: usize) -> u32 {
u32::from_be_bytes(b[o..o + 4].try_into().unwrap())
}
pub(crate) fn be64(b: &[u8], o: usize) -> u64 {
u64::from_be_bytes(b[o..o + 8].try_into().unwrap())
}
pub(crate) fn le16(b: &[u8], o: usize) -> u16 {
u16::from_le_bytes(b[o..o + 2].try_into().unwrap())
}
pub(crate) fn le32(b: &[u8], o: usize) -> u32 {
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
}
pub(crate) fn le64(b: &[u8], o: usize) -> u64 {
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
}
/// Reads within a single format block/cluster; returns the number of bytes filled (may be
/// less than `buf.len()`, but never `0` before the logical end of disk).
pub(crate) trait Backend: Send + Sync {
fn size(&self) -> u64;
fn read_chunk(&self, voff: u64, buf: &mut [u8]) -> io::Result<usize>;
}
/// Opened virtual disk backed by a container image.
pub struct VDisk {
inner: Box<dyn Backend>,
format: &'static str,
}
impl VDisk {
/// Format label (`"QCOW2"`, `"VMDK"`, `"VHD"`, `"VHDX"`, `"DMG"`).
pub fn format(&self) -> &'static str {
self.format
}
}
impl Source for VDisk {
fn size(&self) -> u64 {
self.inner.size()
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
let size = self.inner.size();
if offset >= size {
return Ok(0);
}
let want = buf.len().min((size - offset) as usize);
let mut done = 0;
while done < want {
let n = self
.inner
.read_chunk(offset + done as u64, &mut buf[done..want])?;
if n == 0 {
break;
}
done += n;
}
Ok(done)
}
}
pub(crate) fn bad(msg: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg.into())
}
pub(crate) fn io_err(e: io::Error, what: &str) -> Error {
Error::msg(format!("{what}: {e}"))
}
/// Maximum parent/backing chain depth (prevents infinite loops on cyclic parent references).
///
const MAX_DEPTH: u32 = 16;
/// Identifies a virtual disk container format from its leading bytes (and trailing footer
/// for fixed VHD and DMG). Returns `None` for raw disk images.
pub fn detect(file: &FileSource, head: &[u8]) -> Option<&'static str> {
if head.starts_with(b"QFI\xfb") {
return Some("QCOW2");
}
if head.starts_with(b"KDMV") || head.starts_with(b"# Disk DescriptorFile") {
return Some("VMDK");
}
if head.starts_with(b"vhdxfile") {
return Some("VHDX");
}
if head.starts_with(b"conectix") || vhd::is_fixed(file) {
return Some("VHD");
}
if dmg::is_encrypted(head) || dmg::is_dmg(file) {
return Some("DMG");
}
None
}
/// Opens `path` as a virtual disk, or returns `Ok(None)` if it is not a recognized container.
pub fn open(path: &Path) -> Result<Option<VDisk>> {
open_depth(path, 0)
}
pub(crate) fn open_depth(path: &Path, depth: u32) -> Result<Option<VDisk>> {
if depth > MAX_DEPTH {
return Err(Error::msg("cadeia de discos pai longa demais ou circular"));
}
let file = FileSource::open(path)?;
let mut head = [0u8; 96];
let got = file.read_at(0, &mut head).unwrap_or(0);
let Some(format) = detect(&file, &head[..got]) else {
return Ok(None);
};
let name = format!("{format} {}", path.display());
let inner: Box<dyn Backend> = match format {
"QCOW2" => Box::new(qcow2::Qcow2::open(file, path, depth).map_err(|e| io_err(e, &name))?),
"VMDK" => Box::new(vmdk::Vmdk::open(file, path, depth).map_err(|e| io_err(e, &name))?),
"VHD" => Box::new(vhd::Vhd::open(file, path, depth).map_err(|e| io_err(e, &name))?),
"VHDX" => Box::new(vhdx::Vhdx::open(file, path, depth).map_err(|e| io_err(e, &name))?),
_ => Box::new(dmg::Dmg::open(file).map_err(|e| io_err(e, &name))?),
};
Ok(Some(VDisk { inner, format }))
}
/// Opens a parent/backing image at `depth`. When `raw` is `true`, treats the backing file
/// directly as a raw image without probing for a container header.
pub(crate) fn open_parent(
candidates: &[std::path::PathBuf],
depth: u32,
raw: bool,
) -> io::Result<Box<dyn Source>> {
for p in candidates {
if !p.is_file() {
continue;
}
if !raw {
if let Some(v) = open_depth(p, depth + 1).map_err(|e| bad(e.to_string()))? {
return Ok(Box::new(v));
}
} else if depth > MAX_DEPTH {
return Err(bad("cadeia de discos pai longa demais ou circular"));
}
let f = FileSource::open(p)?;
return Ok(Box::new(f));
}
Err(bad(format!(
"disco pai não encontrado (procurado: {})",
candidates
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
)))
}
/// Positional read helper that fills all of `buf` or returns `UnexpectedEof`.
pub(crate) fn read_exact(f: &FileSource, off: u64, buf: &mut [u8]) -> io::Result<()> {
crate::source::read_exact_at(f, off, buf)
}
pub(crate) fn read_vec(f: &FileSource, off: u64, len: usize) -> io::Result<Vec<u8>> {
let mut v = vec![0u8; len];
read_exact(f, off, &mut v)?;
Ok(v)
}
/// Upper bound on in-memory allocation tables so a corrupted header cannot trigger multi-GB allocations.
pub(crate) fn check_table(entries: u64, entry_size: u64, what: &str) -> io::Result<usize> {
const MAX: u64 = 512 * 1024 * 1024;
if entries.saturating_mul(entry_size) > MAX {
return Err(bad(format!("{what} grande demais ({entries} entradas)")));
}
Ok(entries as usize)
}
/// Decodes UTF-16 (BE or LE) up to the first NUL code unit.
pub(crate) fn utf16(b: &[u8], big: bool) -> String {
let units: Vec<u16> = b
.chunks_exact(2)
.map(|c| if big { be16(c, 0) } else { le16(c, 0) })
.take_while(|&u| u != 0)
.collect();
String::from_utf16_lossy(&units)
}
/// Candidate paths for locating a parent/backing disk: relative to the child image's directory
/// (normalizing `\` to `/`), falling back to the basename in the same directory.
pub(crate) fn candidate_paths(dir: &Path, names: &[String]) -> Vec<std::path::PathBuf> {
let mut out: Vec<std::path::PathBuf> = vec![];
for n in names {
let n = n.trim().replace('\\', "/");
let n = n.strip_prefix("file:///").unwrap_or(&n).to_string();
if n.is_empty() {
continue;
}
let rel = n.trim_start_matches("./");
let full = std::path::PathBuf::from(rel);
for p in [
if full.is_absolute() {
full.clone()
} else {
dir.join(rel)
},
dir.join(rel.rsplit('/').next().unwrap_or(rel)),
] {
if !out.contains(&p) {
out.push(p);
}
}
}
out
}
/// Reads from the parent disk (or fills zeros if there is no parent or `off` is past parent EOF).
pub(crate) fn read_parent(
parent: &Option<Box<dyn Source>>,
voff: u64,
buf: &mut [u8],
) -> io::Result<()> {
let Some(p) = parent else {
buf.fill(0);
return Ok(());
};
let mut done = 0;
while done < buf.len() {
let n = p.read_at(voff + done as u64, &mut buf[done..])?;
if n == 0 {
buf[done..].fill(0);
break;
}
done += n;
}
Ok(())
}