Files
wordcraft/crates/docbin/src/tests/drawing.rs
T
Martin BallandClaude Sonnet 5.5 ed1296a1d4 #3871 follow-up: .doc picture decode charges zlib input and DIB copies; budget scales with the file (#4382)
* #3871: review — metafile input charged to the decode budget; inline BLIPs deduped; budget scales with input

- A compressed metafile is charged max(inflated size, zlib input), so a record claiming to inflate to nothing still costs its input.
- Inline pictures share one decode per BLIP record, cached by (stream, foDelay, record's own length) instead of the FBSE size field.
- The decode budget is max(512 MB, 64 x input bytes), capped at 2 GiB, so documents of many large legitimate DIBs decode; high-ratio bombs stay capped.
- Decode-budget refusals get their own log line.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2

* #3871: review — a DIB's copy into a BMP is charged to the decode budget before it is made

The charge was w*h*4 after the copy, so a 1x1 header followed by megabytes of tail cost 4 bytes per picture. It is now max(pixels, DIB length), charged first; a bitmap whose copy doesn't fit is dropped rather than copied. The doc comment on the budget scale now says RLE8/RLE4 bitmaps (about 510x) rely on the 512 MB floor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 22:37:10 -04:00

618 lines
31 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Pictures and drawings: OfficeArt records ([MS-ODRAW]) hand-built for these tests from the
//! specification, with tiny pictures generated in code. No file produced by Word is used.
use proptest::prelude::*;
use wordcraft_doc::fill::Fill;
use wordcraft_doc::para::{Anchor, InlineObject, ShapeKind, Wrap};
use wordcraft_doc::props::Rgb;
use super::{StructSpec, grpprl, plc, read, struct_doc, structured_spec};
use crate::officeart;
/// One OfficeArt record: header (recVer, recInstance, recType, recLen) + body.
fn rec(ver: u16, inst: u16, typ: u16, body: &[u8]) -> Vec<u8> {
let vi = (inst << 4) | (ver & 0xF);
[vi.to_le_bytes().as_slice(), &typ.to_le_bytes(), &(body.len() as u32).to_le_bytes(), body].concat()
}
fn container(typ: u16, kids: &[Vec<u8>]) -> Vec<u8> {
rec(0xF, 0, typ, &kids.concat())
}
/// `OfficeArtFSP`: shape type, spid, flags.
fn fsp(spt: u16, spid: u32, flags: u32) -> Vec<u8> {
rec(2, spt, 0xF00A, &[spid.to_le_bytes(), flags.to_le_bytes()].concat())
}
/// `OfficeArtFOPT` with simple properties and complex ones (data appended in order).
fn fopt(simple: &[(u16, u32)], complex: &[(u16, Vec<u8>)]) -> Vec<u8> {
let mut body = Vec::new();
for (pid, op) in simple {
body.extend_from_slice(&pid.to_le_bytes());
body.extend_from_slice(&op.to_le_bytes());
}
for (pid, data) in complex {
body.extend_from_slice(&(pid | 0x8000).to_le_bytes());
body.extend_from_slice(&(data.len() as u32).to_le_bytes());
}
for (_, data) in complex {
body.extend_from_slice(data);
}
rec(3, (simple.len() + complex.len()) as u16, 0xF00B, &body)
}
/// A shape container with the given records.
fn sp(kids: &[Vec<u8>]) -> Vec<u8> {
container(0xF004, kids)
}
/// A real 2×2 PNG.
fn tiny_png() -> Vec<u8> {
let img = image::RgbaImage::from_pixel(2, 2, image::Rgba([10, 20, 30, 255]));
let mut out = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
out
}
/// `OfficeArtBlipPNG` (one UID, tag) and the `OfficeArtFBSE` embedding it.
fn fbse_png(png: &[u8]) -> Vec<u8> {
let blip = rec(0, 0x6E0, 0xF01E, &[[0u8; 16].as_slice(), &[0xFF], png].concat());
let mut body = vec![6u8, 6];
body.extend_from_slice(&[0u8; 16]); // rgbUid
body.extend_from_slice(&0u16.to_le_bytes()); // tag
body.extend_from_slice(&(blip.len() as u32).to_le_bytes()); // size
body.extend_from_slice(&1u32.to_le_bytes()); // cRef
body.extend_from_slice(&0u32.to_le_bytes()); // foDelay
body.extend_from_slice(&[0, 0, 0, 0]); // unused1, cbName, unused2, unused3
body.extend_from_slice(&blip);
rec(2, 6, 0xF007, &body)
}
/// UTF-16LE, NUL-terminated (a complex string property).
fn wz(s: &str) -> Vec<u8> {
s.encode_utf16().chain([0]).flat_map(u16::to_le_bytes).collect()
}
/// A `Spa` (26 bytes): spid, bounds in twips, flags, cTxbx.
fn spa(spid: u32, [l, t, r, b]: [i32; 4], flags: u16) -> Vec<u8> {
[spid.to_le_bytes().as_slice(), &l.to_le_bytes(), &t.to_le_bytes(), &r.to_le_bytes(), &b.to_le_bytes(), &flags.to_le_bytes(), &0u32.to_le_bytes()]
.concat()
}
/// `Spa` flags from bx, by, wr and fBelowText.
fn spa_flags(bx: u16, by: u16, wr: u16, below: bool) -> u16 {
(bx << 1) | (by << 3) | (wr << 5) | (u16::from(below) << 14)
}
/// `OfficeArtContent`: a DggContainer holding a BLIP store, then one main-document drawing.
fn art_content(fbses: &[Vec<u8>], shapes: &[Vec<u8>]) -> Vec<u8> {
let bstore = rec(0xF, fbses.len() as u16, 0xF001, &fbses.concat());
let dgg = container(0xF000, &[bstore]);
let patriarch = sp(&[fsp(0, 1024, 0b101)]);
let spgr = container(0xF003, &[[patriarch].as_slice(), shapes].concat());
let dg = container(0xF002, &[rec(0, 1, 0xF008, &[0u8; 8]), spgr]);
[dgg, vec![0u8], dg].concat()
}
/// A PICF for an inline OfficeArt picture: `MM_SHAPE`, 1 × 0.5 inch goal size at 100 %.
fn picf_shape(rest: &[u8]) -> Vec<u8> {
let mut picf = vec![0u8; 0x44];
picf[0..4].copy_from_slice(&((0x44 + rest.len()) as u32).to_le_bytes());
picf[4..6].copy_from_slice(&0x44u16.to_le_bytes());
picf[6..8].copy_from_slice(&0x0064u16.to_le_bytes()); // mfpf.mm = MM_SHAPE
picf[28..30].copy_from_slice(&1440i16.to_le_bytes());
picf[30..32].copy_from_slice(&720i16.to_le_bytes());
picf[32..34].copy_from_slice(&1000u16.to_le_bytes());
picf[34..36].copy_from_slice(&1000u16.to_le_bytes());
[picf.as_slice(), rest].concat()
}
#[test]
fn inline_officeart_picture_with_crop_and_alt_text() {
let png = tiny_png();
// A picture frame whose pib names the first FBSE that follows it, a quarter cropped
// off the left, with alt text.
let shape = sp(&[fsp(75, 1025, 0xA00), fopt(&[(0x0102, 0x4000), (0x0104, 1)], &[(0x0381, wz("Logo"))])]);
let data = picf_shape(&[shape, fbse_png(&png)].concat());
let text = "Pic\u{1}!\r";
let ccp = text.chars().count() as u32;
let spec = StructSpec { text, data_stream: data, chpx_runs: vec![(0, grpprl(&[(0x6A03, &0u32.to_le_bytes())]))], ..structured_spec(text, ccp) };
let doc = read(&struct_doc(&spec)).expect("opens");
let p0 = doc.body.first().and_then(|b| b.as_para()).expect("para");
assert_eq!(p0.text, "Pic\u{FFFC}!");
let Some(InlineObject::Image { media, w, h, alt, crop, float, .. }) = p0.objects.first() else { panic!("no image: {:?}", p0.objects) };
assert_eq!((*w, *h), (54.0, 36.0)); // 72 pt less a quarter, × 36 pt
assert_eq!(*crop, [0.25, 0.0, 0.0, 0.0]);
assert_eq!(alt, "Logo");
assert_eq!(float.wrap, Wrap::Inline);
assert_eq!(doc.media.get(media).map(|b| b.as_slice()), Some(png.as_slice()));
}
#[test]
fn floating_picture_from_the_blip_store_keeps_position_and_wrap() {
let png = tiny_png();
let pic = sp(&[fsp(75, 1025, 0xA00), fopt(&[(0x0104, 1)], &[])]);
let art = art_content(&[fbse_png(&png)], &[pic]);
// Two inches by one, 1" from the page's left edge and 0.5" below the top margin; square wrap.
let spas = plc(&[1, 3], &spa(1025, [1440, 720, 4320, 2160], spa_flags(1, 0, 2, false)));
let text = "A\u{8}B\r";
let ccp = text.chars().count() as u32;
let spec = StructSpec { text, extra_pairs: vec![(40, spas), (50, art)], ..structured_spec(text, ccp) };
let doc = read(&struct_doc(&spec)).expect("opens");
let p0 = doc.body.first().and_then(|b| b.as_para()).expect("para");
assert_eq!(p0.text, "A\u{FFFC}B");
let Some(InlineObject::Image { media, w, h, float, .. }) = p0.objects.first() else { panic!("no image: {:?}", p0.objects) };
assert_eq!((*w, *h), (144.0, 72.0));
assert_eq!((float.wrap, float.h_rel, float.v_rel, float.x, float.y), (Wrap::Square, Anchor::Page, Anchor::Margin, 72.0, 36.0));
assert_eq!(doc.media.get(media).map(|b| b.as_slice()), Some(png.as_slice()));
}
#[test]
fn shapes_keep_kind_colours_wrap_and_text_box_text() {
// A red ellipse with a 2 pt blue outline behind the text, and an unfilled text box.
let ellipse = sp(&[fsp(3, 1026, 0xA00), fopt(&[(0x0181, 0x0000_00FF), (0x01C0, 0x00FF_0000), (0x01CB, 25_400)], &[])]);
let tb = sp(&[fsp(202, 1027, 0xA00), fopt(&[(0x01BF, 1 << 20)], &[]), rec(0, 0, 0xF00D, &0x0001_0000u32.to_le_bytes())]);
let art = art_content(&[], &[ellipse, tb]);
let spas =
plc(&[1, 2, 4], &[spa(1026, [0, 0, 1440, 1440], spa_flags(2, 2, 3, true)), spa(1027, [0, 0, 2880, 720], spa_flags(0, 0, 1, false))].concat());
// The text box story: an FTXBXS whose lid is the shape's spid (cTxbx, cReusable, fReusable,
// reserved, lid, txidUndo).
let ftxbxs =
[1i32.to_le_bytes().as_slice(), &0i32.to_le_bytes(), &0i16.to_le_bytes(), &0i32.to_le_bytes(), &1027u32.to_le_bytes(), &0i32.to_le_bytes()]
.concat();
let txbx = plc(&[0, 7], &ftxbxs);
let text = "A\u{8}\u{8}B\r";
let ccp = text.chars().count() as u32;
// The text box story is stored as the subdocument right after the main text; the endnote
// slot of the fixture writes it there and the FIB is then patched to call it the text box
// subdocument (ccpEdn = 0, ccpTxbx = 7).
let spec =
StructSpec { text, endnote_stories: vec!["Inside\r"], extra_pairs: vec![(40, spas), (50, art), (56, txbx)], ..structured_spec(text, ccp) };
let file = patch_word(&struct_doc(&spec), &[(0x60, 0), (0x64, 7)]);
let doc = read(&file).expect("opens");
let p0 = doc.body.first().and_then(|b| b.as_para()).expect("para");
assert_eq!(p0.objects.len(), 2, "{:?}", p0.objects);
let Some(InlineObject::Shape { kind, w, h, fill, stroke, stroke_width, float, .. }) = p0.objects.first() else { panic!("{:?}", p0.objects) };
assert_eq!((*kind, *w, *h), (ShapeKind::Ellipse, 72.0, 72.0));
assert_eq!((fill.clone(), *stroke, *stroke_width), (Some(Fill::solid(Rgb(255, 0, 0))), Some(Rgb(0, 0, 255)), 2.0));
assert_eq!((float.wrap, float.h_rel, float.v_rel), (Wrap::BehindText, Anchor::Column, Anchor::Paragraph));
let Some(InlineObject::Shape { kind, fill, float, story, .. }) = p0.objects.get(1) else { panic!("{:?}", p0.objects) };
assert_eq!((*kind, fill.clone(), float.wrap), (ShapeKind::TextBox, None, Wrap::TopAndBottom));
let part = story.and_then(|id| doc.parts.get(&id)).expect("text box story part");
assert_eq!(part.kind, wordcraft_doc::PartKind::TextBox);
assert_eq!(part.blocks.first().and_then(|b| b.as_para()).map(|p| p.text.as_str()), Some("Inside"));
}
#[test]
fn text_boxes_pointing_at_the_same_story_text_read_it_once() {
// Two text boxes whose PlcftxbxTxt stories are both [0, 7) (the table steps back to CP 0
// in between): each was read into its own part, so a hostile table could copy one story
// thousands of times. The second story is dropped and its box stays empty.
let tb = |spid: u32| sp(&[fsp(202, spid, 0xA00), fopt(&[(0x01BF, 1 << 20)], &[]), rec(0, 0, 0xF00D, &0x0001_0000u32.to_le_bytes())]);
let art = art_content(&[], &[tb(1027), tb(1028)]);
let flags = spa_flags(0, 0, 1, false);
let spas = plc(&[1, 2, 4], &[spa(1027, [0, 0, 2880, 720], flags), spa(1028, [0, 0, 2880, 720], flags)].concat());
// FTXBXS: cTxbx, cReusable, fReusable, reserved, lid, txidUndo.
let ftxbxs = |lid: u32| {
[1i32.to_le_bytes().as_slice(), &0i32.to_le_bytes(), &0i16.to_le_bytes(), &0i32.to_le_bytes(), &lid.to_le_bytes(), &0i32.to_le_bytes()]
.concat()
};
let txbx = plc(&[0, 7, 0, 7], &[ftxbxs(1027), ftxbxs(1), ftxbxs(1028)].concat());
let text = "A\u{8}\u{8}B\r";
let ccp = text.chars().count() as u32;
// As above: the endnote slot holds the text box subdocument, then the FIB is patched.
let spec =
StructSpec { text, endnote_stories: vec!["Inside\r"], extra_pairs: vec![(40, spas), (50, art), (56, txbx)], ..structured_spec(text, ccp) };
let doc = read(&patch_word(&struct_doc(&spec), &[(0x60, 0), (0x64, 7)])).expect("opens");
let p0 = doc.body.first().and_then(|b| b.as_para()).expect("para");
let stories: Vec<Option<u32>> =
p0.objects.iter().filter_map(|o| if let InlineObject::Shape { story, .. } = o { Some(*story) } else { None }).collect();
let [Some(first), None] = stories[..] else { panic!("stories: {stories:?}") };
let part = doc.parts.get(&first).expect("text box story part");
assert_eq!(part.blocks.first().and_then(|b| b.as_para()).map(|p| p.text.as_str()), Some("Inside"));
assert_eq!(doc.parts.len(), 1);
}
/// Overwrite little-endian u32s in the WordDocument stream of a compound file.
fn patch_word(file: &[u8], at: &[(usize, u32)]) -> Vec<u8> {
use std::io::Read;
let mut comp = cfb::CompoundFile::open(std::io::Cursor::new(file.to_vec())).unwrap();
let mut streams = Vec::new();
for name in ["WordDocument", "0Table", "Data"] {
if let Ok(mut s) = comp.open_stream(format!("/{name}")) {
let mut v = Vec::new();
s.read_to_end(&mut v).unwrap();
if name == "WordDocument" {
for (o, x) in at {
v[*o..*o + 4].copy_from_slice(&x.to_le_bytes());
}
}
streams.push((name, v));
}
}
let refs: Vec<(&str, &[u8])> = streams.iter().map(|(n, v)| (*n, v.as_slice())).collect();
super::cfb_file(&refs)
}
#[test]
fn dib_becomes_png_and_compressed_metafiles_inflate() {
// A 2×1 24-bit DIB (BITMAPINFOHEADER, rows padded to 4 bytes).
let mut dib = Vec::new();
for v in [40u32, 2, 1] {
dib.extend_from_slice(&v.to_le_bytes());
}
dib.extend_from_slice(&1u16.to_le_bytes());
dib.extend_from_slice(&24u16.to_le_bytes());
dib.extend_from_slice(&[0u8; 24]);
dib.extend_from_slice(&[0, 0, 255, 0, 255, 0, 0, 0]);
let r = rec(0, 0x7A8, 0xF01F, &[[0u8; 16].as_slice(), &[0xFF], &dib].concat());
let (dib_rec, _) = officeart::record(&r).unwrap();
let blip = officeart::blip(&dib_rec, 1 << 20, &mut officeart::Decode::new(usize::MAX)).expect("DIB decodes");
assert_eq!(blip.ext, "png");
let img = image::load_from_memory(&blip.bytes).unwrap().to_rgb8();
assert_eq!((img.width(), img.height(), img.get_pixel(0, 0).0), (2, 1, [255, 0, 0]));
// A deflated WMF stays a WMF (the metafile renderer draws it).
let wmf: Vec<u8> = [1u16, 9, 0x300].iter().flat_map(|v| v.to_le_bytes()).chain([0u8; 12]).collect();
let packed = miniz_oxide::deflate::compress_to_vec_zlib(&wmf, 6);
let mut hdr = Vec::new();
hdr.extend_from_slice(&(wmf.len() as u32).to_le_bytes());
hdr.extend_from_slice(&[0u8; 24]); // rcBounds, ptSize
hdr.extend_from_slice(&(packed.len() as u32).to_le_bytes());
hdr.extend_from_slice(&[0x00, 0xFE]); // compression = deflate, filter
let r = rec(0, 0x216, 0xF01B, &[[0u8; 16].as_slice(), &hdr, &packed].concat());
let (wmf_rec, _) = officeart::record(&r).unwrap();
let blip = officeart::blip(&wmf_rec, 1 << 20, &mut officeart::Decode::new(usize::MAX)).expect("WMF inflates");
assert_eq!((blip.ext, blip.bytes), ("wmf", wmf));
}
#[test]
fn deeply_nested_groups_and_lying_lengths_are_bounded() {
// 2000 nested group containers: the walk stops at its depth cap (no stack overflow).
let mut g = sp(&[fsp(1, 2000, 0)]);
for _ in 0..2_000 {
g = container(0xF003, &[sp(&[fsp(0, 2001, 1)]), g]);
}
let _ = officeart::content(&art_content(&[], &[g]));
// A metafile claiming 4 GB uncompressed with garbage data, and an FBSE pointing far out.
let mut hdr = vec![0xFFu8; 4];
hdr.extend_from_slice(&[0u8; 24]);
hdr.extend_from_slice(&u32::MAX.to_le_bytes());
hdr.extend_from_slice(&[0, 0]);
let r = rec(0, 0x3D4, 0xF01A, &[[0u8; 16].as_slice(), &hdr, &[0x78, 0x9C, 1, 2, 3]].concat());
let (emf, _) = officeart::record(&r).unwrap();
assert!(officeart::blip(&emf, 1 << 20, &mut officeart::Decode::new(usize::MAX)).is_none());
let mut fbse = vec![0u8; 36];
fbse[28..32].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(officeart::fbse_blip(&fbse, &[0u8; 64], 1 << 20, &mut officeart::Decode::new(usize::MAX)).is_none());
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn junk_drawings_never_panic(art in proptest::collection::vec(any::<u8>(), 0..2048), spas in proptest::collection::vec(any::<u8>(), 0..512), data in proptest::collection::vec(any::<u8>(), 0..1024)) {
let text = "A\u{8}\u{1}\u{8}B\r";
let ccp = text.chars().count() as u32;
let picf = picf_shape(&data);
let spec = StructSpec {
text,
data_stream: picf,
chpx_runs: vec![(0, grpprl(&[(0x6A03, &0u32.to_le_bytes())]))],
extra_pairs: vec![(40, spas), (50, art.clone()), (56, art)],
..structured_spec(text, ccp)
};
let _ = read(&struct_doc(&spec));
}
}
// A 1×1 24-bit DIB, including its padded four-byte row.
fn one_pixel_dib() -> Vec<u8> {
let mut dib = [40u32, 1, 1].iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<_>>();
dib.extend_from_slice(&1u16.to_le_bytes());
dib.extend_from_slice(&24u16.to_le_bytes());
dib.extend_from_slice(&[0u8; 24]);
dib.extend_from_slice(&[30, 60, 90, 0]);
dib
}
#[test]
fn dib_conversion_caps_header_and_encoded_bytes() {
let dib = one_pixel_dib();
let record = rec(0, 0x7A8, officeart::rt::BLIP_DIB, &[[0u8; 16].as_slice(), &[0xFF], &dib].concat());
let (record, _) = officeart::record(&record).unwrap();
let bmp_size = 14 + dib.len();
assert!(officeart::blip(&record, bmp_size - 1, &mut officeart::Decode::new(usize::MAX)).is_none(), "the BMP header also needs room");
let fallback = officeart::blip(&record, bmp_size, &mut officeart::Decode::new(usize::MAX)).expect("the BMP fallback fits exactly");
assert_eq!((fallback.ext, fallback.placeholder, fallback.bytes.len()), ("bmp", true, bmp_size));
let img = image::load_from_memory(&fallback.bytes).unwrap().to_rgb8();
assert_eq!((img.width(), img.height(), img.get_pixel(0, 0).0), (1, 1, [90, 60, 30]));
let normal = officeart::blip(&record, 512, &mut officeart::Decode::new(usize::MAX)).expect("normal conversion fits");
assert_eq!((normal.ext, normal.placeholder), ("png", false));
assert!(normal.bytes.len() > bmp_size && normal.bytes.len() <= 512);
assert_eq!(image::load_from_memory(&normal.bytes).unwrap().width(), 1);
}
/// #3709: every anchor of one inline picture got its own copy of the alt text (up to 4096
/// UTF-16 units). Copies are charged to the document's work budget; past it alt is empty.
#[test]
fn alt_text_copies_across_many_anchors_are_bounded() {
let alt: String = "€".repeat(4096); // 12 KB as UTF-8
let shape = sp(&[fsp(75, 1025, 0xA00), fopt(&[(0x0104, 1)], &[(0x0381, wz(&alt))])]);
let data = picf_shape(&[shape, fbse_png(&tiny_png())].concat());
let text: &'static str = Box::leak(format!("{}\r", "\u{1}".repeat(8000)).into_boxed_str());
let ccp = text.chars().count() as u32;
let spec = StructSpec { text, data_stream: data, chpx_runs: vec![(0, grpprl(&[(0x6A03, &0u32.to_le_bytes())]))], ..structured_spec(text, ccp) };
let doc = read(&struct_doc(&spec)).expect("opens");
let p0 = doc.body.first().and_then(|b| b.as_para()).expect("para");
let alts: Vec<&str> = p0.objects.iter().filter_map(|o| if let InlineObject::Image { alt, .. } = o { Some(alt.as_str()) } else { None }).collect();
assert_eq!(alts.len(), 8000, "every anchor keeps its picture");
assert_eq!(alts.first().copied(), Some(alt.as_str()));
let total: usize = alts.iter().map(|a| a.len()).sum();
assert!(total <= 64 << 20, "alt bytes {total}");
assert_eq!(alts.last().copied(), Some(""));
// Leaving it out is logged, once (#3871 review).
assert_eq!(crate::LAST_DROPPED.with(|d| d.borrow().clone()), [crate::Cost::AltText]);
}
/// #3710: every PlcSpa entry naming a group built another copy of all its members. A shape is
/// anchored once; repeated spids are skipped.
#[test]
fn a_group_named_by_many_anchors_is_built_once() {
let rect = |l: i32| rec(0, 0, 0xF00F, &[l, 0, l + 10, 10].iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<_>>());
let head = sp(&[fsp(0, 3000, 0x201), rec(1, 0, 0xF009, &[0i32, 0, 10_000, 10].iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<_>>())]);
let members: Vec<Vec<u8>> = (0..1000).map(|i| sp(&[fsp(1, 4000 + i as u32, 0xA02), rect(i * 10)])).collect();
let group = container(0xF003, &[vec![head], members].concat());
let art = art_content(&[], &[group]);
let n = 5000u32;
let cps: Vec<u32> = (1..=n + 1).collect();
let spas: Vec<u8> = (0..n).flat_map(|_| spa(3000, [0, 0, 1440, 1440], spa_flags(0, 0, 3, false))).collect();
let text = "A\u{8}\u{8}\u{8}B\r";
let ccp = text.chars().count() as u32;
let spec = StructSpec { text, extra_pairs: vec![(40, plc(&cps, &spas)), (50, art)], ..structured_spec(text, ccp) };
let t = std::time::Instant::now();
let doc = read(&struct_doc(&spec)).expect("opens");
// About 10 ms on a loaded machine, so 10 s is a backstop; the old code (a copy per entry,
// 6-8 s here) is caught by the group count below, one group per anchor ([1000; 3]).
assert!(t.elapsed().as_secs() < 10, "opened in {:?}", t.elapsed());
let p0 = doc.body.first().and_then(|b| b.as_para()).expect("para");
let groups: Vec<usize> =
p0.objects.iter().filter_map(|o| if let InlineObject::Group { children, .. } = o { Some(children.len()) } else { None }).collect();
assert_eq!(groups, [1000]);
}
/// #3710: a floating picture's copy is charged its alt text too, and a group every member's.
#[test]
fn a_floating_copy_is_charged_its_alt_text() {
let image = |alt: String| InlineObject::Image {
media: String::new(),
w: 1.0,
h: 1.0,
alt,
float: Default::default(),
crop: Default::default(),
ole: None,
};
let bare = crate::object_cost(&image(String::new()));
assert_eq!(crate::object_cost(&image("€".repeat(4096))), bare + 3 * 4096);
let member = |obj| wordcraft_doc::para::GroupChild { x: 0.0, y: 0.0, obj };
let group = InlineObject::Group {
w: 1.0,
h: 1.0,
float: Default::default(),
ch_w: 1.0,
ch_h: 1.0,
children: vec![member(image("x".repeat(100))), member(image(String::new()))],
canvas: None,
};
assert_eq!(crate::object_cost(&group), 3 * bare + 100);
}
/// A deflated WMF BLIP record declaring `size` uncompressed bytes.
fn packed_wmf(size: u32, packed: &[u8]) -> Vec<u8> {
let mut hdr = size.to_le_bytes().to_vec();
hdr.extend_from_slice(&[0u8; 24]); // rcBounds, ptSize
hdr.extend_from_slice(&(packed.len() as u32).to_le_bytes());
hdr.extend_from_slice(&[0x00, 0xFE]); // compression = deflate, filter
rec(0, 0x216, officeart::rt::BLIP_WMF, &[[0u8; 16].as_slice(), &hdr, packed].concat())
}
/// An `OfficeArtFBSE` body whose BLIP (of `size` bytes) sits at `fo_delay` in the delay stream.
fn delayed_fbse(size: usize, fo_delay: usize) -> Vec<u8> {
let mut fbse = vec![0u8; 36];
fbse[20..24].copy_from_slice(&(size as u32).to_le_bytes());
fbse[28..32].copy_from_slice(&(fo_delay as u32).to_le_bytes());
fbse
}
/// #3711: store entries aliasing one delay-stream metafile each inflated (and copied) it again.
/// Now the record is decoded once and every entry shares its media key.
#[test]
fn store_entries_aliasing_one_delayed_metafile_decode_it_once() {
let wmf = vec![0u8; 1 << 20];
let delay = packed_wmf(wmf.len() as u32, &miniz_oxide::deflate::compress_to_vec_zlib(&wmf, 6));
let fbse = delayed_fbse(delay.len(), 0);
let store: Vec<&[u8]> = vec![&fbse; 5_000];
let mut pics = crate::media::Pictures::default();
pics.work = officeart::Decode::new(8 << 20);
let keys: Vec<_> = (1..=store.len() as u32).map(|pib| pics.bse(pib, &store, &delay)).collect();
assert!(keys.iter().all(|k| k.is_some() && *k == keys[0]), "every entry shares one picture");
assert_eq!(pics.work.left, (8 << 20) - wmf.len(), "inflated once");
}
/// #3711: distinct metafiles that fail to inflate are charged their declared size anyway, so
/// a document of them stops decoding once the work budget is spent.
#[test]
fn failing_metafile_inflations_spend_the_decode_budget() {
let records: Vec<Vec<u8>> = (0..10u8).map(|i| packed_wmf(1 << 20, &[0x78, 0x9C, i, 1, 2, 3])).collect();
let delay = records.concat();
let mut at = 0;
let fbses: Vec<Vec<u8>> = records
.iter()
.map(|r| {
at += r.len();
delayed_fbse(r.len(), at - r.len())
})
.collect();
let store: Vec<&[u8]> = fbses.iter().map(Vec::as_slice).collect();
let mut pics = crate::media::Pictures::default();
pics.work = officeart::Decode::new(3 << 20);
for pib in 1..=store.len() as u32 {
assert!(pics.bse(pib, &store, &delay).is_none());
}
assert_eq!(pics.work.left, 0, "three attempts spent the budget; the rest weren't tried");
}
/// #3712: a DIB's declared pixels are charged before decoding (an RLE bitmap is tiny on disk but
/// decodes to w × h × 4 bytes); past the budget it stays a BMP placeholder, undecoded.
#[test]
fn dib_decoding_is_charged_its_declared_pixels() {
let blip_of = |dib: &[u8]| rec(0, 0x7A8, officeart::rt::BLIP_DIB, &[[0u8; 16].as_slice(), &[0xFF], dib].concat());
let one = blip_of(&one_pixel_dib());
let (one, _) = officeart::record(&one).unwrap();
let mut work = officeart::Decode::new(3);
assert!(officeart::blip(&one, 512, &mut work).is_none(), "not even the copy fits: nothing is copied");
assert_eq!(work.left, 3, "nothing decoded, nothing charged");
let dib_len = one.body.len() - 17;
let mut work = officeart::Decode::new(dib_len);
assert_eq!(officeart::blip(&one, 512, &mut work).map(|b| (b.ext, b.placeholder)), Some(("png", false)));
assert_eq!(work.left, 0, "the copy is charged, not just 4 bytes a pixel");
// 8-bit RLE declaring 16384 × 16384 (1 GiB decoded) in a few bytes.
let mut big = [40u32, 16_384, 16_384].iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<_>>();
big.extend_from_slice(&1u16.to_le_bytes());
big.extend_from_slice(&8u16.to_le_bytes());
big.extend_from_slice(&1u32.to_le_bytes()); // BI_RLE8
big.extend_from_slice(&[0u8; 12]);
big.extend_from_slice(&1u32.to_le_bytes()); // one palette colour
big.extend_from_slice(&[0u8; 4]); // clrImportant
big.extend_from_slice(&[0u8; 4]); // the palette colour
big.extend_from_slice(&[0, 0, 0, 1]); // end of line, end of bitmap
let big = blip_of(&big);
let (big, _) = officeart::record(&big).unwrap();
let mut work = officeart::Decode::new(512 << 20);
assert_eq!(officeart::blip(&big, 1 << 20, &mut work).map(|b| (b.ext, b.placeholder)), Some(("bmp", true)));
assert_eq!(work.left, (512 << 20) - (big.body.len() - 17), "only the copy of the DIB was charged, not its declared pixels");
}
/// An EMF `OfficeArtBlip` record (compressed) with `raw` as its stream, claiming `cb_size`.
fn emf_blip(cb_size: u32, raw: &[u8]) -> Vec<u8> {
let mut hdr = vec![0u8; 34];
hdr[0..4].copy_from_slice(&cb_size.to_le_bytes());
hdr[28..32].copy_from_slice(&(raw.len() as u32).to_le_bytes());
hdr[32] = 0; // compressed
hdr[33] = 0xFE;
rec(0, 0x3D4, 0xF01A, &[[0u8; 16].as_slice(), &hdr, raw].concat())
}
/// #3871 review: the zlib input is charged to the decode budget, not just what it inflates to.
#[test]
fn compressed_metafile_input_is_charged_to_the_decode_budget() {
let raw = vec![0x5Au8; 1 << 20]; // not even zlib: costs its input all the same
let r = emf_blip(1, &raw);
let (emf, _) = officeart::record(&r).unwrap();
let mut work = officeart::Decode::new(10 << 20);
assert!(officeart::blip(&emf, 64 << 20, &mut work).is_none());
assert_eq!(work.left, (10 << 20) - raw.len(), "1 MB of input charged, however little it claims to inflate to");
// Ten more of them exhaust the budget; none is inflated past it.
let mut work = officeart::Decode::new(3 << 20);
for _ in 0..10 {
let _ = officeart::blip(&emf, 64 << 20, &mut work);
}
assert_eq!(work.left, 0, "three attempts fit, the rest are refused");
}
/// An inline `PICFAndOfficeArtData` (shape, FBSE) whose BLIP sits at `fo_delay` in Data; the
/// FBSE claims `size` bytes.
fn inline_pic(fo_delay: u32, size: u32) -> Vec<u8> {
let mut fbse = vec![0u8; 36];
fbse[20..24].copy_from_slice(&size.to_le_bytes());
fbse[28..32].copy_from_slice(&fo_delay.to_le_bytes());
let body = [container(0xF004, &[]), rec(2, 6, 0xF007, &fbse)].concat();
let mut picf = vec![0u8; 0x44];
picf[0..4].copy_from_slice(&((0x44 + body.len()) as u32).to_le_bytes());
picf[4..6].copy_from_slice(&0x44u16.to_le_bytes());
picf[6..8].copy_from_slice(&0x64u16.to_le_bytes()); // MM_SHAPE
picf[28..30].copy_from_slice(&1440i16.to_le_bytes());
picf[30..32].copy_from_slice(&1440i16.to_le_bytes());
[picf, body].concat()
}
/// #3871 review: inline pictures that alias one BLIP record share one decode, whatever sizes
/// their FBSEs claim and wherever their PICFs sit.
#[test]
fn inline_pictures_aliasing_one_blip_record_are_decoded_once() {
let blip = emf_blip(1, &vec![0x5Au8; 64 << 10]);
let a = inline_pic(0, 70_000);
let b = inline_pic(0, 80_000);
let c = inline_pic(0, 90_000);
let blip_at = (a.len() + b.len() + c.len()) as u32;
let (a, b, c) = (inline_pic(blip_at, 70_000), inline_pic(blip_at, 80_000), inline_pic(blip_at, 90_000));
let data = [a.clone(), b.clone(), c, blip].concat();
let mut pics = crate::media::Pictures::default();
let start = pics.work.left;
let _ = pics.get(&data, 0);
let _ = pics.get(&data, a.len() as u32);
let _ = pics.get(&data, (a.len() + b.len()) as u32);
assert_eq!(start - pics.work.left, 64 << 10, "one record, one charge");
}
/// A 1-bit-per-pixel DIB of `side`×`side` pixels with `bytes` of pixel data.
fn dib_1bpp(side: u32, bytes: usize) -> Vec<u8> {
let mut d = vec![0u8; 40];
d[0..4].copy_from_slice(&40u32.to_le_bytes());
d[4..8].copy_from_slice(&side.to_le_bytes());
d[8..12].copy_from_slice(&side.to_le_bytes());
d[12..14].copy_from_slice(&1u16.to_le_bytes());
d[14..16].copy_from_slice(&1u16.to_le_bytes());
d.extend_from_slice(&[0, 0, 0, 0, 255, 255, 255, 0]); // palette
d.extend(std::iter::repeat_n(0u8, bytes));
d
}
fn dib_record(dib: &[u8]) -> Vec<u8> {
rec(0, 0x7A8, 0xF01F, &[[0u8; 16].as_slice(), &[0xFF], dib].concat())
}
/// #3871 review: the decode budget scales with the input, so a document of many legitimate
/// large scans decodes them all; a high-ratio set is still capped.
#[test]
fn the_decode_budget_scales_with_the_input_but_still_caps_bombs() {
let floor = 1 << 20;
// 20 legitimate 1024² 1-bit scans: 4 MB charged each, 128 KB of file each.
let real = dib_record(&dib_1bpp(1024, 1024 * 1024 / 8));
let (rec_real, _) = officeart::record(&real).unwrap();
let input = 20 * real.len();
let mut work = officeart::Decode::new(crate::media::decode_work(input, floor));
for i in 0..20 {
let b = officeart::blip(&rec_real, 64 << 20, &mut work).unwrap();
assert!(!b.placeholder, "scan {i} decodes");
}
// Declared 20000² (1.6 GB each) in a few dozen bytes each: a placeholder, never decoded.
let bomb = dib_record(&dib_1bpp(20_000, 16));
let (rec_bomb, _) = officeart::record(&bomb).unwrap();
let mut work = officeart::Decode::new(crate::media::decode_work(20 * bomb.len(), floor));
for _ in 0..20 {
assert!(officeart::blip(&rec_bomb, 64 << 20, &mut work).unwrap().placeholder);
}
assert!(work.left <= crate::media::decode_work(20 * bomb.len(), floor), "nothing was refunded or invented");
}
/// #3871 review: the copy of a DIB into a BMP is charged before it is made, however few pixels
/// the header declares; with the budget spent nothing is copied at all.
#[test]
fn a_dib_with_a_large_tail_is_charged_for_its_copy() {
let big = dib_record(&dib_1bpp(1, 1 << 20)); // 1 pixel declared, 1 MB of trailing bytes
let (big, _) = officeart::record(&big).unwrap();
let dib_len = big.body.len() - 17;
let mut work = officeart::Decode::new(10 << 20);
assert!(!officeart::blip(&big, 64 << 20, &mut work).unwrap().placeholder);
assert_eq!(10 << 20, work.left + dib_len, "charged for the bytes, not just 4 per pixel");
// Room for the copy: decoded. Not even that: nothing is copied.
let mut work = officeart::Decode::new(dib_len + 1);
assert!(officeart::blip(&big, 64 << 20, &mut work).is_some());
let mut work = officeart::Decode::new(dib_len - 1);
assert!(officeart::blip(&big, 64 << 20, &mut work).is_none());
assert_eq!(work.left, dib_len - 1);
}