mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-25 18:47:42 -06:00
`lightcraft_raw::sparse::header_plan` says which byte ranges of a raw file the header probe (`probe_info`, `lightcraft_meta::extract`) reads, given a buffer of the file's length in which only some ranges are loaded: everything but the bulk sample data (large TIFF strips/tiles minus the first chunk's coding headers, a CR3's image tracks minus the raw track's CRX headers and QP maps, a RAF's CFA strip, an RW2's raw data). `preview_plan` also reads what `embedded_preview` needs, for raws shown from their preview. NEF, ORF and PEF (their header probe looks at samples) and anything else are read whole. The corpus test checks, for every raw.pixls.us sample the plan describes, that probing the partly loaded buffer gives exactly what the whole file gives, with the skipped bytes zero and again 0xFF: CR3 2.7 %, DNG 2.4 %, CR2 3 %, RAF 6 %, RW2 7 %, ARW 8 % of their bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
5.3 KiB
Rust
109 lines
5.3 KiB
Rust
//! [`lightcraft_raw::sparse::header_plan`] over `corpus/raw/**` (git-ignored CC0 samples from
|
|
//! raw.pixls.us, fetched with `cargo xtask corpus --download`; `LIGHTCRAFT_CORPUS` overrides the
|
|
//! corpus root). Skips cleanly when absent.
|
|
//!
|
|
//! For every file the plan reaches `Ready` on, probing the partly loaded buffer must give exactly
|
|
//! what probing the whole file gives — with the skipped bytes zero and again with them 0xFF, so a
|
|
//! reader that looked at skipped data would be caught. CR3 and DNG files must be described from a
|
|
//! small part of the file (`cargo test -p lightcraft-raw --release --test sparse_corpus -- --nocapture`
|
|
//! prints what each format reads).
|
|
|
|
use lightcraft_raw::sparse::{HeaderPlan, Ranges, header_plan, preview_plan};
|
|
use lightcraft_raw::{embedded_preview, probe, probe_info};
|
|
use std::collections::BTreeMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn corpus_root() -> PathBuf {
|
|
std::env::var_os("LIGHTCRAFT_CORPUS").map(PathBuf::from).unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"))
|
|
}
|
|
|
|
/// Run the plan (with the embedded preview when `preview`), loading from `file` what it asks for
|
|
/// into a buffer whose other bytes are `fill`.
|
|
fn run(file: &[u8], fill: u8, preview: bool) -> (HeaderPlan, Vec<u8>, Ranges) {
|
|
let mut buf = vec![fill; file.len()];
|
|
let mut loaded = Ranges::new();
|
|
let prefix = file.len().min(512 * 1024);
|
|
buf[..prefix].copy_from_slice(&file[..prefix]);
|
|
loaded.insert(0, prefix as u64);
|
|
for _ in 0..32 {
|
|
match if preview { preview_plan(&buf, &loaded) } else { header_plan(&buf, &loaded) } {
|
|
HeaderPlan::Need(r) => {
|
|
for &(a, b) in r.spans() {
|
|
assert!(!loaded.overlaps(a, b), "asked again for {a}..{b}");
|
|
buf[a as usize..b as usize].copy_from_slice(&file[a as usize..b as usize]);
|
|
loaded.insert(a, b);
|
|
}
|
|
}
|
|
done => return (done, buf, loaded),
|
|
}
|
|
}
|
|
panic!("no answer after 32 rounds");
|
|
}
|
|
|
|
/// What an import learns from a file: the raw description and the metadata.
|
|
fn describe(bytes: &[u8]) -> String {
|
|
let e = lightcraft_meta::embedded(bytes);
|
|
format!("{:?}\n{:?}\n{:?}", probe_info(bytes), lightcraft_meta::extract(bytes), e.xmp)
|
|
}
|
|
|
|
#[test]
|
|
fn partial_reads_describe_raws_as_whole_files_do() {
|
|
let dir = corpus_root().join("raw");
|
|
let mut paths: Vec<PathBuf> = std::fs::read_dir(corpus_root().join("leica"))
|
|
.into_iter()
|
|
.chain(std::fs::read_dir(&dir))
|
|
.flatten()
|
|
.flatten()
|
|
.map(|e| e.path())
|
|
.filter(|p| p.is_file() && !p.extension().is_some_and(|e| e == "part" || e == "txt" || e == "md"))
|
|
.collect();
|
|
if paths.is_empty() {
|
|
eprintln!("skip: {} absent", dir.display());
|
|
return;
|
|
}
|
|
paths.sort();
|
|
// format → (files ready, bytes read, bytes total, files read whole)
|
|
let mut stats: BTreeMap<String, (usize, u64, u64, usize)> = BTreeMap::new();
|
|
for p in paths {
|
|
let name = p.file_name().unwrap().to_string_lossy().to_string();
|
|
let file = std::fs::read(&p).unwrap();
|
|
let format = probe(&file).map_or("?".to_string(), |f| format!("{f:?}"));
|
|
let (plan, zeros, loaded) = run(&file, 0, false);
|
|
let entry = stats.entry(format.clone()).or_default();
|
|
match plan {
|
|
HeaderPlan::Ready => {
|
|
let (plan_ff, ones, loaded_ff) = run(&file, 0xff, false);
|
|
assert_eq!((plan_ff, &loaded_ff), (HeaderPlan::Ready, &loaded), "{name}: the plan read skipped bytes");
|
|
let whole = describe(&file);
|
|
assert_eq!(describe(&zeros), whole, "{name}: differs from the whole file (skipped bytes 0)");
|
|
assert_eq!(describe(&ones), whole, "{name}: differs from the whole file (skipped bytes 0xFF)");
|
|
entry.0 += 1;
|
|
entry.1 += loaded.total();
|
|
entry.2 += file.len() as u64;
|
|
// with the preview (for a raw shown from it), the preview is what the whole file gives too
|
|
let preview = embedded_preview(&file);
|
|
for fill in [0, 0xff] {
|
|
let (plan, buf, _) = run(&file, fill, true);
|
|
assert_eq!(plan, HeaderPlan::Ready, "{name}: preview plan");
|
|
assert_eq!(describe(&buf), whole, "{name}: preview plan, skipped bytes {fill}");
|
|
assert!(embedded_preview(&buf) == preview, "{name}: a different embedded preview (skipped bytes {fill})");
|
|
}
|
|
if loaded.total() * 4 > file.len() as u64 {
|
|
eprintln!("{name}: read {:.0} %", loaded.total() as f64 * 100.0 / file.len() as f64);
|
|
}
|
|
if matches!(format.as_str(), "Cr3" | "Dng") && file.len() > 8 << 20 {
|
|
assert!(loaded.total() * 4 < file.len() as u64, "{name}: read {} of {} bytes", loaded.total(), file.len());
|
|
}
|
|
}
|
|
HeaderPlan::Whole(why) => {
|
|
entry.3 += 1;
|
|
eprintln!("{name}: whole ({why})");
|
|
}
|
|
HeaderPlan::Need(_) => unreachable!(),
|
|
}
|
|
}
|
|
for (format, (ready, read, total, whole)) in stats {
|
|
eprintln!("{format:>10}: {ready} described from {:.1} % of their bytes, {whole} read whole", read as f64 * 100.0 / total.max(1) as f64);
|
|
}
|
|
}
|