Carving de JPEG fragmentado: decodificador de entropia, índice de clusters e remontagem de até três fragmentos
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9ac1dddba
commit
7f00445d67
10 files changed
+1304
-22
No files matched your search
@@ -22,6 +22,12 @@ impl Sink for Collect {
|
||||
f.confidence = f.confidence.max(c);
|
||||
}
|
||||
}
|
||||
fn replace(&mut self, f: Found) {
|
||||
match self.found.iter_mut().find(|x| x.id == f.id) {
|
||||
Some(x) => *x = f,
|
||||
None => self.found.push(f),
|
||||
}
|
||||
}
|
||||
fn progress(&mut self, _: Progress) {}
|
||||
fn warning(&mut self, w: String) {
|
||||
self.warnings.push(w);
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
use guara_core::defrag::*;
|
||||
use guara_core::source::MemSource;
|
||||
use guara_core::types::Extent;
|
||||
|
||||
fn photo(w: u16, h: u16, seed: u64, shift: i32) -> Vec<u8> {
|
||||
photo_n(w, h, seed, shift, 31)
|
||||
}
|
||||
|
||||
fn photo_n(w: u16, h: u16, seed: u64, shift: i32, nmask: u64) -> Vec<u8> {
|
||||
let mut rgb = Vec::with_capacity(w as usize * h as usize * 3);
|
||||
let mut x = seed | 1;
|
||||
for yy in 0..h as i32 {
|
||||
for xx in 0..w as i32 {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
let noise = ((x >> 40) & nmask) as i32;
|
||||
let fx = xx as f32 / w as f32;
|
||||
let fy = yy as f32 / h as f32;
|
||||
let wave = ((fx * 9.0 + seed as f32 * 0.37).sin() * (fy * 7.0 + 1.3).cos() * 40.0) as i32 + ((fx * 31.0).sin() * 8.0) as i32;
|
||||
let base = 110 + shift + wave + (xx * 40 / w as i32);
|
||||
let blob = if ((xx - 300).pow(2) + (yy - 200).pow(2)) < 5000 { 60 } else { 0 };
|
||||
let v = (base + blob + noise).clamp(0, 255) as u8;
|
||||
rgb.extend_from_slice(&[v, v.saturating_sub(10), (v as i32 / 2 + noise) as u8]);
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
jpeg_encoder::Encoder::new(&mut out, 92).encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
fn noise(n: usize, seed: u64) -> Vec<u8> {
|
||||
let mut x = seed | 1;
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
(x >> 24) as u8
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pad(v: &mut Vec<u8>) {
|
||||
v.resize(v.len().div_ceil(4096) * 4096, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_a_jpeg_split_around_another_one() {
|
||||
let a = photo(1024, 768, 7, 0);
|
||||
let b = photo(800, 600, 99, 40);
|
||||
let split = 14 * 4096;
|
||||
assert!(a.len() > split + 3 * 4096, "A pequena: {}", a.len());
|
||||
let mut disk: Vec<u8> = vec![0u8; 64 * 4096];
|
||||
let a_off = disk.len() as u64;
|
||||
disk.extend_from_slice(&a[..split]);
|
||||
let b_off = disk.len() as u64;
|
||||
let mut bb = b.clone();
|
||||
pad(&mut bb);
|
||||
disk.extend_from_slice(&bb);
|
||||
disk.extend(noise(20 * 4096, 5)); // lixo
|
||||
let a2_off = disk.len() as u64;
|
||||
let mut rest = a[split..].to_vec();
|
||||
pad(&mut rest);
|
||||
disk.extend_from_slice(&rest);
|
||||
disk.extend(noise(10 * 4096, 6));
|
||||
let src = MemSource(disk.clone());
|
||||
|
||||
let mut idx = ClusterIndex::new(disk.len() as u64);
|
||||
idx.scan_chunk(0, &disk);
|
||||
// O fragmento correto precisa estar marcado.
|
||||
assert!(idx.get(a2_off));
|
||||
eprintln!("TRUE a2_off={a2_off}");
|
||||
let pool = Pool { index: &idx, used: &[Extent::new(b_off, bb.len() as u64)] };
|
||||
match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) {
|
||||
Analysis::Rebuilt { extents, size } => {
|
||||
assert_eq!(size as usize, a.len(), "tamanho");
|
||||
assert_eq!(extents.len(), 2);
|
||||
assert_eq!(extents[0], Extent::new(a_off, split as u64));
|
||||
assert_eq!(extents[1].offset, a2_off);
|
||||
let mut got = disk[a_off as usize..a_off as usize + split].to_vec();
|
||||
got.extend_from_slice(&disk[a2_off as usize..a2_off as usize + (size as usize - split)]);
|
||||
assert!(got == a, "conteúdo recomposto difere");
|
||||
}
|
||||
other => panic!("esperava Rebuilt, veio {other:?}"),
|
||||
}
|
||||
// O outro JPEG, íntegro, continua íntegro.
|
||||
let pool2 = Pool { index: &idx, used: &[] };
|
||||
match analyze_jpeg(&src, b_off, 4 << 20, &pool2, true) {
|
||||
Analysis::Intact { len } => assert_eq!(len as usize, b.len()),
|
||||
other => panic!("B deveria estar íntegro: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_three_fragments_among_many_decoys() {
|
||||
let a = photo_n(2048, 1536, 21, 10, 3);
|
||||
let n = a.len();
|
||||
let (s1, s2) = (10 * 4096, 10 * 4096 + (n - 10 * 4096) / 2 / 4096 * 4096);
|
||||
assert!(n > s2 + 4 * 4096);
|
||||
let mut disk: Vec<u8> = vec![0u8; 32 * 4096];
|
||||
let a_off = disk.len() as u64;
|
||||
disk.extend_from_slice(&a[..s1]);
|
||||
let mut decoys = vec![];
|
||||
for k in 0..6u64 {
|
||||
let mut d = photo_n(1600, 1200, 100 + k, (k as i32) * 17, 3);
|
||||
pad(&mut d);
|
||||
decoys.push((disk.len() as u64, d.len() as u64));
|
||||
disk.extend_from_slice(&d);
|
||||
if k == 1 {
|
||||
// segundo fragmento de A no meio
|
||||
let off2 = disk.len() as u64;
|
||||
disk.extend_from_slice(&a[s1..s2]);
|
||||
decoys.push((off2, (s2 - s1) as u64));
|
||||
}
|
||||
if k == 4 {
|
||||
let mut r = a[s2..].to_vec();
|
||||
pad(&mut r);
|
||||
disk.extend_from_slice(&r);
|
||||
}
|
||||
}
|
||||
let src = MemSource(disk.clone());
|
||||
let mut idx = ClusterIndex::new(disk.len() as u64);
|
||||
idx.scan_chunk(0, &disk);
|
||||
eprintln!("TRUE frag2={:?}", decoys[2]);
|
||||
let used: Vec<Extent> = decoys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| *i != 2) // o fragmento do meio de A não é de outro arquivo
|
||||
.map(|(_, (o, l))| Extent::new(*o, *l))
|
||||
.collect();
|
||||
let pool = Pool { index: &idx, used: &used };
|
||||
match analyze_jpeg(&src, a_off, 4 << 20, &pool, true) {
|
||||
Analysis::Rebuilt { extents, size } => {
|
||||
assert_eq!(size as usize, n);
|
||||
let mut got = vec![];
|
||||
for e in &extents {
|
||||
got.extend_from_slice(&disk[e.offset as usize..(e.offset + e.len) as usize]);
|
||||
}
|
||||
assert!(got == a, "conteúdo difere ({} fragmentos)", extents.len());
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn deep_scan_recovers_fragmented_jpeg_end_to_end() {
|
||||
use guara_core::{recover, ScanMode, ScanOptions};
|
||||
common::init();
|
||||
let a = photo_n(2048, 1536, 5, 0, 3);
|
||||
let n = a.len();
|
||||
let split = 12 * 4096;
|
||||
let mut disk: Vec<u8> = vec![0u8; 16 * 4096];
|
||||
let a_off = disk.len();
|
||||
disk.extend_from_slice(&a[..split]);
|
||||
let mut b = photo_n(1600, 1200, 77, 30, 3);
|
||||
pad(&mut b);
|
||||
let b_len = b.len();
|
||||
disk.extend_from_slice(&b);
|
||||
disk.extend(noise(8 * 4096, 3));
|
||||
let mut rest = a[split..].to_vec();
|
||||
pad(&mut rest);
|
||||
disk.extend_from_slice(&rest);
|
||||
disk.extend(noise(8 * 4096, 4));
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let img = dir.path().join("d.img");
|
||||
std::fs::write(&img, &disk).unwrap();
|
||||
let c = common::scan_path(&img, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
||||
let jpgs: Vec<_> = c.found.iter().filter(|f| f.ext == "jpg").collect();
|
||||
assert_eq!(jpgs.len(), 2, "{:?}", jpgs.iter().map(|f| (f.size, f.first_offset(), f.confidence)).collect::<Vec<_>>());
|
||||
let fa = jpgs.iter().find(|f| f.first_offset() == Some(a_off as u64)).unwrap();
|
||||
assert_eq!(fa.size as usize, n, "{:?}", fa.note);
|
||||
let out = dir.path().join("out");
|
||||
let rep = common::recover_all(&img, std::slice::from_ref(*fa), &out);
|
||||
let p = std::path::PathBuf::from(&rep.recovered[0].path);
|
||||
let got = std::fs::read(if p.is_absolute() { p } else { out.join(p) }).unwrap();
|
||||
assert!(got == a, "JPEG recuperado difere do original");
|
||||
let fb = jpgs.iter().find(|f| f.first_offset() != Some(a_off as u64)).unwrap();
|
||||
assert_eq!(fb.size as usize, photo_n(1600, 1200, 77, 30, 3).len());
|
||||
let _ = (b_len, recover::RecoverReport::default());
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use guara_core::jpegscan::*;
|
||||
|
||||
fn photo(w: u16, h: u16) -> Vec<u8> {
|
||||
// Imagem "natural": gradientes suaves + textura + bordas.
|
||||
let mut rgb = Vec::with_capacity(w as usize * h as usize * 3);
|
||||
let mut x = 0x9E3779B97F4A7C15u64;
|
||||
for yy in 0..h as i32 {
|
||||
for xx in 0..w as i32 {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
let noise = ((x >> 40) & 31) as i32;
|
||||
let base = 90 + (xx * 90 / w as i32) + (yy * 60 / h as i32);
|
||||
let blob = if ((xx - 200).pow(2) + (yy - 150).pow(2)) < 3600 { 70 } else { 0 };
|
||||
let v = (base + blob + noise).clamp(0, 255) as u8;
|
||||
rgb.extend_from_slice(&[v, v.saturating_sub(20), (v as i32 / 2 + noise) as u8]);
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
let enc = jpeg_encoder::Encoder::new(&mut out, 92);
|
||||
enc.encode(&rgb, w, h, jpeg_encoder::ColorType::Rgb).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_whole_image_and_ends_at_eoi() {
|
||||
let j = photo(1024, 768);
|
||||
let h = parse_header(&j).expect("cabeçalho");
|
||||
assert_eq!((h.width, h.height), (1024, 768));
|
||||
let d = decode(&h, &j, State::start(&h), u32::MAX, 0);
|
||||
assert_eq!(d.stop, Stop::Done);
|
||||
assert_eq!(d.dc.len() as u32, h.total_mcus());
|
||||
// O que sobra depois do último MCU é o EOI.
|
||||
let tail = &j[d.state.pos.min(j.len())..];
|
||||
assert!(tail.windows(2).any(|w| w == [0xFF, 0xD9]), "sem EOI depois do último MCU");
|
||||
assert!(first_discontinuity(&h, &d.dc).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_break_and_resumes_with_the_right_continuation() {
|
||||
let j = photo(1024, 768);
|
||||
let h = parse_header(&j).unwrap();
|
||||
let cut = 12 * 4096; // quebra no limite de cluster
|
||||
assert!(j.len() > cut + 8192, "imagem pequena demais: {}", j.len());
|
||||
// Dados estranhos no lugar do resto.
|
||||
let mut x = 12345u64;
|
||||
let junk: Vec<u8> = (0..40_000)
|
||||
.map(|_| {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
(x >> 24) as u8
|
||||
})
|
||||
.collect();
|
||||
let mut broken = j[..cut].to_vec();
|
||||
broken.extend_from_slice(&junk);
|
||||
let d = decode(&h, &broken, State::start(&h), u32::MAX, 512);
|
||||
assert_ne!(d.stop, Stop::Done);
|
||||
// Decodificação segue o lixo por um tempo; mas a quebra aparece pelo erro ou pela descontinuidade.
|
||||
let mcu_break = first_discontinuity(&h, &d.dc);
|
||||
// MCUs bons: os decodificados ao fim do cluster `cut`.
|
||||
let good = d.snaps.iter().filter(|s| s.pos <= cut).last().expect("snapshot antes da quebra").clone();
|
||||
// Retomada com a continuação verdadeira reproduz a decodificação inteira.
|
||||
let mut joined = j[..cut].to_vec();
|
||||
joined.extend_from_slice(&j[cut..]);
|
||||
let r = decode(&h, &joined, good.clone(), u32::MAX, 0);
|
||||
assert_eq!(r.stop, Stop::Done);
|
||||
assert_eq!(good.mcu as usize + r.dc.len(), h.total_mcus() as usize);
|
||||
let _ = mcu_break;
|
||||
}
|
||||
Reference in new issue
Block a user