//! Imagens de disco virtual e contêineres: QCOW2, VMDK, VHD, VHDX e DMG (UDIF). //! //! Cada formato é aberto como o disco que contém, somente para leitura, e entra na cadeia //! normal (partições, sistemas de arquivos, cifras) por meio de [`Source`]. Discos //! diferenciais (VHD e VHDX com pai, QCOW2 com arquivo de apoio, VMDK com pai) seguem a //! cadeia até o disco base. A identidade do pai (UUID, CID, data) não é conferida: se o pai foi //! alterado depois do filho, os dados lidos refletem o pai atual. 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()) } /// Leitura dentro de um único bloco do formato; devolve quantos bytes entregou (pode ser /// menos que `buf.len()`, nunca zero antes do fim do disco). pub(crate) trait Backend: Send + Sync { fn size(&self) -> u64; fn read_chunk(&self, voff: u64, buf: &mut [u8]) -> io::Result; } /// Disco virtual aberto de uma imagem de contêiner. pub struct VDisk { inner: Box, format: &'static str, } impl VDisk { /// Nome do formato (`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 { 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) -> 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}")) } /// Cadeias de arquivos pai (VHD/VHDX diferenciais) não passam deste limite, o que também /// corta ciclos. const MAX_DEPTH: u32 = 16; /// Formato de contêiner reconhecido pelos primeiros bytes (e, para VHD fixo e DMG, pelo /// fim do arquivo). `None` quando é um disco bruto. 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 } /// Abre `path` como disco virtual, ou `Ok(None)` se não for um contêiner conhecido. pub fn open(path: &Path) -> Result> { open_depth(path, 0) } pub(crate) fn open_depth(path: &Path, depth: u32) -> Result> { 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 = 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 })) } /// Abre o disco pai (VHD/VHDX diferencial, QCOW2 com apoio, VMDK com pai). Com `raw`, o /// arquivo é tratado como disco bruto sem tentar reconhecer o formato. pub(crate) fn open_parent( candidates: &[std::path::PathBuf], depth: u32, raw: bool, ) -> io::Result> { 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::>() .join(", ") ))) } /// Lê de um arquivo por posição e exige todos os bytes. 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> { let mut v = vec![0u8; len]; read_exact(f, off, &mut v)?; Ok(v) } /// Tabelas em memória têm teto: um cabeçalho adulterado não pode pedir gigabytes. pub(crate) fn check_table(entries: u64, entry_size: u64, what: &str) -> io::Result { 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) } /// UTF-16 (big ou little endian) até o primeiro nulo. pub(crate) fn utf16(b: &[u8], big: bool) -> String { let units: Vec = 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) } /// Caminhos onde procurar o disco pai: o caminho gravado (relativo ao arquivo filho, com `\` /// virando `/`) e, como alternativa, só o nome do arquivo na mesma pasta. pub(crate) fn candidate_paths(dir: &Path, names: &[String]) -> Vec { let mut out: Vec = 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 } /// Lê do disco pai (ou zeros, sem pai); o que o pai não cobre também é zero. pub(crate) fn read_parent( parent: &Option>, 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(()) }