fix: PNG truncado não vira arquivo gigante; filtros de tamanho e megapixels
Descoberto ao abrir uma sessão real: texto ASCII após um PNG truncado era lido como tamanho de bloco (~1,9 GB). Também adiciona filtros para separar fotos de ícones e falha cedo ao retomar sem a origem. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
354caaf8f0
commit
b9bef85bc7
6 files changed
+90
-7
No files matched your search
@@ -180,7 +180,14 @@ pub fn facets(found: &[Found]) -> Value {
|
||||
let mut conf = [0usize; 3];
|
||||
let mut seen_sig = std::collections::HashSet::new();
|
||||
let mut dups = 0usize;
|
||||
let (mut small, mut mp1) = (0usize, 0usize);
|
||||
for f in found {
|
||||
if f.size < 30 * 1024 {
|
||||
small += 1;
|
||||
}
|
||||
if f.px.is_some_and(|(w, h)| w as u64 * h as u64 >= 1_000_000) {
|
||||
mp1 += 1;
|
||||
}
|
||||
if let Some(g) = f.sig {
|
||||
if !seen_sig.insert((g, f.size)) {
|
||||
dups += 1;
|
||||
@@ -242,6 +249,8 @@ pub fn facets(found: &[Found]) -> Value {
|
||||
"root": root,
|
||||
"gps": gps,
|
||||
"duplicates": dups,
|
||||
"small": small,
|
||||
"mp1": mp1,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -217,6 +217,9 @@ impl Engine {
|
||||
|
||||
pub fn resume(&self, id: &str) -> Result<()> {
|
||||
let j = self.job(id)?;
|
||||
if j.source().is_none() {
|
||||
return Err(no_source());
|
||||
}
|
||||
let cp = {
|
||||
let mut s = j.st.lock().unwrap();
|
||||
if s.status != Status::Paused {
|
||||
|
||||
@@ -219,8 +219,10 @@ fn jpeg(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||
// ---------------------------------------------------------------- PNG
|
||||
|
||||
fn png(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||
let total = src.size().saturating_sub(off);
|
||||
let mut p = 8u64;
|
||||
let mut first = true;
|
||||
let mut saw_idat = false;
|
||||
for _ in 0..200_000 {
|
||||
let c = read_vec(src, off + p, 8).ok()?;
|
||||
if c.len() < 8 {
|
||||
@@ -228,13 +230,20 @@ fn png(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||
}
|
||||
let len = u32::from_be_bytes([c[0], c[1], c[2], c[3]]) as u64;
|
||||
let ty = &c[4..8];
|
||||
if !ty.iter().all(|b| b.is_ascii_alphabetic()) || len > 0x7FFF_FFFF {
|
||||
// Bloco válido: 4 letras, 3ª maiúscula (bit reservado), tamanho plausível e dentro do
|
||||
// disco. Texto ASCII solto parece "tamanho" de ~1,9 GB e não pode virar um PNG gigante.
|
||||
let valid = ty.iter().all(|b| b.is_ascii_alphabetic())
|
||||
&& ty[2].is_ascii_uppercase()
|
||||
&& len <= 128 * 1024 * 1024
|
||||
&& p + 12 + len <= total;
|
||||
if !valid {
|
||||
break;
|
||||
}
|
||||
if first && ty != b"IHDR" {
|
||||
return None;
|
||||
}
|
||||
first = false;
|
||||
saw_idat |= ty == b"IDAT";
|
||||
p += 12 + len;
|
||||
if p > max {
|
||||
break;
|
||||
@@ -247,11 +256,12 @@ fn png(src: &dyn Source, off: u64, max: u64) -> Option<Hit> {
|
||||
});
|
||||
}
|
||||
}
|
||||
if first {
|
||||
// Sem IEND: só vale como imagem truncada se já tem dados e é de tamanho razoável.
|
||||
if first || !saw_idat || p > 64 * 1024 * 1024 {
|
||||
return None;
|
||||
}
|
||||
Some(Hit {
|
||||
len: p.min(max).min(src.size().saturating_sub(off)),
|
||||
len: p,
|
||||
ext: "png",
|
||||
confidence: Confidence::Low,
|
||||
})
|
||||
|
||||
@@ -473,6 +473,7 @@ fn facetas_filtros_e_duplicadas() {
|
||||
mk(3, "c.jpg", Some("Viagens"), Some(jul23), None, Some(7)), // duplicada de a.jpg
|
||||
mk(4, "d.jpg", None, None, None, None),
|
||||
];
|
||||
let q = |json: &str| -> Query { serde_json::from_str(json).unwrap() };
|
||||
let f = explorer::facets(&found);
|
||||
assert_eq!(f["total"], 4);
|
||||
assert_eq!(f["years"][0]["year"], 2024);
|
||||
@@ -481,6 +482,8 @@ fn facetas_filtros_e_duplicadas() {
|
||||
assert_eq!(f["undated"], 1);
|
||||
assert_eq!(f["carved"], 1);
|
||||
assert_eq!(f["duplicates"], 1);
|
||||
assert_eq!(f["small"], 4); // todos têm 10 bytes
|
||||
assert_eq!(q(r#"{"min_size":30720}"#).select(&found).len(), 0);
|
||||
assert_eq!(f["cameras"][0]["name"], "Canon");
|
||||
let dcim = f["folders"]
|
||||
.as_array()
|
||||
@@ -490,7 +493,6 @@ fn facetas_filtros_e_duplicadas() {
|
||||
.unwrap();
|
||||
assert_eq!(dcim["count"], 2);
|
||||
|
||||
let q = |json: &str| -> Query { serde_json::from_str(json).unwrap() };
|
||||
assert_eq!(q(r#"{"year":2023}"#).select(&found).len(), 2);
|
||||
assert_eq!(q(r#"{"year":2023,"month":7}"#).select(&found).len(), 2);
|
||||
assert_eq!(q(r#"{"year":2023,"month":8}"#).select(&found).len(), 0);
|
||||
@@ -505,3 +507,49 @@ fn facetas_filtros_e_duplicadas() {
|
||||
assert_eq!(items[0].id, 2); // mais recente primeiro
|
||||
assert_eq!(items.last().unwrap().id, 4); // sem data por último
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ carving
|
||||
|
||||
fn png_chunk(ty: &[u8; 4], data: &[u8]) -> Vec<u8> {
|
||||
let mut c = (data.len() as u32).to_be_bytes().to_vec();
|
||||
c.extend(ty);
|
||||
c.extend(data);
|
||||
c.extend([0, 0, 0, 0]);
|
||||
c
|
||||
}
|
||||
|
||||
/// Regressão: texto ASCII depois de um PNG truncado vira "tamanho" de ~1,9 GB;
|
||||
/// o carving não pode inventar um PNG gigante.
|
||||
#[test]
|
||||
fn png_truncado_seguido_de_texto_nao_vira_arquivo_gigante() {
|
||||
let mut d = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
|
||||
d.extend(png_chunk(b"IHDR", &[0, 0, 0, 8, 0, 0, 0, 8, 8, 2, 0, 0, 0]));
|
||||
d.extend(png_chunk(b"IDAT", &vec![7u8; 5000]));
|
||||
let real_end = d.len();
|
||||
d.extend(b"ualiBase64 texto qualquer que segue no disco, como parte de outro arquivo html ");
|
||||
d.resize(4 * 1024 * 1024, 0x41);
|
||||
let src = MemSource(d.clone());
|
||||
let hit =
|
||||
signatures::detect(&src, 0, &d[..64], 8 << 30).expect("PNG truncado ainda é reconhecido");
|
||||
assert_eq!(
|
||||
hit.len as usize, real_end,
|
||||
"deve parar no fim do último bloco válido"
|
||||
);
|
||||
assert_eq!(hit.confidence, Confidence::Low);
|
||||
|
||||
// sem nenhum dado de imagem (só cabeçalho) não é arquivo
|
||||
let mut d2 = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
|
||||
d2.extend(png_chunk(b"IHDR", &[0, 0, 0, 8, 0, 0, 0, 8, 8, 2, 0, 0, 0]));
|
||||
d2.resize(1 << 20, b'e');
|
||||
assert!(signatures::detect(&MemSource(d2.clone()), 0, &d2[..64], 8 << 30).is_none());
|
||||
|
||||
// PNG completo continua com confiança alta e tamanho exato
|
||||
let mut ok = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
|
||||
ok.extend(png_chunk(b"IHDR", &[0, 0, 0, 8, 0, 0, 0, 8, 8, 2, 0, 0, 0]));
|
||||
ok.extend(png_chunk(b"IDAT", &[1; 100]));
|
||||
ok.extend(png_chunk(b"IEND", &[]));
|
||||
let n = ok.len();
|
||||
ok.resize(1 << 20, 0);
|
||||
let h = signatures::detect(&MemSource(ok.clone()), 0, &ok[..64], 8 << 30).unwrap();
|
||||
assert_eq!((h.len as usize, h.confidence), (n, Confidence::High));
|
||||
}
|
||||
Reference in new issue
Block a user