use crate::error::{Result, VmSpectError};
use crate::vms::vmdk;
use std::collections::VecDeque;
use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
pub const EXTENSIONES_SOPORTADAS: &[&str] = &["vmdk", "qcow2", "vdi", "vhd", "vhdx", "raw", "img"];
pub const MAGIC_QCOW2: &[u8; 4] = b"QFI\xfb";
pub const MAGIC_VMDK_KDMV: &[u8; 4] = b"KDMV";
pub const MAGIC_VHDX: &[u8; 8] = b"vhdxfile";
pub const MAGIC_VHD_CONECTIX: &[u8; 8] = b"conectix";
pub const MAGIC_VDI_SIGNATURE: &[u8; 4] = &[0x7F, 0x10, 0xDA, 0xBE];
pub const MAGIC_VDI_PREFIX_SUN: &[u8] = b"<<< Sun VirtualBox Disk Image >>>";
pub const MAGIC_VDI_PREFIX_ORACLE: &[u8] = b"<<< Oracle VM VirtualBox Disk Image >>>";
pub fn es_extent_secundario(ruta: &Path) -> bool {
let nombre = match ruta.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_ascii_lowercase(),
None => return false,
};
if nombre.ends_with(".vmdk") {
let stem = match ruta.file_stem().and_then(|s| s.to_str()) {
Some(s) => s.to_ascii_lowercase(),
None => return false,
};
if stem.ends_with("-flat")
|| stem.ends_with("_flat")
|| stem.ends_with("-delta")
|| stem.ends_with("_delta")
|| stem.ends_with("-sesparse")
|| stem.ends_with("_sesparse")
{
return true;
}
for sep in &["-s", "_s"] {
if let Some(pos) = stem.rfind(sep) {
let suffix = &stem[pos + sep.len()..];
if !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()) {
return true;
}
}
}
} else if nombre.ends_with(".vhd") || nombre.ends_with(".vhdx") {
let stem = match ruta.file_stem().and_then(|s| s.to_str()) {
Some(s) => s.to_ascii_lowercase(),
None => return false,
};
if stem.ends_with("-sys")
|| stem.ends_with("_sys")
|| stem.ends_with("-delta")
|| stem.ends_with("_delta")
{
return true;
}
}
false
}
pub use es_extent_secundario as is_secondary_extent;
pub fn es_imagen_vm(ruta: &Path) -> bool {
if ruta.is_dir() {
return false;
}
let ext = match ruta.extension().and_then(|e| e.to_str()) {
Some(e) => e.to_ascii_lowercase(),
None => return false,
};
if !EXTENSIONES_SOPORTADAS.contains(&ext.as_str()) {
return false;
}
if es_extent_secundario(ruta) {
return false;
}
if let Ok(meta) = fs::metadata(ruta) {
if !meta.is_file() || meta.len() == 0 {
return false;
}
}
true
}
pub use es_imagen_vm as is_vm_image;
pub fn listar_vms(directorio: &Path, recursivo: bool) -> Result<Vec<PathBuf>> {
if !directorio.exists() {
return Err(VmSpectError::ImageNotFound(format!(
"Directorio no encontrado: {}",
directorio.display()
)));
}
if !directorio.is_dir() {
return Err(VmSpectError::Other(format!(
"La ruta especificada no es un directorio: {}",
directorio.display()
)));
}
let mut imagenes = Vec::new();
let mut cola = VecDeque::new();
cola.push_back(directorio.to_path_buf());
while let Some(dir_actual) = cola.pop_front() {
let entries = match fs::read_dir(&dir_actual) {
Ok(entries) => entries,
Err(e) => return Err(VmSpectError::Io(e)),
};
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => return Err(VmSpectError::Io(e)),
};
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(e) => return Err(VmSpectError::Io(e)),
};
if file_type.is_dir() {
if recursivo {
cola.push_back(path);
}
} else if file_type.is_file() && es_imagen_vm(&path) {
imagenes.push(path);
}
}
}
imagenes.sort();
Ok(imagenes)
}
pub use listar_vms as list_vms;
pub fn contar_vms(directorio: &Path, recursivo: bool) -> Result<usize> {
listar_vms(directorio, recursivo).map(|lista| lista.len())
}
pub use contar_vms as count_vms;
pub fn hay_vms(directorio: &Path, recursivo: bool) -> Result<bool> {
if !directorio.exists() {
return Err(VmSpectError::ImageNotFound(format!(
"Directorio no encontrado: {}",
directorio.display()
)));
}
if !directorio.is_dir() {
return Err(VmSpectError::Other(format!(
"La ruta especificada no es un directorio: {}",
directorio.display()
)));
}
let mut cola = VecDeque::new();
cola.push_back(directorio.to_path_buf());
while let Some(dir_actual) = cola.pop_front() {
let entries = match fs::read_dir(&dir_actual) {
Ok(entries) => entries,
Err(e) => return Err(VmSpectError::Io(e)),
};
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => return Err(VmSpectError::Io(e)),
};
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(e) => return Err(VmSpectError::Io(e)),
};
if file_type.is_dir() {
if recursivo {
cola.push_back(path);
}
} else if file_type.is_file() && es_imagen_vm(&path) {
return Ok(true);
}
}
}
Ok(false)
}
pub use hay_vms as has_vms;
pub fn verificar_integridad_imagen(ruta: &Path) -> Result<bool> {
if !ruta.exists() {
return Err(VmSpectError::ImageNotFound(format!(
"Imagen no encontrada: {}",
ruta.display()
)));
}
let mut archivo = File::open(ruta).map_err(VmSpectError::Io)?;
let tamano = archivo.metadata().map_err(VmSpectError::Io)?.len();
if tamano == 0 {
return Ok(false);
}
let cant_leer = (tamano as usize).min(4096);
let mut cabecera = vec![0u8; cant_leer];
archivo
.read_exact(&mut cabecera)
.map_err(VmSpectError::Io)?;
let ext = ruta
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.unwrap_or_default();
match ext.as_str() {
"qcow2" => Ok(cabecera.len() >= 4 && cabecera.starts_with(MAGIC_QCOW2)),
"vmdk" => {
if cabecera.len() >= 4 && cabecera.starts_with(MAGIC_VMDK_KDMV) {
return Ok(true);
}
let texto = String::from_utf8_lossy(&cabecera);
let texto_trim = texto.trim_start();
if texto_trim.starts_with("# Disk DescriptorFile")
|| texto_trim.starts_with("# VMDK Header")
|| texto_trim.starts_with("# VMDK")
|| texto_trim.contains("# Disk DescriptorFile")
{
return Ok(true);
}
Ok(false)
}
"vdi" => {
if cabecera.starts_with(b"<<< ") {
if cabecera.starts_with(MAGIC_VDI_PREFIX_SUN)
|| cabecera.starts_with(MAGIC_VDI_PREFIX_ORACLE)
{
return Ok(true);
}
if cabecera.len() >= 0x44 && cabecera[0x40..0x44] == *MAGIC_VDI_SIGNATURE {
return Ok(true);
}
}
if cabecera.len() >= 0x44 && cabecera[0x40..0x44] == *MAGIC_VDI_SIGNATURE {
return Ok(true);
}
Ok(false)
}
"vhdx" => Ok(cabecera.len() >= 8 && cabecera.starts_with(MAGIC_VHDX)),
"vhd" => {
if cabecera.len() >= 8
&& (cabecera.starts_with(MAGIC_VHD_CONECTIX) || cabecera.starts_with(b"cxsparse"))
{
return Ok(true);
}
if tamano >= 512 {
let mut footer = [0u8; 512];
archivo
.seek(SeekFrom::Start(tamano - 512))
.map_err(VmSpectError::Io)?;
archivo.read_exact(&mut footer).map_err(VmSpectError::Io)?;
if footer.starts_with(MAGIC_VHD_CONECTIX) {
return Ok(true);
}
}
Ok(false)
}
"raw" | "img" => {
if tamano < 512 {
return Ok(false);
}
if cabecera.len() >= 512 && cabecera[510] == 0x55 && cabecera[511] == 0xAA {
return Ok(true);
}
if cabecera.len() >= 520 && &cabecera[512..520] == b"EFI PART" {
return Ok(true);
}
Ok(true)
}
_ => {
if cabecera.starts_with(MAGIC_QCOW2)
|| cabecera.starts_with(MAGIC_VMDK_KDMV)
|| cabecera.starts_with(MAGIC_VHDX)
|| cabecera.starts_with(MAGIC_VHD_CONECTIX)
|| (cabecera.len() >= 0x44 && cabecera[0x40..0x44] == *MAGIC_VDI_SIGNATURE)
{
Ok(true)
} else {
Ok(false)
}
}
}
}
pub use verificar_integridad_imagen as verify_image_integrity;
pub fn requiere_nbd(ruta: &Path) -> Result<bool> {
if !ruta.exists() {
return Err(VmSpectError::ImageNotFound(format!(
"Imagen no encontrada: {}",
ruta.display()
)));
}
let ext = ruta
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.unwrap_or_default();
match ext.as_str() {
"raw" | "img" => Ok(false),
"qcow2" | "vdi" | "vhd" | "vhdx" => Ok(true),
"vmdk" => {
let mut archivo = File::open(ruta).map_err(VmSpectError::Io)?;
let mut cabecera = [0u8; 512];
let n = archivo.read(&mut cabecera).map_err(VmSpectError::Io)?;
let cabecera = &cabecera[..n];
if vmdk::es_cabecera_sparse(cabecera) {
let cab = match vmdk::leer_cabecera_sparse(cabecera) {
Ok(c) => c,
Err(_) => return Ok(true),
};
if cab.motivo_no_soportado().is_some() {
return Ok(true);
}
if cab.descriptor_offset != 0 && cab.descriptor_sectores != 0 {
let bytes_desc = (cab.descriptor_sectores * vmdk::SECTOR) as usize;
let mut texto = vec![0u8; bytes_desc.min(64 * 1024)];
if archivo
.seek(SeekFrom::Start(cab.descriptor_offset * vmdk::SECTOR))
.is_ok()
&& archivo.read_exact(&mut texto).is_ok()
{
let d = vmdk::parsear_descriptor(&String::from_utf8_lossy(&texto));
if d.tiene_padre() || d.extents.len() > 1 {
return Ok(true);
}
}
}
Ok(false)
} else if vmdk::es_descriptor_texto(cabecera) {
let texto = fs::read_to_string(ruta).map_err(VmSpectError::Io)?;
let d = vmdk::parsear_descriptor(&texto);
if d.tiene_padre() || d.extents.is_empty() {
return Ok(true);
}
if d.extents.len() > 1
|| d.create_type
.to_ascii_lowercase()
.contains("twogbmaxextent")
{
return Ok(true);
}
if d.extents.len() == 1 {
let tipo = d.extents[0].tipo.to_ascii_uppercase();
if tipo == "FLAT" || tipo == "ZERO" {
return Ok(false);
}
}
Ok(true)
} else {
Ok(true)
}
}
_ => Ok(true),
}
}
pub use requiere_nbd as requires_nbd;
pub fn requiere_qemu(ruta: &Path) -> Result<bool> {
requiere_nbd(ruta)
}
pub use requiere_qemu as requires_qemu;
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_es_extent_secundario() {
assert!(es_extent_secundario(Path::new("ubuntu-flat.vmdk")));
assert!(es_extent_secundario(Path::new("ubuntu_flat.vmdk")));
assert!(es_extent_secundario(Path::new("ubuntu-delta.vmdk")));
assert!(es_extent_secundario(Path::new("ubuntu-sesparse.vmdk")));
assert!(es_extent_secundario(Path::new("windows-s001.vmdk")));
assert!(es_extent_secundario(Path::new("windows-s02.vmdk")));
assert!(es_extent_secundario(Path::new("windows_s1.vmdk")));
assert!(es_extent_secundario(Path::new("disk-sys.vhd")));
assert!(es_extent_secundario(Path::new("disk_sys.vhd")));
assert!(es_extent_secundario(Path::new("disk-delta.vhd")));
assert!(es_extent_secundario(Path::new("disk-delta.vhdx")));
assert!(!es_extent_secundario(Path::new("ubuntu.vmdk")));
assert!(!es_extent_secundario(Path::new("windows-server.vmdk")));
assert!(!es_extent_secundario(Path::new("disk.qcow2")));
assert!(!es_extent_secundario(Path::new("disk.vdi")));
assert!(!es_extent_secundario(Path::new("disk.vhd")));
assert!(!es_extent_secundario(Path::new("disk.vhdx")));
assert!(!es_extent_secundario(Path::new("disk.raw")));
}
#[test]
fn test_es_imagen_vm() {
assert!(es_imagen_vm(Path::new("vm.vmdk")));
assert!(es_imagen_vm(Path::new("vm.qcow2")));
assert!(es_imagen_vm(Path::new("vm.vdi")));
assert!(es_imagen_vm(Path::new("vm.vhd")));
assert!(es_imagen_vm(Path::new("vm.vhdx")));
assert!(es_imagen_vm(Path::new("vm.raw")));
assert!(es_imagen_vm(Path::new("vm.img")));
assert!(!es_imagen_vm(Path::new("vm-flat.vmdk")));
assert!(!es_imagen_vm(Path::new("vm-s001.vmdk")));
assert!(!es_imagen_vm(Path::new("vm.iso")));
assert!(!es_imagen_vm(Path::new("vm.txt")));
}
#[test]
fn test_verificar_integridad_qcow2() {
let dir = tempdir().unwrap();
let ruta = dir.path().join("test.qcow2");
let mut f = File::create(&ruta).unwrap();
f.write_all(b"QFI\xfb\x00\x00\x00\x03").unwrap();
assert!(verificar_integridad_imagen(&ruta).unwrap());
let ruta_invalida = dir.path().join("invalido.qcow2");
let mut f2 = File::create(&ruta_invalida).unwrap();
f2.write_all(b"NOT_QCOW2_HEADER").unwrap();
assert!(!verificar_integridad_imagen(&ruta_invalida).unwrap());
}
#[test]
fn test_verificar_integridad_vmdk() {
let dir = tempdir().unwrap();
let ruta_sparse = dir.path().join("sparse.vmdk");
let mut f1 = File::create(&ruta_sparse).unwrap();
f1.write_all(b"KDMV\x01\x00\x00\x00").unwrap();
assert!(verificar_integridad_imagen(&ruta_sparse).unwrap());
let ruta_desc = dir.path().join("desc.vmdk");
let mut f2 = File::create(&ruta_desc).unwrap();
f2.write_all(b"# Disk DescriptorFile\nversion=1\nCID=fffffffe\n")
.unwrap();
assert!(verificar_integridad_imagen(&ruta_desc).unwrap());
}
#[test]
fn test_verificar_integridad_vdi_vhdx_vhd() {
let dir = tempdir().unwrap();
let ruta_vhdx = dir.path().join("test.vhdx");
let mut f = File::create(&ruta_vhdx).unwrap();
f.write_all(b"vhdxfile\x00\x00\x00\x00").unwrap();
assert!(verificar_integridad_imagen(&ruta_vhdx).unwrap());
let ruta_vhd = dir.path().join("test.vhd");
let mut f2 = File::create(&ruta_vhd).unwrap();
f2.write_all(b"conectix\x00\x00\x00\x00").unwrap();
assert!(verificar_integridad_imagen(&ruta_vhd).unwrap());
let ruta_vdi = dir.path().join("test.vdi");
let mut f3 = File::create(&ruta_vdi).unwrap();
let mut header_vdi = vec![0u8; 100];
header_vdi[..MAGIC_VDI_PREFIX_ORACLE.len()].copy_from_slice(MAGIC_VDI_PREFIX_ORACLE);
header_vdi[0x40..0x44].copy_from_slice(MAGIC_VDI_SIGNATURE);
f3.write_all(&header_vdi).unwrap();
assert!(verificar_integridad_imagen(&ruta_vdi).unwrap());
}
#[test]
fn test_listar_contar_hay_vms() {
let dir = tempdir().unwrap();
let sub = dir.path().join("subdir");
fs::create_dir(&sub).unwrap();
let vm1 = dir.path().join("ubuntu.qcow2");
let vm2 = dir.path().join("disco.vmdk");
let extent = dir.path().join("disco-flat.vmdk");
let vm3 = sub.join("windows.vhdx");
let dummy = dir.path().join("notas.txt");
File::create(&vm1).unwrap().write_all(b"data").unwrap();
File::create(&vm2).unwrap().write_all(b"data").unwrap();
File::create(&extent).unwrap().write_all(b"data").unwrap();
File::create(&vm3).unwrap().write_all(b"data").unwrap();
File::create(&dummy).unwrap().write_all(b"data").unwrap();
let vms_no_rec = listar_vms(dir.path(), false).unwrap();
assert_eq!(vms_no_rec.len(), 2);
assert!(vms_no_rec.contains(&vm1));
assert!(vms_no_rec.contains(&vm2));
assert!(!vms_no_rec.contains(&extent));
assert_eq!(contar_vms(dir.path(), false).unwrap(), 2);
assert!(hay_vms(dir.path(), false).unwrap());
let vms_rec = listar_vms(dir.path(), true).unwrap();
assert_eq!(vms_rec.len(), 3);
assert!(vms_rec.contains(&vm3));
assert_eq!(contar_vms(dir.path(), true).unwrap(), 3);
assert!(hay_vms(dir.path(), true).unwrap());
let dir_vacio = tempdir().unwrap();
assert_eq!(contar_vms(dir_vacio.path(), true).unwrap(), 0);
assert!(!hay_vms(dir_vacio.path(), true).unwrap());
}
#[test]
fn test_requiere_nbd_formatos() {
let dir = tempdir().unwrap();
let raw = dir.path().join("disco.raw");
File::create(&raw).unwrap().write_all(b"raw data").unwrap();
assert!(!requiere_nbd(&raw).unwrap());
let qcow2 = dir.path().join("disco.qcow2");
File::create(&qcow2).unwrap().write_all(b"qcow2").unwrap();
assert!(requiere_nbd(&qcow2).unwrap());
assert!(requiere_qemu(&qcow2).unwrap());
let vhdx = dir.path().join("disco.vhdx");
File::create(&vhdx).unwrap().write_all(b"vhdx").unwrap();
assert!(requiere_nbd(&vhdx).unwrap());
let vmdk_flat_desc = dir.path().join("monolithic_flat.vmdk");
let mut f_desc = File::create(&vmdk_flat_desc).unwrap();
f_desc
.write_all(
b"# Disk DescriptorFile\ncreateType=\"monolithicFlat\"\nRW 2048 FLAT \"data.flat\" 0\n",
)
.unwrap();
assert!(!requiere_nbd(&vmdk_flat_desc).unwrap());
let vmdk_snap = dir.path().join("snapshot.vmdk");
let mut f_snap = File::create(&vmdk_snap).unwrap();
f_snap
.write_all(
b"# Disk DescriptorFile\nparentFileNameHint=\"base.vmdk\"\nRW 2048 FLAT \"snap.flat\" 0\n",
)
.unwrap();
assert!(requiere_nbd(&vmdk_snap).unwrap());
}
}