Discos virtuais: leitura de QCOW2, VMDK, VHD, VHDX e DMG
Cada contêiner é aberto como o disco que contém, somente leitura, pela mesma open_path das imagens E01 e AFF4. VHD e VHDX diferenciais seguem a cadeia de pais. QCOW2 com arquivo de apoio, VMDK com pai, DMG cifrado e LZFSE são recusados com o motivo. Conferido por hash contra o 7-Zip em imagens reais do dfvfs e do VirtualBox. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c1c4f442d7
commit
213a5a3df5
22 files changed
+2023
-4
No files matched your search
@@ -47,6 +47,7 @@ pub mod source;
|
||||
pub mod sqlite;
|
||||
pub mod thumb;
|
||||
pub mod types;
|
||||
pub mod vdisk;
|
||||
pub mod volume;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
@@ -286,7 +286,13 @@ pub fn open_path(path: &Path) -> io::Result<Arc<dyn Source>> {
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
||||
return Ok(Arc::new(e));
|
||||
}
|
||||
Ok(Arc::new(file))
|
||||
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)?))
|
||||
}
|
||||
|
||||
/// Identidade do conteúdo de um disco: SHA-256 do tamanho + primeiro e último MiB
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
//! 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.
|
||||
|
||||
use super::{bad, be32, be64, read_exact, read_vec, Backend};
|
||||
use crate::source::{FileSource, Source};
|
||||
use std::io::{self, Read};
|
||||
use std::sync::Mutex;
|
||||
|
||||
const SECTOR: u64 = 512;
|
||||
|
||||
pub(super) fn is_dmg(file: &FileSource) -> bool {
|
||||
let size = file.size();
|
||||
if size < 1024 {
|
||||
return false;
|
||||
}
|
||||
let mut t = [0u8; 512];
|
||||
read_exact(file, size - SECTOR, &mut t).is_ok()
|
||||
&& &t[..4] == b"koly"
|
||||
&& be32(&t, 4) == 4
|
||||
&& be32(&t, 8) == 512
|
||||
}
|
||||
|
||||
/// Imagem cifrada pelo `hdiutil`: começa com `encrcdsa` ou `cdsaencr`.
|
||||
pub(super) fn is_encrypted(head: &[u8]) -> bool {
|
||||
head.starts_with(b"encrcdsa") || head.starts_with(b"cdsaencr")
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Chunk {
|
||||
start: u64,
|
||||
len: u64,
|
||||
ty: u32,
|
||||
off: u64,
|
||||
comp_len: u64,
|
||||
}
|
||||
|
||||
pub(super) struct Dmg {
|
||||
file: FileSource,
|
||||
size: u64,
|
||||
chunks: Vec<Chunk>,
|
||||
cache: Mutex<Option<(usize, Vec<u8>)>>,
|
||||
}
|
||||
|
||||
fn base64(s: &str) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(s.len() * 3 / 4);
|
||||
let (mut acc, mut bits) = (0u32, 0);
|
||||
for c in s.bytes() {
|
||||
let v = match c {
|
||||
b'A'..=b'Z' => c - b'A',
|
||||
b'a'..=b'z' => c - b'a' + 26,
|
||||
b'0'..=b'9' => c - b'0' + 52,
|
||||
b'+' => 62,
|
||||
b'/' => 63,
|
||||
_ => continue,
|
||||
};
|
||||
acc = acc << 6 | v as u32;
|
||||
bits += 6;
|
||||
if bits >= 8 {
|
||||
bits -= 8;
|
||||
out.push((acc >> bits) as u8);
|
||||
acc &= (1 << bits) - 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Descompacta ADC (Apple Data Compression).
|
||||
fn adc(src: &[u8], max: usize) -> io::Result<Vec<u8>> {
|
||||
let mut out: Vec<u8> = Vec::with_capacity(max);
|
||||
let mut i = 0;
|
||||
while i < src.len() && out.len() < max {
|
||||
let c = src[i];
|
||||
i += 1;
|
||||
if c & 0x80 != 0 {
|
||||
let n = (c & 0x7F) as usize + 1;
|
||||
let lit = src.get(i..i + n).ok_or_else(|| bad("chunk ADC truncado"))?;
|
||||
out.extend_from_slice(lit);
|
||||
i += n;
|
||||
} else {
|
||||
let (len, off) = if c & 0x40 != 0 {
|
||||
let b = src.get(i..i + 2).ok_or_else(|| bad("chunk ADC truncado"))?;
|
||||
i += 2;
|
||||
(
|
||||
(c & 0x3F) as usize + 4,
|
||||
(b[0] as usize) << 8 | b[1] as usize,
|
||||
)
|
||||
} else {
|
||||
let b = *src.get(i).ok_or_else(|| bad("chunk ADC truncado"))?;
|
||||
i += 1;
|
||||
((c >> 2) as usize + 3, ((c & 3) as usize) << 8 | b as usize)
|
||||
};
|
||||
let off = off + 1;
|
||||
if off > out.len() {
|
||||
return Err(bad("referência ADC inválida"));
|
||||
}
|
||||
for _ in 0..len {
|
||||
out.push(out[out.len() - off]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
impl Dmg {
|
||||
pub(super) fn open(file: FileSource) -> io::Result<Self> {
|
||||
let mut head = [0u8; 8];
|
||||
let _ = file.read_at(0, &mut head);
|
||||
if is_encrypted(&head) {
|
||||
return Err(bad(
|
||||
"DMG cifrado (encrcdsa) não é suportado; desproteja a imagem antes",
|
||||
));
|
||||
}
|
||||
let size = file.size();
|
||||
let t = read_vec(&file, size - SECTOR, SECTOR as usize)?;
|
||||
let fork = be64(&t, 24);
|
||||
let xml_off = be64(&t, 216);
|
||||
let xml_len = be64(&t, 224);
|
||||
let sectors = be64(&t, 492);
|
||||
if xml_len == 0 || xml_len > 256 << 20 {
|
||||
return Err(bad("plist do DMG ausente ou grande demais"));
|
||||
}
|
||||
let xml = read_vec(&file, xml_off, xml_len as usize)?;
|
||||
let xml = String::from_utf8_lossy(&xml);
|
||||
let from = xml
|
||||
.find("<key>blkx</key>")
|
||||
.ok_or_else(|| bad("tabela blkx não encontrada no DMG"))?;
|
||||
let rest = &xml[from..];
|
||||
let rest = &rest[..rest.find("</array>").unwrap_or(rest.len())];
|
||||
let mut chunks: Vec<Chunk> = vec![];
|
||||
let mut cur = rest;
|
||||
while let Some(a) = cur.find("<data>") {
|
||||
let after = &cur[a + 6..];
|
||||
let Some(b) = after.find("</data>") else {
|
||||
break;
|
||||
};
|
||||
let mish = base64(&after[..b]);
|
||||
cur = &after[b + 7..];
|
||||
if mish.len() < 204 || &mish[..4] != b"mish" {
|
||||
continue;
|
||||
}
|
||||
let first = be64(&mish, 8);
|
||||
let data_off = be64(&mish, 24);
|
||||
let n = be32(&mish, 200) as usize;
|
||||
for i in 0..n {
|
||||
let Some(c) = mish.get(204 + i * 40..204 + (i + 1) * 40) else {
|
||||
break;
|
||||
};
|
||||
let ty = be32(c, 0);
|
||||
if ty == 0x7FFF_FFFE || ty == 0xFFFF_FFFF {
|
||||
continue;
|
||||
}
|
||||
chunks.push(Chunk {
|
||||
start: (first + be64(c, 8)) * SECTOR,
|
||||
len: be64(c, 16) * SECTOR,
|
||||
ty,
|
||||
off: fork + data_off + be64(c, 24),
|
||||
comp_len: be64(c, 32),
|
||||
});
|
||||
}
|
||||
}
|
||||
if chunks.is_empty() {
|
||||
return Err(bad("DMG sem chunks"));
|
||||
}
|
||||
chunks.sort_by_key(|c| c.start);
|
||||
let end = chunks.iter().map(|c| c.start + c.len).max().unwrap_or(0);
|
||||
Ok(Self {
|
||||
file,
|
||||
size: end.max(sectors * SECTOR),
|
||||
chunks,
|
||||
cache: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
fn decode(&self, c: &Chunk) -> io::Result<Vec<u8>> {
|
||||
if c.len > 64 << 20 || c.comp_len > 64 << 20 {
|
||||
return Err(bad("chunk de DMG grande demais"));
|
||||
}
|
||||
let raw = read_vec(&self.file, c.off, c.comp_len as usize)?;
|
||||
let mut out = Vec::with_capacity(c.len as usize);
|
||||
match c.ty {
|
||||
0x8000_0004 => out = adc(&raw, c.len as usize)?,
|
||||
0x8000_0005 => {
|
||||
flate2::read::ZlibDecoder::new(&raw[..]).read_to_end(&mut out)?;
|
||||
}
|
||||
0x8000_0006 => {
|
||||
bzip2::read::BzDecoder::new(&raw[..]).read_to_end(&mut out)?;
|
||||
}
|
||||
0x8000_0007 => {
|
||||
return Err(bad("chunk LZFSE (formato ULFO) ainda não é suportado"));
|
||||
}
|
||||
t => return Err(bad(format!("tipo de chunk DMG 0x{t:08x} desconhecido"))),
|
||||
}
|
||||
out.resize(c.len as usize, 0);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for Dmg {
|
||||
fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn read_chunk(&self, voff: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let i = self.chunks.partition_point(|c| c.start + c.len <= voff);
|
||||
let Some(c) = self.chunks.get(i).copied() else {
|
||||
let n = buf.len().min((self.size - voff) as usize);
|
||||
buf[..n].fill(0);
|
||||
return Ok(n);
|
||||
};
|
||||
if voff < c.start {
|
||||
// espaço que nenhuma tabela cobre
|
||||
let n = buf.len().min((c.start - voff) as usize);
|
||||
buf[..n].fill(0);
|
||||
return Ok(n);
|
||||
}
|
||||
let rel = voff - c.start;
|
||||
let n = buf.len().min((c.len - rel) as usize);
|
||||
match c.ty {
|
||||
0 | 2 => buf[..n].fill(0),
|
||||
1 => read_exact(&self.file, c.off + rel, &mut buf[..n])?,
|
||||
_ => {
|
||||
let mut cache = self.cache.lock().unwrap();
|
||||
if cache.as_ref().map(|x| x.0) != Some(i) {
|
||||
*cache = Some((i, self.decode(&c)?));
|
||||
}
|
||||
let data = &cache.as_ref().unwrap().1;
|
||||
buf[..n].copy_from_slice(&data[rel as usize..rel as usize + n]);
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//! 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) 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.
|
||||
|
||||
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<usize>;
|
||||
}
|
||||
|
||||
/// Disco virtual aberto de uma imagem de contêiner.
|
||||
pub struct VDisk {
|
||||
inner: Box<dyn Backend>,
|
||||
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<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}"))
|
||||
}
|
||||
|
||||
/// 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<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).map_err(|e| io_err(e, &name))?),
|
||||
"VMDK" => Box::new(vmdk::Vmdk::open(file, path).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 de um VHD/VHDX diferencial.
|
||||
pub(crate) fn open_parent(
|
||||
candidates: &[std::path::PathBuf],
|
||||
depth: u32,
|
||||
) -> 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));
|
||||
}
|
||||
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(", ")
|
||||
)))
|
||||
}
|
||||
|
||||
/// 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<Vec<u8>> {
|
||||
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<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)
|
||||
}
|
||||
|
||||
/// UTF-16 (big ou little endian) até o primeiro nulo.
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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<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
|
||||
}
|
||||
|
||||
/// 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<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(())
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//! 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.
|
||||
|
||||
use super::{bad, be32, be64, check_table, read_exact, read_vec, Backend};
|
||||
use crate::source::{FileSource, Source};
|
||||
use std::io::{self, Read};
|
||||
use std::sync::Mutex;
|
||||
|
||||
const OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00;
|
||||
const COMPRESSED: u64 = 1 << 62;
|
||||
|
||||
pub(super) struct Qcow2 {
|
||||
file: FileSource,
|
||||
size: u64,
|
||||
cluster_bits: u32,
|
||||
l1: Vec<u64>,
|
||||
zstd: bool,
|
||||
/// Ú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> {
|
||||
let h = read_vec(&file, 0, 112.min(file.size() as usize))?;
|
||||
if h.len() < 72 {
|
||||
return Err(bad("cabeçalho QCOW2 truncado"));
|
||||
}
|
||||
let version = be32(&h, 4);
|
||||
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!(
|
||||
"tamanho de cluster inválido (2^{cluster_bits})"
|
||||
)));
|
||||
}
|
||||
let size = be64(&h, 24);
|
||||
match be32(&h, 32) {
|
||||
0 => {}
|
||||
1 => return Err(bad("imagem QCOW2 cifrada (AES legado) não é suportada")),
|
||||
_ => return Err(bad("imagem QCOW2 cifrada (LUKS) não é suportada; abra o volume LUKS depois de convertê-la")),
|
||||
}
|
||||
let mut zstd = false;
|
||||
if version == 3 && h.len() >= 108 {
|
||||
let incompat = be64(&h, 72);
|
||||
if incompat & (1 << 2) != 0 {
|
||||
return Err(bad("QCOW2 com arquivo de dados externo não é suportado"));
|
||||
}
|
||||
if incompat & (1 << 4) != 0 {
|
||||
return Err(bad("QCOW2 com L2 estendido não é suportado"));
|
||||
}
|
||||
if incompat & (1 << 3) != 0 {
|
||||
zstd = h[104] == 1;
|
||||
if h[104] > 1 {
|
||||
return Err(bad("tipo de compressão QCOW2 desconhecido"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let l1_size = be32(&h, 36) as u64;
|
||||
let l1_off = be64(&h, 40);
|
||||
let n = check_table(l1_size, 8, "tabela L1")?;
|
||||
let raw = read_vec(&file, l1_off, n * 8)?;
|
||||
let l1 = raw.chunks_exact(8).map(|c| be64(c, 0)).collect();
|
||||
Ok(Self {
|
||||
file,
|
||||
size,
|
||||
cluster_bits,
|
||||
l1,
|
||||
zstd,
|
||||
cache: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
fn cluster_size(&self) -> u64 {
|
||||
1 << self.cluster_bits
|
||||
}
|
||||
|
||||
fn l2_entry(&self, cluster_idx: u64) -> io::Result<u64> {
|
||||
let per_l2 = self.cluster_size() / 8;
|
||||
let l1_idx = (cluster_idx / per_l2) as usize;
|
||||
let l2_idx = cluster_idx % per_l2;
|
||||
let Some(&l1e) = self.l1.get(l1_idx) else {
|
||||
return Ok(0);
|
||||
};
|
||||
let l2_off = l1e & OFFSET_MASK;
|
||||
if l2_off == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut b = [0u8; 8];
|
||||
read_exact(&self.file, l2_off + l2_idx * 8, &mut b)?;
|
||||
Ok(u64::from_be_bytes(b))
|
||||
}
|
||||
|
||||
fn inflate(&self, entry: u64) -> io::Result<Vec<u8>> {
|
||||
let shift = 62 - (self.cluster_bits - 8);
|
||||
let host = entry & ((1u64 << shift) - 1);
|
||||
let nsect = ((entry >> shift) & ((1u64 << (self.cluster_bits - 8)) - 1)) + 1;
|
||||
let csize = (nsect * 512 - (host & 511)) as usize;
|
||||
let avail = self.file.size().saturating_sub(host) as usize;
|
||||
let raw = read_vec(&self.file, host, csize.min(avail))?;
|
||||
let mut out = Vec::with_capacity(self.cluster_size() as usize);
|
||||
if self.zstd {
|
||||
let mut d = ruzstd::decoding::StreamingDecoder::new(&raw[..])
|
||||
.map_err(|e| bad(format!("cluster zstd inválido: {e}")))?;
|
||||
d.read_to_end(&mut out)?;
|
||||
} else {
|
||||
flate2::read::DeflateDecoder::new(&raw[..]).read_to_end(&mut out)?;
|
||||
}
|
||||
out.resize(self.cluster_size() as usize, 0);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for Qcow2 {
|
||||
fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn read_chunk(&self, voff: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let cs = self.cluster_size();
|
||||
let idx = voff / cs;
|
||||
let within = voff % cs;
|
||||
let n = buf.len().min((cs - within) as usize);
|
||||
let entry = self.l2_entry(idx)?;
|
||||
if entry & COMPRESSED != 0 {
|
||||
let mut cache = self.cache.lock().unwrap();
|
||||
if cache.as_ref().map(|c| c.0) != Some(idx) {
|
||||
*cache = Some((idx, self.inflate(entry)?));
|
||||
}
|
||||
let data = &cache.as_ref().unwrap().1;
|
||||
buf[..n].copy_from_slice(&data[within as usize..within as usize + n]);
|
||||
return Ok(n);
|
||||
}
|
||||
let off = entry & OFFSET_MASK;
|
||||
if off == 0 || entry & 1 != 0 {
|
||||
buf[..n].fill(0);
|
||||
} else {
|
||||
read_exact(&self.file, off + within, &mut buf[..n])?;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! VHD (Microsoft Virtual PC e Hyper-V): discos fixos, dinâmicos e diferenciais. O disco
|
||||
//! diferencial lê o pai (achado pelos localizadores do cabeçalho) nos setores que a camada
|
||||
//! filha não preencheu.
|
||||
|
||||
use super::{
|
||||
bad, be32, be64, candidate_paths, check_table, open_parent, read_exact, read_parent, read_vec,
|
||||
utf16, Backend,
|
||||
};
|
||||
use crate::source::{FileSource, Source};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
const SECTOR: u64 = 512;
|
||||
const NONE: u32 = 0xFFFF_FFFF;
|
||||
|
||||
enum Kind {
|
||||
Fixed,
|
||||
Dynamic {
|
||||
bat: Vec<u32>,
|
||||
block_size: u64,
|
||||
bitmap_size: u64,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) struct Vhd {
|
||||
file: FileSource,
|
||||
size: u64,
|
||||
kind: Kind,
|
||||
parent: Option<Box<dyn Source>>,
|
||||
}
|
||||
|
||||
fn footer_ok(f: &[u8]) -> bool {
|
||||
if f.len() < 512 || &f[..8] != b"conectix" {
|
||||
return false;
|
||||
}
|
||||
let mut sum = 0u32;
|
||||
for (i, &b) in f[..512].iter().enumerate() {
|
||||
if !(64..68).contains(&i) {
|
||||
sum = sum.wrapping_add(b as u32);
|
||||
}
|
||||
}
|
||||
!sum == be32(f, 64)
|
||||
}
|
||||
|
||||
/// VHD fixo: disco bruto seguido de um rodapé de 512 bytes.
|
||||
pub(super) fn is_fixed(file: &FileSource) -> bool {
|
||||
let size = file.size();
|
||||
if size < 2 * SECTOR {
|
||||
return false;
|
||||
}
|
||||
let mut f = [0u8; 512];
|
||||
read_exact(file, size - SECTOR, &mut f).is_ok()
|
||||
&& footer_ok(&f)
|
||||
&& be32(&f, 60) == 2
|
||||
&& be64(&f, 16) == u64::MAX
|
||||
}
|
||||
|
||||
impl Vhd {
|
||||
pub(super) fn open(file: FileSource, path: &Path, depth: u32) -> io::Result<Self> {
|
||||
let size = file.size();
|
||||
let mut footer = [0u8; 512];
|
||||
let tail_ok = size >= SECTOR && {
|
||||
read_exact(&file, size - SECTOR, &mut footer)?;
|
||||
footer_ok(&footer)
|
||||
};
|
||||
if !tail_ok {
|
||||
read_exact(&file, 0, &mut footer)?;
|
||||
if !footer_ok(&footer) {
|
||||
return Err(bad("rodapé VHD inválido"));
|
||||
}
|
||||
}
|
||||
let current = be64(&footer, 48);
|
||||
let disk_type = be32(&footer, 60);
|
||||
match disk_type {
|
||||
2 => {
|
||||
return Ok(Self {
|
||||
file,
|
||||
size: current,
|
||||
kind: Kind::Fixed,
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
3 | 4 => {}
|
||||
t => return Err(bad(format!("tipo de VHD {t} desconhecido"))),
|
||||
}
|
||||
let dh = read_vec(&file, be64(&footer, 16), 1024)?;
|
||||
if &dh[..8] != b"cxsparse" {
|
||||
return Err(bad("cabeçalho de disco dinâmico VHD inválido"));
|
||||
}
|
||||
let bat_off = be64(&dh, 16);
|
||||
let entries = be32(&dh, 28) as u64;
|
||||
let block_size = be32(&dh, 32) as u64;
|
||||
if block_size < SECTOR || !block_size.is_power_of_two() {
|
||||
return Err(bad("tamanho de bloco VHD inválido"));
|
||||
}
|
||||
let n = check_table(entries, 4, "tabela de alocação")?;
|
||||
let raw = read_vec(&file, bat_off, n * 4)?;
|
||||
let bat = raw.chunks_exact(4).map(|c| be32(c, 0)).collect();
|
||||
let bitmap_size = (block_size / SECTOR / 8).div_ceil(SECTOR) * SECTOR;
|
||||
let parent = if disk_type == 4 {
|
||||
let dir = path.parent().unwrap_or(Path::new("."));
|
||||
let mut names = vec![];
|
||||
for i in 0..8 {
|
||||
let o = 576 + i * 24;
|
||||
let code = &dh[o..o + 4];
|
||||
let len = be32(&dh, o + 8) as usize;
|
||||
let off = be64(&dh, o + 16);
|
||||
if matches!(code, b"W2ru" | b"W2ku" | b"Wi2r" | b"Wi2k") && len > 0 && len < 4096 {
|
||||
if let Ok(b) = read_vec(&file, off, len) {
|
||||
names.push(utf16(&b, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
names.push(utf16(&dh[64..64 + 512], true));
|
||||
Some(open_parent(&candidate_paths(dir, &names), depth)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Self {
|
||||
file,
|
||||
size: current,
|
||||
kind: Kind::Dynamic {
|
||||
bat,
|
||||
block_size,
|
||||
bitmap_size,
|
||||
},
|
||||
parent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for Vhd {
|
||||
fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn read_chunk(&self, voff: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let Kind::Dynamic {
|
||||
bat,
|
||||
block_size,
|
||||
bitmap_size,
|
||||
} = &self.kind
|
||||
else {
|
||||
let n = buf.len().min((self.size - voff) as usize);
|
||||
read_exact(&self.file, voff, &mut buf[..n])?;
|
||||
return Ok(n);
|
||||
};
|
||||
let block = (voff / block_size) as usize;
|
||||
let within = voff % block_size;
|
||||
let mut n = buf.len().min((block_size - within) as usize);
|
||||
let entry = bat.get(block).copied().unwrap_or(NONE);
|
||||
if entry == NONE {
|
||||
read_parent(&self.parent, voff, &mut buf[..n])?;
|
||||
return Ok(n);
|
||||
}
|
||||
let base = entry as u64 * SECTOR;
|
||||
let first = within / SECTOR;
|
||||
let last = (within + n as u64 - 1) / SECTOR;
|
||||
let nbytes = (last / 8 - first / 8 + 1) as usize;
|
||||
let bm = read_vec(&self.file, base + first / 8, nbytes)?;
|
||||
let bit = |s: u64| bm[(s / 8 - first / 8) as usize] & (0x80 >> (s % 8)) != 0;
|
||||
let present = bit(first);
|
||||
let mut s = first;
|
||||
while s <= last && bit(s) == present {
|
||||
s += 1;
|
||||
}
|
||||
n = n.min((s * SECTOR - within) as usize);
|
||||
if present {
|
||||
read_exact(&self.file, base + bitmap_size + within, &mut buf[..n])?;
|
||||
} else {
|
||||
read_parent(&self.parent, voff, &mut buf[..n])?;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
//! VHDX (Hyper-V): tabela de regiões, metadados, BAT com blocos de bitmap intercalados,
|
||||
//! discos fixos, dinâmicos e diferenciais (pai achado pelo localizador de pai).
|
||||
//!
|
||||
//! O log de transações não é reproduzido: abrir a imagem nunca a escreve. Se o disco foi
|
||||
//! desligado no meio de uma escrita, os dados ainda no log não aparecem.
|
||||
|
||||
use super::{
|
||||
bad, candidate_paths, check_table, le16, le32, le64, open_parent, read_exact, read_parent,
|
||||
read_vec, utf16, Backend,
|
||||
};
|
||||
use crate::source::{FileSource, Source};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
const MB: u64 = 1 << 20;
|
||||
|
||||
/// GUID em texto para os 16 bytes gravados em disco (três primeiros campos little-endian).
|
||||
fn guid(s: &str) -> [u8; 16] {
|
||||
let hex: Vec<u8> = s
|
||||
.replace('-', "")
|
||||
.as_bytes()
|
||||
.chunks(2)
|
||||
.map(|p| u8::from_str_radix(std::str::from_utf8(p).unwrap(), 16).unwrap())
|
||||
.collect();
|
||||
let mut g = [0u8; 16];
|
||||
g[..4].copy_from_slice(&[hex[3], hex[2], hex[1], hex[0]]);
|
||||
g[4..6].copy_from_slice(&[hex[5], hex[4]]);
|
||||
g[6..8].copy_from_slice(&[hex[7], hex[6]]);
|
||||
g[8..].copy_from_slice(&hex[8..]);
|
||||
g
|
||||
}
|
||||
|
||||
pub(super) struct Vhdx {
|
||||
file: FileSource,
|
||||
size: u64,
|
||||
block_size: u64,
|
||||
sector_size: u64,
|
||||
chunk_ratio: u64,
|
||||
bat: Vec<u64>,
|
||||
parent: Option<Box<dyn Source>>,
|
||||
}
|
||||
|
||||
impl Vhdx {
|
||||
pub(super) fn open(file: FileSource, path: &Path, depth: u32) -> io::Result<Self> {
|
||||
let rt = read_vec(&file, 192 * 1024, 64 * 1024)?;
|
||||
if &rt[..4] != b"regi" {
|
||||
return Err(bad("tabela de regiões VHDX inválida"));
|
||||
}
|
||||
let count = le32(&rt, 8).min(2047) as usize;
|
||||
let (mut bat_at, mut meta_at) = (None, None);
|
||||
for i in 0..count {
|
||||
let e = &rt[16 + i * 32..16 + (i + 1) * 32];
|
||||
let region = (le64(e, 16), le32(e, 24) as u64);
|
||||
if e[..16] == guid("2DC27766-F623-4200-9D64-115E9BFD4A08") {
|
||||
bat_at = Some(region);
|
||||
} else if e[..16] == guid("8B7CA206-4790-4B9A-B8FE-575F050F886E") {
|
||||
meta_at = Some(region);
|
||||
}
|
||||
}
|
||||
let (Some((bat_off, bat_len)), Some((meta_off, meta_len))) = (bat_at, meta_at) else {
|
||||
return Err(bad("regiões BAT e metadados não encontradas"));
|
||||
};
|
||||
let meta = read_vec(&file, meta_off, meta_len.min(MB) as usize)?;
|
||||
if &meta[..8] != b"metadata" {
|
||||
return Err(bad("região de metadados VHDX inválida"));
|
||||
}
|
||||
let items = le16(&meta, 10) as usize;
|
||||
let find = |id: [u8; 16]| -> Option<&[u8]> {
|
||||
(0..items).find_map(|i| {
|
||||
let e = meta.get(32 + i * 32..32 + (i + 1) * 32)?;
|
||||
if e[..16] != id {
|
||||
return None;
|
||||
}
|
||||
let (o, l) = (le32(e, 16) as usize, le32(e, 20) as usize);
|
||||
meta.get(o..o + l)
|
||||
})
|
||||
};
|
||||
let params = find(guid("CAA16737-FA36-4D43-B3B6-33F0AA44E76B"))
|
||||
.filter(|p| p.len() >= 8)
|
||||
.ok_or_else(|| bad("parâmetros do arquivo VHDX ausentes"))?;
|
||||
let block_size = le32(params, 0) as u64;
|
||||
let has_parent = le32(params, 4) & 2 != 0;
|
||||
let size = find(guid("2FA54224-CD1B-4876-B211-5DBED83BF4B8"))
|
||||
.filter(|p| p.len() >= 8)
|
||||
.map(|p| le64(p, 0))
|
||||
.ok_or_else(|| bad("tamanho do disco VHDX ausente"))?;
|
||||
let sector_size = find(guid("8141BF1D-A96F-4709-BA47-F233A8FAAB5F"))
|
||||
.filter(|p| p.len() >= 4)
|
||||
.map(|p| le32(p, 0) as u64)
|
||||
.unwrap_or(512);
|
||||
if block_size < MB || !block_size.is_power_of_two() || !matches!(sector_size, 512 | 4096) {
|
||||
return Err(bad("parâmetros VHDX inválidos"));
|
||||
}
|
||||
let chunk_ratio = (1u64 << 23) * sector_size / block_size;
|
||||
let payload = size.div_ceil(block_size);
|
||||
// Com pai, cada trecho termina com a entrada do bitmap de setores, mesmo que o disco acabe antes.
|
||||
let want = if has_parent {
|
||||
payload.div_ceil(chunk_ratio) * (chunk_ratio + 1)
|
||||
} else {
|
||||
payload + payload.div_ceil(chunk_ratio)
|
||||
};
|
||||
let n = check_table(want.min(bat_len / 8), 8, "BAT")?;
|
||||
let raw = read_vec(&file, bat_off, n * 8)?;
|
||||
let bat = raw.chunks_exact(8).map(|c| le64(c, 0)).collect();
|
||||
|
||||
let parent = if has_parent {
|
||||
let loc = find(guid("A8D35F2D-B30B-454D-ABF7-D3D84834AB0C"))
|
||||
.ok_or_else(|| bad("localizador de pai VHDX ausente"))?;
|
||||
let count = if loc.len() >= 20 {
|
||||
le16(loc, 18) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut names = vec![];
|
||||
for i in 0..count {
|
||||
let Some(e) = loc.get(20 + i * 12..20 + (i + 1) * 12) else {
|
||||
break;
|
||||
};
|
||||
let (ko, vo) = (le32(e, 0) as usize, le32(e, 4) as usize);
|
||||
let (ks, vs) = (le16(e, 8) as usize, le16(e, 10) as usize);
|
||||
let (Some(k), Some(v)) = (loc.get(ko..ko + ks), loc.get(vo..vo + vs)) else {
|
||||
continue;
|
||||
};
|
||||
let key = utf16(k, false);
|
||||
if matches!(
|
||||
key.as_str(),
|
||||
"relative_path" | "absolute_win32_path" | "volume_path"
|
||||
) {
|
||||
names.push(utf16(v, false));
|
||||
}
|
||||
}
|
||||
let dir = path.parent().unwrap_or(Path::new("."));
|
||||
Some(open_parent(&candidate_paths(dir, &names), depth)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Self {
|
||||
file,
|
||||
size,
|
||||
block_size,
|
||||
sector_size,
|
||||
chunk_ratio,
|
||||
bat,
|
||||
parent,
|
||||
})
|
||||
}
|
||||
|
||||
fn bat(&self, i: u64) -> u64 {
|
||||
self.bat.get(i as usize).copied().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for Vhdx {
|
||||
fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn read_chunk(&self, voff: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let block = voff / self.block_size;
|
||||
let within = voff % self.block_size;
|
||||
let mut n = buf.len().min((self.block_size - within) as usize);
|
||||
let e = self.bat(block + block / self.chunk_ratio);
|
||||
let off = e & 0xFFFF_FFFF_FFF0_0000;
|
||||
match e & 7 {
|
||||
6 => {
|
||||
read_exact(&self.file, off + within, &mut buf[..n])?;
|
||||
}
|
||||
7 => {
|
||||
// Parcialmente presente: o bitmap do trecho diz, setor a setor, onde está o dado.
|
||||
let chunk = block / self.chunk_ratio;
|
||||
let bm_entry = self.bat(chunk * (self.chunk_ratio + 1) + self.chunk_ratio);
|
||||
let ss = self.sector_size;
|
||||
let per_block = self.block_size / ss;
|
||||
let first = within / ss;
|
||||
let last = (within + n as u64 - 1) / ss;
|
||||
let base_bit = (block % self.chunk_ratio) * per_block;
|
||||
let bm_off = bm_entry & 0xFFFF_FFFF_FFF0_0000;
|
||||
if bm_entry & 7 != 6 {
|
||||
read_parent(&self.parent, voff, &mut buf[..n])?;
|
||||
return Ok(n);
|
||||
}
|
||||
let b0 = (base_bit + first) / 8;
|
||||
let b1 = (base_bit + last) / 8;
|
||||
let bm = read_vec(&self.file, bm_off + b0, (b1 - b0 + 1) as usize)?;
|
||||
let bit = |s: u64| {
|
||||
let p = base_bit + s;
|
||||
bm[(p / 8 - b0) as usize] >> (p % 8) & 1 == 1
|
||||
};
|
||||
let present = bit(first);
|
||||
let mut s = first;
|
||||
while s <= last && bit(s) == present {
|
||||
s += 1;
|
||||
}
|
||||
n = n.min((s * ss - within) as usize);
|
||||
if present {
|
||||
read_exact(&self.file, off + within, &mut buf[..n])?;
|
||||
} else {
|
||||
read_parent(&self.parent, voff, &mut buf[..n])?;
|
||||
}
|
||||
}
|
||||
2 | 3 => buf[..n].fill(0),
|
||||
_ => read_parent(&self.parent, voff, &mut buf[..n])?,
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
//! 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.
|
||||
|
||||
use super::{bad, check_table, le16, le32, le64, read_exact, read_vec, Backend};
|
||||
use crate::source::{FileSource, Source};
|
||||
use std::io::{self, Read};
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
const SECTOR: u64 = 512;
|
||||
const GD_AT_END: u64 = u64::MAX;
|
||||
|
||||
struct Sparse {
|
||||
file: FileSource,
|
||||
grain: u64,
|
||||
gtes: u64,
|
||||
gd: Vec<u32>,
|
||||
compressed: bool,
|
||||
cache: Mutex<Option<(u64, Vec<u8>)>>,
|
||||
}
|
||||
|
||||
enum Kind {
|
||||
Zero,
|
||||
Flat { file: FileSource, offset: u64 },
|
||||
Sparse(Sparse),
|
||||
}
|
||||
|
||||
struct Extent {
|
||||
start: u64,
|
||||
len: u64,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
pub(super) struct Vmdk {
|
||||
extents: Vec<Extent>,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
fn parent_in(descriptor: &str) -> bool {
|
||||
descriptor.lines().any(|l| {
|
||||
let l = l.trim();
|
||||
l.strip_prefix("parentCID")
|
||||
.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")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl Sparse {
|
||||
fn open(file: FileSource) -> io::Result<(Self, u64, Option<String>)> {
|
||||
let mut h = read_vec(&file, 0, SECTOR as usize)?;
|
||||
if &h[..4] == b"SESp" || &h[..4] == b"seSp" {
|
||||
return Err(bad("VMDK no formato SEsparse (vSphere) não é suportado"));
|
||||
}
|
||||
if &h[..4] != b"KDMV" {
|
||||
return Err(bad("extensão VMDK sem assinatura KDMV"));
|
||||
}
|
||||
if le64(&h, 56) == GD_AT_END {
|
||||
let size = file.size();
|
||||
if size < 3 * SECTOR {
|
||||
return Err(bad("VMDK truncado"));
|
||||
}
|
||||
h = read_vec(&file, size - 2 * SECTOR, SECTOR as usize)?;
|
||||
if &h[..4] != b"KDMV" {
|
||||
return Err(bad("rodapé do VMDK streamOptimized ausente"));
|
||||
}
|
||||
}
|
||||
let flags = le32(&h, 8);
|
||||
let capacity = le64(&h, 12);
|
||||
let grain = le64(&h, 20);
|
||||
let desc_off = le64(&h, 28);
|
||||
let desc_len = le64(&h, 36);
|
||||
let gtes = le32(&h, 44) as u64;
|
||||
let gd_off = le64(&h, 56);
|
||||
let algo = le16(&h, 77);
|
||||
if grain == 0 || !grain.is_power_of_two() || gtes == 0 {
|
||||
return Err(bad("cabeçalho VMDK inválido"));
|
||||
}
|
||||
let compressed = flags & (1 << 16) != 0;
|
||||
if compressed && algo != 1 {
|
||||
return Err(bad("compressão de grão VMDK desconhecida"));
|
||||
}
|
||||
let descriptor = if desc_off != 0 && desc_len != 0 {
|
||||
let raw = read_vec(
|
||||
&file,
|
||||
desc_off * SECTOR,
|
||||
(desc_len * SECTOR).min(1 << 20) as usize,
|
||||
)?;
|
||||
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
|
||||
Some(String::from_utf8_lossy(&raw[..end]).into_owned())
|
||||
} 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)?;
|
||||
let gd = raw.chunks_exact(4).map(|c| le32(c, 0)).collect();
|
||||
Ok((
|
||||
Self {
|
||||
file,
|
||||
grain: grain * SECTOR,
|
||||
gtes,
|
||||
gd,
|
||||
compressed,
|
||||
cache: Mutex::new(None),
|
||||
},
|
||||
capacity * SECTOR,
|
||||
descriptor,
|
||||
))
|
||||
}
|
||||
|
||||
fn read(&self, off: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let g = off / self.grain;
|
||||
let within = off % self.grain;
|
||||
let n = buf.len().min((self.grain - within) as usize);
|
||||
let gt_sector = self.gd.get((g / self.gtes) as usize).copied().unwrap_or(0) as u64;
|
||||
let mut sector = 0u64;
|
||||
if gt_sector > 1 {
|
||||
let mut b = [0u8; 4];
|
||||
read_exact(&self.file, gt_sector * SECTOR + (g % self.gtes) * 4, &mut b)?;
|
||||
sector = u32::from_le_bytes(b) as u64;
|
||||
}
|
||||
if sector <= 1 {
|
||||
buf[..n].fill(0);
|
||||
return Ok(n);
|
||||
}
|
||||
if !self.compressed {
|
||||
read_exact(&self.file, sector * SECTOR + within, &mut buf[..n])?;
|
||||
return Ok(n);
|
||||
}
|
||||
let mut cache = self.cache.lock().unwrap();
|
||||
if cache.as_ref().map(|c| c.0) != Some(g) {
|
||||
let mut m = [0u8; 12];
|
||||
read_exact(&self.file, sector * SECTOR, &mut m)?;
|
||||
let csize = le32(&m, 8) as usize;
|
||||
if csize == 0 || csize > self.grain as usize * 2 {
|
||||
return Err(bad("grão VMDK comprimido com tamanho inválido"));
|
||||
}
|
||||
let raw = read_vec(&self.file, sector * SECTOR + 12, csize)?;
|
||||
let mut out = Vec::with_capacity(self.grain as usize);
|
||||
flate2::read::ZlibDecoder::new(&raw[..]).read_to_end(&mut out)?;
|
||||
out.resize(self.grain as usize, 0);
|
||||
*cache = Some((g, out));
|
||||
}
|
||||
let data = &cache.as_ref().unwrap().1;
|
||||
buf[..n].copy_from_slice(&data[within as usize..within as usize + n]);
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
let l = line.trim();
|
||||
let mut it = l.split_whitespace();
|
||||
let (Some(access), Some(sectors), Some(kind)) = (it.next(), it.next(), it.next()) else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(access, "RW" | "RDONLY" | "NOACCESS") {
|
||||
continue;
|
||||
}
|
||||
let Ok(sectors) = sectors.parse::<u64>() else {
|
||||
continue;
|
||||
};
|
||||
let len = sectors * SECTOR;
|
||||
let name = match (l.find('"'), l.rfind('"')) {
|
||||
(Some(a), Some(b)) if b > a => Some(&l[a + 1..b]),
|
||||
_ => None,
|
||||
};
|
||||
let kind = match kind {
|
||||
"ZERO" => Kind::Zero,
|
||||
"FLAT" | "VMFS" => {
|
||||
let name = name.ok_or_else(|| bad("extensão FLAT sem nome de arquivo"))?;
|
||||
let offset = l
|
||||
.rsplit_once('"')
|
||||
.and_then(|(_, r)| r.trim().parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
Kind::Flat {
|
||||
file: FileSource::open(&dir.join(name))?,
|
||||
offset: offset * SECTOR,
|
||||
}
|
||||
}
|
||||
"SPARSE" => {
|
||||
let name = name.ok_or_else(|| bad("extensão SPARSE sem nome de arquivo"))?;
|
||||
let (sp, cap, _) = Sparse::open(FileSource::open(&dir.join(name))?)?;
|
||||
if cap != len {
|
||||
return Err(bad("tamanho da extensão VMDK difere do descritor"));
|
||||
}
|
||||
Kind::Sparse(sp)
|
||||
}
|
||||
other => return Err(bad(format!("tipo de extensão VMDK {other} não suportado"))),
|
||||
};
|
||||
out.push(Extent { start, len, kind });
|
||||
start += len;
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(bad("descritor VMDK sem extensões"));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
impl Vmdk {
|
||||
pub(super) fn open(file: FileSource, path: &Path) -> 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),
|
||||
}]
|
||||
} 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 size = extents.iter().map(|e| e.len).sum();
|
||||
Ok(Self { extents, size })
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for Vmdk {
|
||||
fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn read_chunk(&self, voff: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let i = self.extents.partition_point(|e| e.start + e.len <= voff);
|
||||
let Some(e) = self.extents.get(i) else {
|
||||
return Ok(0);
|
||||
};
|
||||
let rel = voff - e.start;
|
||||
let n = buf.len().min((e.len - rel) as usize);
|
||||
match &e.kind {
|
||||
Kind::Zero => {
|
||||
buf[..n].fill(0);
|
||||
Ok(n)
|
||||
}
|
||||
Kind::Flat { file, offset } => {
|
||||
read_exact(file, offset + rel, &mut buf[..n])?;
|
||||
Ok(n)
|
||||
}
|
||||
Kind::Sparse(s) => s.read(rel, &mut buf[..n]),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user