Discos virtuais: arquivo de apoio do QCOW2, snapshots de VMDK e DMG com LZFSE

QCOW2 e VMDK filhos leem o pai no que não alocaram (o QCOW2 respeita o formato do apoio;
o VMDK acha o pai por nome ou, no VirtualBox, por UUID, e recusa o filho se o CID do pai
mudou). Chunks LZFSE do DMG usam lzfse_rust, conferidos com dados do compressor de
referência da Apple. Snapshot real do VirtualBox entra como fixture.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-23 07:55:58 -03:00
1 parent 3a6b0c7b74
commit 4bec3e971a
18 files changed
+469 -86

No files matched your search

+148 -39
View File
@@ -1,9 +1,12 @@
//! VMDK (VMware, VirtualBox): extensões esparsas (inclusive `streamOptimized` com grãos
//! comprimidos) e descritores de texto com extensões `FLAT`, `SPARSE` e `ZERO`, como
//! `monolithicFlat` e `twoGbMaxExtentSparse`. Discos com pai (snapshots) e o formato SEsparse
//! são recusados.
//! `monolithicFlat` e `twoGbMaxExtentSparse`. Discos com pai (snapshots) leem o pai nos grãos
//! que o filho não alocou; o formato SEsparse é recusado.
use super::{bad, check_table, le16, le32, le64, read_exact, read_vec, Backend};
use super::{
bad, candidate_paths, check_table, le16, le32, le64, open_parent, read_exact, read_parent,
read_vec, Backend,
};
use crate::source::{FileSource, Source};
use std::io::{self, Read};
use std::path::Path;
@@ -36,20 +39,73 @@ struct Extent {
pub(super) struct Vmdk {
extents: Vec<Extent>,
size: u64,
parent: Option<Box<dyn Source>>,
}
fn parent_in(descriptor: &str) -> bool {
descriptor.lines().any(|l| {
let l = l.trim();
l.strip_prefix("parentCID")
/// Valor de uma chave `chave=valor` do descritor, sem aspas.
fn desc_value(descriptor: &str, key: &str) -> Option<String> {
descriptor.lines().find_map(|l| {
l.trim()
.strip_prefix(key)
.and_then(|r| r.trim_start().strip_prefix('='))
.is_some_and(|v| {
let v = v.trim().trim_matches('"');
!v.is_empty() && !v.eq_ignore_ascii_case("ffffffff")
})
.map(|v| v.trim().trim_matches('"').to_string())
})
}
/// Descritor (texto) de um arquivo VMDK: o próprio arquivo, se for descritor, ou o embutido
/// logo depois do cabeçalho de uma extensão esparsa.
fn descriptor_of(path: &Path) -> Option<String> {
let f = FileSource::open(path).ok()?;
let head = read_vec(&f, 0, (f.size() as usize).min(24 * 512)).ok()?;
if head.starts_with(b"KDMV") {
let off = (le64(&head, 28) * SECTOR) as usize;
let len = (le64(&head, 36) * SECTOR) as usize;
let d = head.get(off..(off + len).min(head.len()))?;
let end = d.iter().position(|&b| b == 0).unwrap_or(d.len());
return Some(String::from_utf8_lossy(&d[..end]).into_owned());
}
head.starts_with(b"# Disk Desc")
.then(|| String::from_utf8_lossy(&head).into_owned())
}
/// Como o descritor identifica o pai: pelo nome (`parentFileNameHint`) e/ou, no VirtualBox, só
/// pelo UUID (`ddb.uuid.parent`); `cid` é o `parentCID` que o pai precisa ter.
struct ParentRef {
name: Option<String>,
uuid: Option<String>,
cid: Option<String>,
}
fn parent_ref(descriptor: &str) -> Option<ParentRef> {
let cid = desc_value(descriptor, "parentCID")
.filter(|c| !c.is_empty() && !c.eq_ignore_ascii_case("ffffffff"));
let uuid = desc_value(descriptor, "ddb.uuid.parent")
.filter(|u| !u.is_empty() && u.chars().any(|c| c != '0' && c != '-'));
let name = desc_value(descriptor, "parentFileNameHint").filter(|n| !n.is_empty());
(cid.is_some() || uuid.is_some()).then_some(ParentRef { name, uuid, cid })
}
/// Procura, na pasta do filho e nas duas acima, um VMDK cujo `ddb.uuid.image` seja `uuid`.
fn find_by_uuid(dir: &Path, uuid: &str) -> Option<std::path::PathBuf> {
let mut d = Some(dir);
for _ in 0..3 {
let cur = d?;
for e in std::fs::read_dir(cur).ok()?.flatten() {
let p = e.path();
if p.extension()
.is_some_and(|x| x.eq_ignore_ascii_case("vmdk"))
&& descriptor_of(&p)
.and_then(|ds| desc_value(&ds, "ddb.uuid.image"))
.is_some_and(|u| u.eq_ignore_ascii_case(uuid))
{
return Some(p);
}
}
d = cur.parent();
}
None
}
impl Sparse {
fn open(file: FileSource) -> io::Result<(Self, u64, Option<String>)> {
let mut h = read_vec(&file, 0, SECTOR as usize)?;
@@ -95,11 +151,6 @@ impl Sparse {
} else {
None
};
if descriptor.as_deref().is_some_and(parent_in) {
return Err(bad(
"VMDK com disco pai (snapshot); abrir só a camada filha daria dados errados",
));
}
let gde = capacity.div_ceil(gtes * grain);
let n = check_table(gde, 4, "diretório de grãos")?;
let raw = read_vec(&file, gd_off * SECTOR, n * 4)?;
@@ -118,7 +169,9 @@ impl Sparse {
))
}
fn read(&self, off: u64, buf: &mut [u8]) -> io::Result<usize> {
/// Lê dentro de um grão. O `bool` é `false` quando o grão não está alocado neste arquivo
/// (o chamador completa com o pai); grão zerado de propósito devolve zeros.
fn read(&self, off: u64, buf: &mut [u8]) -> io::Result<(usize, bool)> {
let g = off / self.grain;
let within = off % self.grain;
let n = buf.len().min((self.grain - within) as usize);
@@ -129,13 +182,16 @@ impl Sparse {
read_exact(&self.file, gt_sector * SECTOR + (g % self.gtes) * 4, &mut b)?;
sector = u32::from_le_bytes(b) as u64;
}
if sector <= 1 {
if sector == 0 {
return Ok((n, false));
}
if sector == 1 {
buf[..n].fill(0);
return Ok(n);
return Ok((n, true));
}
if !self.compressed {
read_exact(&self.file, sector * SECTOR + within, &mut buf[..n])?;
return Ok(n);
return Ok((n, true));
}
let mut cache = self.cache.lock().unwrap();
if cache.as_ref().map(|c| c.0) != Some(g) {
@@ -153,17 +209,12 @@ impl Sparse {
}
let data = &cache.as_ref().unwrap().1;
buf[..n].copy_from_slice(&data[within as usize..within as usize + n]);
Ok(n)
Ok((n, true))
}
}
/// Extensões de um descritor de texto: `RW 2048 FLAT "disco-f001.vmdk" 0`.
fn parse_descriptor(text: &str, dir: &Path) -> io::Result<Vec<Extent>> {
if parent_in(text) {
return Err(bad(
"VMDK com disco pai (snapshot); abrir só a camada filha daria dados errados",
));
}
let mut out = vec![];
let mut start = 0u64;
for line in text.lines() {
@@ -216,27 +267,79 @@ fn parse_descriptor(text: &str, dir: &Path) -> io::Result<Vec<Extent>> {
}
impl Vmdk {
pub(super) fn open(file: FileSource, path: &Path) -> io::Result<Self> {
fn open_parent_of(r: &ParentRef, dir: &Path, depth: u32) -> io::Result<Box<dyn Source>> {
let mut found = r
.name
.as_ref()
.map(|n| candidate_paths(dir, std::slice::from_ref(n)))
.unwrap_or_default();
let by_name = found.iter().any(|p| p.is_file());
if !by_name {
if let Some(p) = r.uuid.as_deref().and_then(|u| find_by_uuid(dir, u)) {
found = vec![p];
}
}
if r.name.is_none() && r.uuid.is_none() {
return Err(bad(
"VMDK com pai, mas o descritor não diz qual (sem parentFileNameHint nem ddb.uuid.parent)",
));
}
if r.name.is_none() && found.is_empty() {
return Err(bad(
"VMDK com pai identificado só por UUID (VirtualBox) e o pai não está na pasta do filho nem nas duas acima",
));
}
if let Some(cid) = &r.cid {
// o pai precisa ser o mesmo de quando o filho foi criado
if let Some(p) = found.iter().find(|p| p.is_file()) {
let have = descriptor_of(p).and_then(|d| desc_value(&d, "CID"));
if have
.as_deref()
.is_some_and(|h| !h.eq_ignore_ascii_case(cid))
{
return Err(bad(format!(
"o pai {} foi alterado depois do filho (CID {} em vez de {cid}); os dados do filho não valem mais",
p.display(),
have.unwrap_or_default()
)));
}
}
}
open_parent(&found, depth, false)
}
pub(super) fn open(file: FileSource, path: &Path, depth: u32) -> io::Result<Self> {
let mut head = [0u8; 4];
let _ = file.read_at(0, &mut head);
let extents = if &head == b"KDMV" {
let (sp, size, _) = Sparse::open(file)?;
vec![Extent {
start: 0,
len: size,
kind: Kind::Sparse(sp),
}]
let dir = path.parent().unwrap_or(Path::new("."));
let (extents, descriptor) = if &head == b"KDMV" {
let (sp, size, descriptor) = Sparse::open(file)?;
(
vec![Extent {
start: 0,
len: size,
kind: Kind::Sparse(sp),
}],
descriptor,
)
} else {
if file.size() > 1 << 20 {
return Err(bad("descritor VMDK grande demais"));
}
let raw = read_vec(&file, 0, file.size() as usize)?;
let text = String::from_utf8_lossy(&raw);
let dir = path.parent().unwrap_or(Path::new("."));
parse_descriptor(&text, dir)?
let text = String::from_utf8_lossy(&raw).into_owned();
(parse_descriptor(&text, dir)?, Some(text))
};
let parent = match descriptor.as_deref().and_then(parent_ref) {
Some(r) => Some(Self::open_parent_of(&r, dir, depth)?),
None => None,
};
let size = extents.iter().map(|e| e.len).sum();
Ok(Self { extents, size })
Ok(Self {
extents,
size,
parent,
})
}
}
@@ -261,7 +364,13 @@ impl Backend for Vmdk {
read_exact(file, offset + rel, &mut buf[..n])?;
Ok(n)
}
Kind::Sparse(s) => s.read(rel, &mut buf[..n]),
Kind::Sparse(s) => {
let (got, allocated) = s.read(rel, &mut buf[..n])?;
if !allocated {
read_parent(&self.parent, voff, &mut buf[..got])?;
}
Ok(got)
}
}
}
}