Mescla a main (lzfse_rust e ed25519-compact lado a lado)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-23 08:51:40 -03:00
commit 27d4cab2e6
18 files changed
+469 -86

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ pbkdf2 = { version = "0.13", default-features = false, features = ["hmac"] }
ripemd = "0.2"
argon2 = "0.6"
ed25519-compact = { version = "2", default-features = false, features = ["random", "std"] }
lzfse_rust = "0.2.1"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
+4 -3
View File
@@ -1,6 +1,6 @@
//! DMG (Apple UDIF): rodapé `koly`, tabelas `mish` do plist e chunks zerados, brutos, ADC,
//! zlib e bzip2. Imagens cifradas (`encrcdsa`) e chunks LZFSE (`ULFO`) são recusados com
//! mensagem clara.
//! zlib, bzip2 e LZFSE (`ULFO`). Imagens cifradas (`encrcdsa`) são recusadas com mensagem
//! clara.
use super::{bad, be32, be64, read_exact, read_vec, Backend};
use crate::source::{FileSource, Source};
@@ -187,7 +187,8 @@ impl Dmg {
bzip2::read::BzDecoder::new(&raw[..]).read_to_end(&mut out)?;
}
0x8000_0007 => {
return Err(bad("chunk LZFSE (formato ULFO) ainda não é suportado"));
lzfse_rust::decode_bytes(&raw, &mut out)
.map_err(|e| bad(format!("chunk LZFSE inválido: {e}")))?;
}
t => return Err(bad(format!("tipo de chunk DMG 0x{t:08x} desconhecido"))),
}
+14 -8
View File
@@ -2,9 +2,9 @@
//!
//! 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) seguem a cadeia até o disco base; QCOW2 e VMDK com
//! arquivo de apoio são recusados com mensagem clara, porque ler só a camada filha
//! devolveria dados errados.
//! 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;
@@ -130,8 +130,8 @@ pub(crate) fn open_depth(path: &Path, depth: u32) -> Result<Option<VDisk>> {
};
let name = format!("{format} {}", path.display());
let inner: Box<dyn Backend> = match format {
"QCOW2" => Box::new(qcow2::Qcow2::open(file).map_err(|e| io_err(e, &name))?),
"VMDK" => Box::new(vmdk::Vmdk::open(file, path).map_err(|e| io_err(e, &name))?),
"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))?),
@@ -139,17 +139,23 @@ pub(crate) fn open_depth(path: &Path, depth: u32) -> Result<Option<VDisk>> {
Ok(Some(VDisk { inner, format }))
}
/// Abre o disco pai de um VHD/VHDX diferencial.
/// 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<Box<dyn Source>> {
for p in candidates {
if !p.is_file() {
continue;
}
if let Some(v) = open_depth(p, depth + 1).map_err(|e| bad(e.to_string()))? {
return Ok(Box::new(v));
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));
+75 -12
View File
@@ -1,10 +1,15 @@
//! QCOW2 (QEMU), versões 2 e 3: tabelas L1/L2, clusters comprimidos (deflate e zstd) e
//! clusters zerados. Imagens cifradas, com arquivo de apoio, com arquivo de dados externo ou
//! com L2 estendido são recusadas.
//! QCOW2 (QEMU), versões 2 e 3: tabelas L1/L2, clusters comprimidos (deflate e zstd),
//! clusters zerados e arquivo de apoio (backing file): o que o filho não alocou vem do pai.
//! Imagens cifradas, com arquivo de dados externo, com L2 estendido ou com apoio que não é um
//! arquivo local (`json:`, `nbd:`, URLs) são recusadas.
use super::{bad, be32, be64, check_table, read_exact, read_vec, Backend};
use super::{
bad, be32, be64, candidate_paths, check_table, open_parent, read_exact, read_parent, read_vec,
Backend,
};
use crate::source::{FileSource, Source};
use std::io::{self, Read};
use std::path::Path;
use std::sync::Mutex;
const OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00;
@@ -16,12 +21,13 @@ pub(super) struct Qcow2 {
cluster_bits: u32,
l1: Vec<u64>,
zstd: bool,
parent: Option<Box<dyn Source>>,
/// Último cluster comprimido decodificado: (deslocamento do cluster virtual, dados).
cache: Mutex<Option<(u64, Vec<u8>)>>,
}
impl Qcow2 {
pub(super) fn open(file: FileSource) -> io::Result<Self> {
pub(super) fn open(file: FileSource, path: &Path, depth: u32) -> io::Result<Self> {
let h = read_vec(&file, 0, 112.min(file.size() as usize))?;
if h.len() < 72 {
return Err(bad("cabeçalho QCOW2 truncado"));
@@ -30,11 +36,6 @@ impl Qcow2 {
if version != 2 && version != 3 {
return Err(bad(format!("versão QCOW {version} não suportada")));
}
if be64(&h, 8) != 0 {
return Err(bad(
"a imagem QCOW2 usa arquivo de apoio (backing file); abrir só a camada filha daria dados errados",
));
}
let cluster_bits = be32(&h, 20);
if !(9..=21).contains(&cluster_bits) {
return Err(bad(format!(
@@ -63,6 +64,11 @@ impl Qcow2 {
}
}
}
let parent = if be64(&h, 8) != 0 {
Some(Self::open_backing(&file, &h, version, path, depth)?)
} else {
None
};
let l1_size = be32(&h, 36) as u64;
let l1_off = be64(&h, 40);
let n = check_table(l1_size, 8, "tabela L1")?;
@@ -74,10 +80,65 @@ impl Qcow2 {
cluster_bits,
l1,
zstd,
parent,
cache: Mutex::new(None),
})
}
/// Abre o arquivo de apoio. O formato vem da extensão de cabeçalho `0xE2792ACA`; `raw`
/// (ou ausente em v2) é lido sem reconhecer formato, como o QEMU faz.
fn open_backing(
file: &FileSource,
h: &[u8],
version: u32,
path: &Path,
depth: u32,
) -> io::Result<Box<dyn Source>> {
let (off, len) = (be64(h, 8), be32(h, 16) as usize);
if len == 0 || len > 1023 {
return Err(bad("nome do arquivo de apoio QCOW2 inválido"));
}
let name = String::from_utf8_lossy(&read_vec(file, off, len)?).into_owned();
if name.starts_with("json:")
|| name.contains("://")
|| name.split(':').next().is_some_and(|p| {
matches!(p, "nbd" | "ssh" | "rbd" | "sheepdog" | "gluster" | "iscsi")
})
{
return Err(bad(format!(
"o arquivo de apoio {name} não é um arquivo local; só caminhos de arquivo são suportados"
)));
}
// extensões de cabeçalho: depois do cabeçalho, alinhadas a 8 bytes
let hlen = if version == 3 && h.len() >= 104 {
be32(h, 100) as u64
} else {
72
};
let mut raw = false;
let mut pos = hlen.div_ceil(8) * 8;
for _ in 0..64 {
let mut eh = [0u8; 8];
if read_exact(file, pos, &mut eh).is_err() {
break;
}
let (kind, elen) = (be32(&eh, 0), be32(&eh, 4) as u64);
if kind == 0 || elen > 1 << 20 {
break;
}
if kind == 0xE279_2ACA {
let fmt = read_vec(file, pos + 8, elen as usize)?;
raw = fmt == b"raw";
}
pos += 8 + elen.div_ceil(8) * 8;
}
if version == 2 {
raw = false;
}
let dir = path.parent().unwrap_or(Path::new("."));
open_parent(&candidate_paths(dir, &[name]), depth, raw)
}
fn cluster_size(&self) -> u64 {
1 << self.cluster_bits
}
@@ -139,8 +200,10 @@ impl Backend for Qcow2 {
return Ok(n);
}
let off = entry & OFFSET_MASK;
if off == 0 || entry & 1 != 0 {
buf[..n].fill(0);
if entry & 1 != 0 {
buf[..n].fill(0); // zerado de propósito: não consulta o pai
} else if off == 0 {
read_parent(&self.parent, voff, &mut buf[..n])?;
} else {
read_exact(&self.file, off + within, &mut buf[..n])?;
}
+1 -1
View File
@@ -112,7 +112,7 @@ impl Vhd {
}
}
names.push(utf16(&dh[64..64 + 512], true));
Some(open_parent(&candidate_paths(dir, &names), depth)?)
Some(open_parent(&candidate_paths(dir, &names), depth, false)?)
} else {
None
};
+1 -1
View File
@@ -130,7 +130,7 @@ impl Vhdx {
}
}
let dir = path.parent().unwrap_or(Path::new("."));
Some(open_parent(&candidate_paths(dir, &names), depth)?)
Some(open_parent(&candidate_paths(dir, &names), depth, false)?)
} else {
None
};
+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)
}
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+206 -12
View File
@@ -281,22 +281,82 @@ fn qcow2_with_zero_raw_and_compressed_clusters() {
}
#[test]
fn qcow2_with_backing_file_or_encryption_is_refused() {
fn qcow2_with_encryption_or_a_remote_backing_file_is_refused() {
let dir = tempfile::tempdir().unwrap();
let mut img = build_qcow2(&raw());
let p = dir.path().join("b.qcow2");
be(&mut img, 8, 4096, 8);
be(&mut img, 16, 3, 4);
std::fs::write(&p, &img).unwrap();
let e = open_path(&p).err().expect("deveria recusar").to_string();
assert!(e.contains("arquivo de apoio"), "{e}");
let mut img = build_qcow2(&raw());
be(&mut img, 32, 2, 4);
let p = dir.path().join("e.qcow2");
std::fs::write(&p, &img).unwrap();
let e = open_path(&p).err().expect("deveria recusar").to_string();
assert!(e.contains("LUKS"), "{e}");
let mut img = build_qcow2(&raw());
let name = b"nbd://servidor/disco";
img[512..512 + name.len()].copy_from_slice(name);
be(&mut img, 8, 512, 8);
be(&mut img, 16, name.len() as u64, 4);
let p = dir.path().join("r.qcow2");
std::fs::write(&p, &img).unwrap();
let e = open_path(&p).err().expect("deveria recusar").to_string();
assert!(e.contains("não é um arquivo local"), "{e}");
}
/// Filho QCOW2 que só aloca o cluster 1 (dados), marca o 2 como zerado e deixa o resto para o pai.
fn build_qcow2_child(backing: &str, fmt: Option<&str>, own: &[u8]) -> Vec<u8> {
const CS: usize = 1 << 16;
let mut img = vec![0u8; 3 * CS];
img[..4].copy_from_slice(b"QFI\xfb");
be(&mut img, 4, 3, 4);
be(&mut img, 16, backing.len() as u64, 4);
be(&mut img, 8, 1024, 8);
img[1024..1024 + backing.len()].copy_from_slice(backing.as_bytes());
be(&mut img, 20, 16, 4);
be(&mut img, 24, 16 * CS as u64, 8);
be(&mut img, 36, 1, 4);
be(&mut img, 40, CS as u64, 8);
be(&mut img, 100, 112, 4);
if let Some(f) = fmt {
// extensão 0xE2792ACA com o nome do formato do apoio, depois o fim das extensões
be(&mut img, 112, 0xE279_2ACA, 4);
be(&mut img, 116, f.len() as u64, 4);
img[120..120 + f.len()].copy_from_slice(f.as_bytes());
}
be(&mut img, CS, (2 * CS as u64) | (1 << 63), 8);
let host = img.len() as u64;
img.extend_from_slice(own);
img.resize(img.len().div_ceil(CS) * CS, 0);
be(&mut img, 2 * CS + 8, host | (1 << 63), 8); // cluster 1
be(&mut img, 2 * CS + 16, 1, 8); // cluster 2: zerado de propósito
img
}
#[test]
fn qcow2_backing_file_supplies_what_the_child_did_not_allocate() {
const CS: usize = 1 << 16;
let base = raw();
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("base.raw"), &base).unwrap();
let own = vec![0x42u8; CS];
let mut expect = base.clone();
expect[CS..2 * CS].copy_from_slice(&own);
expect[2 * CS..3 * CS].fill(0);
for (name, fmt) in [("a.qcow2", Some("raw")), ("b.qcow2", None)] {
let p = dir.path().join(name);
std::fs::write(&p, build_qcow2_child("base.raw", fmt, &own)).unwrap();
check_content(&p, &expect);
}
// com o pai em outro formato: o filho aponta para um QCOW2
std::fs::write(dir.path().join("base.qcow2"), build_qcow2(&base)).unwrap();
let p = dir.path().join("c.qcow2");
std::fs::write(&p, build_qcow2_child("base.qcow2", Some("qcow2"), &own)).unwrap();
check_content(&p, &expect);
// sem o pai, a abertura diz o que falta
std::fs::remove_file(dir.path().join("base.raw")).unwrap();
let e = open_path(&dir.path().join("a.qcow2"))
.err()
.unwrap()
.to_string();
assert!(e.contains("disco pai não encontrado"), "{e}");
}
// ---------- VMDK ----------
@@ -324,8 +384,114 @@ fn vmdk_descriptor_with_flat_and_zero_extents() {
check_content(&p, &expect);
}
/// VMDK esparso feito à mão: 16 grãos de 64 KiB; `grains[i]` = `None` (não alocado, vem do pai),
/// `Some(None)` (zerado de propósito) ou `Some(Some(dados))`.
fn build_vmdk_sparse(descriptor: &str, grains: &[Option<Option<Vec<u8>>>]) -> Vec<u8> {
const GRAIN: usize = 128; // setores
let mut img = vec![0u8; 26 * 512];
img[..4].copy_from_slice(b"KDMV");
le(&mut img, 4, 1, 4);
le(&mut img, 8, 3, 4);
le(&mut img, 12, (grains.len() * GRAIN) as u64, 8);
le(&mut img, 20, GRAIN as u64, 8);
le(&mut img, 28, 1, 8);
le(&mut img, 36, 20, 8);
le(&mut img, 44, 512, 4);
le(&mut img, 56, 21, 8);
le(&mut img, 64, 26, 8);
img[512..512 + descriptor.len()].copy_from_slice(descriptor.as_bytes());
le(&mut img, 21 * 512, 22, 4); // diretório de grãos -> tabela no setor 22
for (i, g) in grains.iter().enumerate() {
let entry = match g {
None => 0,
Some(None) => 1,
Some(Some(d)) => {
let sector = (img.len() / 512) as u64;
let mut d = d.clone();
d.resize(GRAIN * 512, 0);
img.extend_from_slice(&d);
sector
}
};
le(&mut img, 22 * 512 + i * 4, entry, 4);
}
img
}
#[test]
fn vmdk_with_a_parent_is_refused() {
fn vmdk_child_reads_the_parent_for_unallocated_grains() {
let base = raw();
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("base.vmdk"), build_vmdk_sparse(
"# Disk DescriptorFile\nversion=1\nCID=11111111\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n",
&(0..16)
.map(|i| Some(Some(base[i * 65536..(i + 1) * 65536].to_vec())))
.collect::<Vec<_>>(),
))
.unwrap();
let own = vec![0x37u8; 65536];
let mut grains: Vec<Option<Option<Vec<u8>>>> = vec![None; 16];
grains[3] = Some(Some(own.clone()));
grains[5] = Some(None); // zerado de propósito: não consulta o pai
let child = build_vmdk_sparse(
"# Disk DescriptorFile\nversion=1\nCID=22222222\nparentCID=11111111\ncreateType=\"monolithicSparse\"\nparentFileNameHint=\"base.vmdk\"\n",
&grains,
);
let p = dir.path().join("child.vmdk");
std::fs::write(&p, child).unwrap();
let mut expect = base.clone();
expect[3 * 65536..4 * 65536].copy_from_slice(&own);
expect[5 * 65536..6 * 65536].fill(0);
check_content(&p, &expect);
}
/// Snapshot real do VirtualBox: o filho só diz o UUID do pai (`ddb.uuid.parent`), sem nome.
#[test]
fn virtualbox_snapshot_vmdk_finds_its_parent_by_uuid() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("Snapshots");
std::fs::create_dir(&sub).unwrap();
// o pai fica na pasta de cima, como o VirtualBox organiza a máquina
std::fs::copy(fx("disk.vmdk"), dir.path().join("disco-base.vmdk")).unwrap();
std::fs::copy(fx("vbox-snapshot.vmdk"), sub.join("filho.vmdk")).unwrap();
check_content(&sub.join("filho.vmdk"), &raw());
// sem o pai por perto, a abertura explica
std::fs::remove_file(dir.path().join("disco-base.vmdk")).unwrap();
let e = open_path(&sub.join("filho.vmdk"))
.err()
.unwrap()
.to_string();
assert!(e.contains("identificado só por UUID"), "{e}");
}
#[test]
fn vmdk_child_is_refused_when_the_parent_changed() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("base.vmdk"),
build_vmdk_sparse(
"# Disk DescriptorFile\nversion=1\nCID=aaaaaaaa\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n",
&vec![None; 16],
),
)
.unwrap();
std::fs::write(
dir.path().join("filho.vmdk"),
build_vmdk_sparse(
"# Disk DescriptorFile\nversion=1\nCID=bbbbbbbb\nparentCID=11111111\ncreateType=\"monolithicSparse\"\nparentFileNameHint=\"base.vmdk\"\n",
&vec![None; 16],
),
)
.unwrap();
let e = open_path(&dir.path().join("filho.vmdk"))
.err()
.unwrap()
.to_string();
assert!(e.contains("foi alterado depois do filho"), "{e}");
}
#[test]
fn vmdk_with_a_parent_but_no_name_is_refused() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("c.vmdk");
std::fs::write(
@@ -334,7 +500,7 @@ fn vmdk_with_a_parent_is_refused() {
)
.unwrap();
let e = open_path(&p).err().expect("deveria recusar").to_string();
assert!(e.contains("disco pai"), "{e}");
assert!(e.contains("não diz qual"), "{e}");
}
// ---------- VHD ----------
@@ -619,8 +785,36 @@ fn dmg_with_every_chunk_type() {
check_content(&p, &expect);
}
/// Chunks `ULFO` com dados compactados pelo compressor de referência da Apple (`lzfse`):
/// um bloco `bvx2` (FSE, 64 KiB) e um pequeno (`bvxn`/LZVN).
#[test]
fn dmg_lzfse_and_encrypted_are_refused_with_a_reason() {
fn dmg_lzfse_chunks_from_the_reference_compressor() {
let big = std::fs::read(fx("lzfse-64k.bin")).unwrap();
let small = std::fs::read(fx("lzfse-small.bin")).unwrap();
let mut small_padded = small.clone();
small_padded.resize(small.len().div_ceil(512) * 512, 0);
let chunks = [
DmgChunk {
ty: 0x8000_0007,
sectors: (big.len() / 512) as u64,
payload: std::fs::read(fx("lzfse-64k.bin.lzfse")).unwrap(),
},
DmgChunk {
ty: 0x8000_0007,
sectors: (small_padded.len() / 512) as u64,
payload: std::fs::read(fx("lzfse-small.bin.lzfse")).unwrap(),
},
];
let mut expect = big.clone();
expect.extend_from_slice(&small_padded);
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("l.dmg");
std::fs::write(&p, build_dmg(&chunks, expect.len() as u64 / 512)).unwrap();
check_content(&p, &expect);
}
#[test]
fn dmg_with_a_corrupt_lzfse_chunk_and_encrypted_are_refused_with_a_reason() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("l.dmg");
let chunks = [DmgChunk {