Files
deckcraft/crates/pptx/tests/sniff.rs
b48870989c PPTX: recognise packages whose [Content_Types].xml is not in the first 4 KB (#19)
`sniff` searched only the leading 4096 bytes for the content-types part, but
OOXML writers may place that part anywhere in the archive. Recent PowerPoint
builds (16.x) store it as the last zip entry, so every deck saved by them was
rejected with "not a presentation DeckCraft can read" although it imports fine.
In one real folder 169 of 300 decks were affected.

Look the part up in the zip's central directory instead. The regression test
re-packs the sample deck with the part last behind 64 KB of padding and
checks that it is both recognised and imported.

Co-authored-by: Matthias Weber <webermatt@me.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 10:28:45 -04:00

59 lines
2.6 KiB
Rust

//! `sniff` must recognise a package wherever the writer put `[Content_Types].xml`: recent
//! PowerPoint builds store it as the last zip entry, well past the first few kilobytes.
use std::io::{Cursor, Read, Write};
use deckcraft_engine::Session;
use zip::write::SimpleFileOptions;
fn sample_pptx() -> Vec<u8> {
let mut s = Session::new();
deckcraft_engine::sample::open_sample(&mut s).expect("sample");
let p = s.doc().expect("doc").doc.as_ref().clone();
deckcraft_pptx::export(&p).expect("export")
}
/// Re-pack `bytes` with `[Content_Types].xml` moved to the end and a padding part in front, so
/// the content-types name first appears far beyond any fixed-size prefix.
fn content_types_last(bytes: &[u8]) -> Vec<u8> {
let mut src = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");
let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));
let stored = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
out.start_file("ppt/media/padding.bin", stored).expect("start");
out.write_all(&vec![0u8; 64 * 1024]).expect("padding");
let names: Vec<String> = (0..src.len()).map(|i| src.by_index(i).expect("entry").name().to_string()).collect();
for name in names.iter().filter(|n| *n != "[Content_Types].xml").chain(std::iter::once(&"[Content_Types].xml".to_string())) {
let mut data = Vec::new();
src.by_name(name).expect("by_name").read_to_end(&mut data).expect("read");
out.start_file(name, SimpleFileOptions::default()).expect("start");
out.write_all(&data).expect("write");
}
out.finish().expect("finish").into_inner()
}
#[test]
fn sniff_accepts_content_types_anywhere_in_the_archive() {
let bytes = sample_pptx();
assert!(deckcraft_pptx::sniff(&bytes));
let moved = content_types_last(&bytes);
let first_4k = &moved[..4096.min(moved.len())];
assert!(!first_4k.windows(19).any(|w| w == b"[Content_Types].xml"), "test setup: name must not be in the prefix");
assert!(deckcraft_pptx::sniff(&moved));
let p = deckcraft_pptx::import(&moved).expect("import");
assert!(!p.slides.is_empty());
}
#[test]
fn sniff_rejects_non_packages() {
assert!(!deckcraft_pptx::sniff(b""));
assert!(!deckcraft_pptx::sniff(b"PK"));
assert!(!deckcraft_pptx::sniff(b"%PDF-1.7 [Content_Types].xml"));
let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));
out.start_file("hello.txt", SimpleFileOptions::default()).expect("start");
out.write_all(b"not an office package").expect("write");
let plain_zip = out.finish().expect("finish").into_inner();
assert!(!deckcraft_pptx::sniff(&plain_zip));
}