Files
Lucas RayanandClaude Sonnet 5 6328220c60
CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s
chore(i18n): finish translating code comments and scripts to English
Completes the English-only standardization for code, comments, scripts
and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en,
untouched). Covers the remaining crates/, apps/gui/ui/ and
apps/gui/src-tauri/ comments, .gitea/ workflow and template comments,
.githooks/ hooks, and the scripts/ source comments for the files
already renamed in a prior commit (check.sh, generate-logo.py,
install-hooks.sh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:43:02 -03:00

181 lines
6.0 KiB
Rust

//! PDF report generator tests: xref table offsets, WinAnsi accented text escaping, and multi-page tables.
use guara_core::recover::{RecoverReport, RecoveredFile};
use guara_core::report::{self, AuditNote, CaseInfo, ReportInput};
use guara_core::*;
use std::io::Read;
fn found(i: u64) -> Found {
Found {
id: i,
name: format!("fotografia_{i:04}.jpg"),
dir: Some("/Usuários/João/Imagens (cópia)".into()),
category: Category::Image,
ext: "jpg".into(),
size: 1000 + i,
origin: Origin::Carved,
confidence: Confidence::High,
data: DataLoc::Resident { bytes: vec![0; 4] },
modified: Some(1_689_358_925),
created: None,
accessed: None,
taken: None,
camera: None,
px: None,
gps: false,
sig: None,
note: None,
}
}
fn build(n: u64) -> Vec<u8> {
let files: Vec<Found> = (0..n).map(found).collect();
let rec = RecoverReport {
destination: "/mnt/outro/Recuperados".into(),
recovered: (0..3)
.map(|i| RecoveredFile {
id: i,
path: format!("Fotos/fotografia_{i:04}.jpg"),
size: 1000 + i,
sha256: "ab".repeat(32),
md5: "cd".repeat(16),
sha1: "ef".repeat(20),
confidence: Confidence::High,
origin: Origin::Carved,
partial: false,
})
.collect(),
total_bytes: 3003,
..Default::default()
};
let case = CaseInfo {
case_id: "2026-001".into(),
examiner: "Perita (Ana) \\ Souza".into(),
organization: "Laboratório".into(),
notes: "Observação com acentos: ção, ã, é, ü e “aspas”.".into(),
};
let opts = ScanOptions::default();
report::pdf(&ReportInput {
case: &case,
target: "/dev/sdb",
options: &opts,
layout: None,
found: &files,
warnings: &["aviso importante sobre o disco".to_string()],
started_unix: Some(1_689_358_925),
elapsed: 1.5,
recoveries: &[rec],
source_hashes: None,
audit: Some(AuditNote {
entries: 7,
ok: true,
head: "12".repeat(32),
}),
})
}
fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
hay[from..]
.windows(needle.len())
.position(|w| w == needle)
.map(|p| p + from)
}
/// Concatenates all uncompressed content streams in the generated PDF.
fn streams(pdf: &[u8]) -> Vec<String> {
let mut out = vec![];
let mut at = 0;
while let Some(s) = find(pdf, b">>\nstream\n", at) {
let start = s + 10;
let end = find(pdf, b"\nendstream", start).unwrap();
let mut text = String::new();
flate2::read::ZlibDecoder::new(&pdf[start..end])
.read_to_string(&mut text)
.unwrap_or_else(|e| panic!("fluxo inválido: {e}"));
out.push(text);
at = end;
}
out
}
#[test]
fn pdf_has_a_valid_cross_reference_table() {
let pdf = build(5);
assert!(pdf.starts_with(b"%PDF-1.4"));
assert!(pdf.ends_with(b"%%EOF\n"));
let sx = find(&pdf, b"startxref\n", 0).unwrap() + 10;
let nl = find(&pdf, b"\n", sx).unwrap();
let xref: usize = std::str::from_utf8(&pdf[sx..nl]).unwrap().parse().unwrap();
assert_eq!(&pdf[xref..xref + 4], b"xref");
// every xref entry offset points directly to `"N 0 obj"`
let table = &pdf[xref..];
let header_end = find(table, b"\n", 5).unwrap();
let count: usize = std::str::from_utf8(&table[5..header_end])
.unwrap()
.split(' ')
.nth(1)
.unwrap()
.parse()
.unwrap();
for n in 1..count {
let e = &table[header_end + 1 + n * 20..header_end + 1 + (n + 1) * 20];
let off: usize = std::str::from_utf8(&e[..10]).unwrap().parse().unwrap();
let want = format!("{n} 0 obj");
assert_eq!(&pdf[off..off + want.len()], want.as_bytes(), "objeto {n}");
}
}
#[test]
fn pdf_text_survives_accents_and_escaping() {
let all = streams(&build(5)).join("\n");
// Latin-1 accents encode as WinAnsi octal escapes; parentheses and backslashes are escaped
assert!(all.contains("Identifica\\347\\343o do caso"), "{all}");
assert!(all.contains("Perita \\(Ana\\) \\\\ Souza"));
assert!(all.contains("Laborat\\363rio"));
assert!(all.contains("fotografia_0003.jpg"));
assert!(all.contains(&"ab".repeat(32)));
assert!(all.contains(&"12".repeat(32)), "hash da trilha ausente");
// curly quotes map to WinAnsi `0x93`/`0x94` (`\223`/`\224`)
assert!(all.contains("\\223aspas\\224"));
}
#[test]
fn long_lists_paginate_with_numbered_footers() {
let pdf = build(400);
let pages = streams(&pdf);
assert!(pages.len() >= 5, "{} páginas", pages.len());
let last = pages.len();
assert!(pages[last - 1].contains(&format!("p\\341gina {last} de {last}")));
assert!(pages[0].contains(&format!("p\\341gina 1 de {last}")));
// the table header row is repeated at the top of each continuation page
assert!(pages[2].contains("Modificado \\(UTC\\)"));
// every table row appears exactly once across all pages
let all = pages.join("\n");
assert_eq!(all.matches("fotografia_0399.jpg").count(), 1);
// `arq-000.jpg` appears both in the findings table and in the recovered files section
assert_eq!(all.matches("fotografia_0000.jpg").count(), 2);
}
#[test]
fn text_wraps_instead_of_running_off_the_page() {
use guara_core::pdf::{text_width, wrap, Font};
let lines = wrap(&"a".repeat(200), 8.0, Font::Mono, 100.0);
assert!(lines.len() > 5);
assert!(lines
.iter()
.all(|l| text_width(l, 8.0, Font::Mono) <= 100.0 + 1e-6));
let words = wrap(
"uma frase comum com várias palavras curtas",
10.0,
Font::Regular,
90.0,
);
assert!(
words.len() >= 3
&& words
.iter()
.all(|l| text_width(l, 10.0, Font::Regular) <= 90.0)
);
assert_eq!(wrap("a\nb", 10.0, Font::Regular, 100.0), ["a", "b"]);
}