Mescla a main (CI com imagens de referência e o passo do XFS)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
ccbaa2d7e6
31 files changed
+1414
-101
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,267 @@
|
||||
//! Assinatura digital: chaves, arquivos, adulteração, recuperação inteira e interoperabilidade.
|
||||
|
||||
use guara_core::recover;
|
||||
use guara_core::sign::{self, SigningKey};
|
||||
use guara_core::source::FileSource;
|
||||
use guara_core::*;
|
||||
use std::path::Path;
|
||||
|
||||
fn resident(id: u64, name: &str, bytes: Vec<u8>) -> Found {
|
||||
let ext = name.rsplit('.').next().unwrap_or("").to_string();
|
||||
Found {
|
||||
id,
|
||||
name: name.into(),
|
||||
dir: None,
|
||||
category: signatures::category_for_ext(&ext),
|
||||
ext,
|
||||
size: bytes.len() as u64,
|
||||
origin: Origin::Carved,
|
||||
confidence: Confidence::High,
|
||||
data: DataLoc::Resident { bytes },
|
||||
modified: None,
|
||||
created: None,
|
||||
accessed: None,
|
||||
taken: None,
|
||||
camera: None,
|
||||
px: None,
|
||||
gps: false,
|
||||
sig: None,
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `recover` assina sozinho quando `GUARA_SIGNING_KEY` existe; testes que recuperam não se misturam.
|
||||
static ENV: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
fn keyed(dir: &Path) -> SigningKey {
|
||||
let k = SigningKey::generate();
|
||||
k.save(&dir.join("k.key")).unwrap();
|
||||
k
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_roundtrip_and_no_overwrite() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let k = keyed(dir.path());
|
||||
let again = SigningKey::load(&dir.path().join("k.key")).unwrap();
|
||||
assert_eq!(k.public_bytes(), again.public_bytes());
|
||||
assert_eq!(k.fingerprint(), again.fingerprint());
|
||||
assert_eq!(k.fingerprint().len(), 24); // 5 grupos de 4 + 4 separadores
|
||||
let pubfile = std::fs::read_to_string(dir.path().join("k.pub")).unwrap();
|
||||
assert!(pubfile.contains(&k.fingerprint()));
|
||||
let e = SigningKey::generate()
|
||||
.save(&dir.path().join("k.key"))
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(e.contains("já existe uma chave"), "{e}");
|
||||
// a chave antiga continua intacta
|
||||
assert_eq!(
|
||||
SigningKey::load(&dir.path().join("k.key"))
|
||||
.unwrap()
|
||||
.fingerprint(),
|
||||
k.fingerprint()
|
||||
);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = std::fs::metadata(dir.path().join("k.key"))
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode();
|
||||
assert_eq!(
|
||||
mode & 0o777,
|
||||
0o600,
|
||||
"a chave privada precisa ser só do dono"
|
||||
);
|
||||
}
|
||||
std::fs::write(dir.path().join("lixo.key"), "outra coisa").unwrap();
|
||||
assert!(SigningKey::load(&dir.path().join("lixo.key")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_file_verifies_and_tampering_is_caught() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let k = keyed(dir.path());
|
||||
let f = dir.path().join("relatorio.pdf");
|
||||
std::fs::write(&f, b"conteudo do relatorio").unwrap();
|
||||
let sig = sign::sign_file(&k, &f, Some("Perita Ana")).unwrap();
|
||||
assert_eq!(sig, sign::signature_path(&f));
|
||||
let c = sign::verify_file(&f, None, Some(&k.fingerprint())).unwrap();
|
||||
assert!(
|
||||
c.valid && c.trusted == Some(true) && c.signer.as_deref() == Some("Perita Ana"),
|
||||
"{c:?}"
|
||||
);
|
||||
// fingerprint aceito em outro formato (maiúsculas, sem dois pontos)
|
||||
let loose = k.fingerprint().replace(':', " ").to_uppercase();
|
||||
assert_eq!(
|
||||
sign::verify_file(&f, None, Some(&loose)).unwrap().trusted,
|
||||
Some(true)
|
||||
);
|
||||
|
||||
// outra chave não é a esperada
|
||||
let other = SigningKey::generate().fingerprint();
|
||||
let c = sign::verify_file(&f, None, Some(&other)).unwrap();
|
||||
assert!(!c.valid && c.trusted == Some(false));
|
||||
|
||||
// arquivo alterado
|
||||
std::fs::write(&f, b"conteudo ALTERADO do relatorio").unwrap();
|
||||
let c = sign::verify_file(&f, None, None).unwrap();
|
||||
assert!(!c.valid && !c.file_matches && c.signature_valid, "{c:?}");
|
||||
std::fs::write(&f, b"conteudo do relatorio").unwrap();
|
||||
assert!(sign::verify_file(&f, None, None).unwrap().valid);
|
||||
|
||||
// campos da assinatura alterados (hora, assinante, hash) invalidam
|
||||
let raw = std::fs::read_to_string(&sig).unwrap();
|
||||
for (from, to) in [
|
||||
("Perita Ana", "Outra Pessoa"),
|
||||
("\"assinado_em\": \"20", "\"assinado_em\": \"19"),
|
||||
] {
|
||||
assert!(raw.contains(from), "{from}");
|
||||
std::fs::write(&sig, raw.replacen(from, to, 1)).unwrap();
|
||||
let c = sign::verify_file(&f, None, None).unwrap();
|
||||
assert!(!c.valid && !c.signature_valid, "{from}: {c:?}");
|
||||
}
|
||||
// trocar a chave pública pela de outra pessoa também não passa
|
||||
let mut j: serde_json::Value = serde_json::from_str(&raw).unwrap();
|
||||
let evil = SigningKey::generate();
|
||||
j["chave_publica"] = evil
|
||||
.public_bytes()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect::<String>()
|
||||
.into();
|
||||
std::fs::write(&sig, serde_json::to_vec(&j).unwrap()).unwrap();
|
||||
assert!(!sign::verify_file(&f, None, None).unwrap().valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovery_folder_is_checked_file_by_file() {
|
||||
let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let k = keyed(dir.path());
|
||||
let src = dir.path().join("origem.img");
|
||||
std::fs::write(&src, b"x").unwrap();
|
||||
let fs = FileSource::open(&src).unwrap();
|
||||
let a = resident(1, "a.txt", b"primeiro arquivo".to_vec());
|
||||
let b = resident(2, "b.txt", b"segundo arquivo".to_vec());
|
||||
let out = dir.path().join("saida");
|
||||
let rep = recover::recover(&fs, &[&a, &b], &out, &|| false, &mut |_, _, _| {}).unwrap();
|
||||
assert_eq!(rep.recovered.len(), 2);
|
||||
let manifest = out.join("guara-manifesto.json");
|
||||
sign::sign_file(&k, &manifest, None).unwrap();
|
||||
|
||||
let c = sign::verify_recovery(&out, Some(&k.fingerprint())).unwrap();
|
||||
assert!(c.valid && c.ok == 2 && c.files == 2, "{c:?}");
|
||||
|
||||
let first = out.join(&rep.recovered[0].path);
|
||||
let second = out.join(&rep.recovered[1].path);
|
||||
std::fs::write(&first, b"adulterado").unwrap();
|
||||
std::fs::remove_file(&second).unwrap();
|
||||
let c = sign::verify_recovery(&out, None).unwrap();
|
||||
assert!(
|
||||
!c.valid && c.changed.len() == 1 && c.missing.len() == 1,
|
||||
"{c:?}"
|
||||
);
|
||||
|
||||
// manifesto adulterado (hash trocado para acompanhar o arquivo alterado) quebra a assinatura
|
||||
let m = std::fs::read_to_string(&manifest).unwrap();
|
||||
let real = &rep.recovered[0].sha256;
|
||||
let fake = sign::hash_file(&first).unwrap().0;
|
||||
std::fs::write(&manifest, m.replace(real, &fake)).unwrap();
|
||||
let c = sign::verify_recovery(&out, None).unwrap();
|
||||
let sigcheck = c.manifest_signature.as_ref().unwrap();
|
||||
assert!(!sigcheck.valid && !sigcheck.file_matches, "{c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_paths_cannot_escape_the_folder() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("fora.txt"), b"segredo").unwrap();
|
||||
let out = dir.path().join("saida");
|
||||
std::fs::create_dir(&out).unwrap();
|
||||
let sha = sign::hash_file(&dir.path().join("fora.txt")).unwrap().0;
|
||||
std::fs::write(
|
||||
out.join("guara-manifesto.json"),
|
||||
serde_json::json!({ "relatorio": { "recovered": [ { "path": "../fora.txt", "sha256": sha } ] } }).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
let c = sign::verify_recovery(&out, None).unwrap();
|
||||
assert!(!c.valid && c.changed[0].contains("fora da pasta"), "{c:?}");
|
||||
}
|
||||
|
||||
/// O mesmo que o `openssl` faria: a assinatura é Ed25519 padrão sobre a mensagem documentada.
|
||||
#[test]
|
||||
fn signature_verifies_with_openssl() {
|
||||
let Ok(v) = std::process::Command::new("openssl")
|
||||
.arg("version")
|
||||
.output()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !v.status.success() {
|
||||
return;
|
||||
}
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let k = keyed(dir.path());
|
||||
let f = dir.path().join("doc.txt");
|
||||
std::fs::write(&f, b"hello\n").unwrap();
|
||||
let sig = sign::sign_file(&k, &f, Some("Ana")).unwrap();
|
||||
let j: serde_json::Value = serde_json::from_slice(&std::fs::read(sig).unwrap()).unwrap();
|
||||
let unhex = |s: &str| -> Vec<u8> {
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
|
||||
.collect()
|
||||
};
|
||||
let msg = format!(
|
||||
"{}\narquivo={}\ntamanho={}\nsha256={}\nassinado_em={}\nassinante=Ana\n",
|
||||
j["formato"].as_str().unwrap(),
|
||||
j["arquivo"].as_str().unwrap(),
|
||||
j["tamanho"],
|
||||
j["sha256"].as_str().unwrap(),
|
||||
j["assinado_em"].as_str().unwrap()
|
||||
);
|
||||
std::fs::write(dir.path().join("msg"), msg).unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("sig"),
|
||||
unhex(j["assinatura"].as_str().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
let mut der = unhex("302a300506032b6570032100");
|
||||
der.extend(unhex(j["chave_publica"].as_str().unwrap()));
|
||||
std::fs::write(dir.path().join("pub.der"), der).unwrap();
|
||||
let r = std::process::Command::new("openssl")
|
||||
.current_dir(dir.path())
|
||||
.args([
|
||||
"pkeyutl", "-verify", "-pubin", "-inkey", "pub.der", "-keyform", "DER", "-rawin",
|
||||
"-in", "msg", "-sigfile", "sig",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
// OpenSSL 1.1 não tem -rawin para Ed25519; só exige sucesso onde ele existe
|
||||
let text = String::from_utf8_lossy(&r.stdout).to_string() + &String::from_utf8_lossy(&r.stderr);
|
||||
if text.contains("Unknown option") || text.contains("unknown option") {
|
||||
return;
|
||||
}
|
||||
assert!(r.status.success(), "{text}");
|
||||
}
|
||||
|
||||
/// Com chave configurada, a recuperação e o relatório saem assinados sem ninguém pedir.
|
||||
#[test]
|
||||
fn configured_key_signs_manifest_automatically() {
|
||||
let _g = ENV.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let k = keyed(dir.path());
|
||||
std::env::set_var("GUARA_SIGNING_KEY", dir.path().join("k.key"));
|
||||
std::env::set_var("GUARA_CONFIG_DIR", dir.path().join("cfg"));
|
||||
let src = dir.path().join("origem.img");
|
||||
std::fs::write(&src, b"x").unwrap();
|
||||
let fs = FileSource::open(&src).unwrap();
|
||||
let a = resident(1, "a.txt", b"dados".to_vec());
|
||||
let out = dir.path().join("saida");
|
||||
recover::recover(&fs, &[&a], &out, &|| false, &mut |_, _, _| {}).unwrap();
|
||||
std::env::remove_var("GUARA_SIGNING_KEY");
|
||||
let c = sign::verify_recovery(&out, Some(&k.fingerprint())).unwrap();
|
||||
assert!(c.valid && c.manifest_signature.is_some(), "{c:?}");
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user