mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-31 14:07:06 -06:00
Model: an exhausted id space refuses growth instead of duplicating ids (#407)
alloc_id bumped next_id with saturating_add: a presentation whose next_id reached u32::MAX (a hostile file can arrive that way; fix_next_id imports it) handed out that same id to every new slide, shape, layout and media - duplicate ids, ambiguous references, silent corruption on every later insert. alloc_id now returns Option<u32> via checked_add, and every caller refuses: the engine's insert/duplicate/media/layout commands answer with a clear error naming the command, paste and duplicate re-id helpers propagate it, media addition is fallible, and a fresh slide is Option. The pptx export's synthesized blank layout is guarded the same way. Loading keeps such a deck viewable: repair re-ids duplicate slides while it can, and a duplicate it cannot re-id stays as it is - growth is refused, and only that duplicate keeps its ambiguous id. Renumbering the loaded graph was rejected: it needs the cross-document reference remapping that issue #406 tracks. Tests: model exhaustion (alloc_id None, fresh media refused, a new slide refused), engine (slide.new, shape.insert, slide.duplicate and edit.duplicate all refuse; the document holds no duplicate ids), format repair (an unre-id-able duplicate stays visible).
This commit is contained in:
@@ -835,3 +835,28 @@ fn slide_ranges_parse_like_powerpoints_print_box() {
|
||||
let opts = cmd::file::pdf_options(&json!({"slides": [0, 7, 1]}), 2).unwrap();
|
||||
assert_eq!(opts.slides, Some(vec![0, 1]), "indices past the end are dropped");
|
||||
}
|
||||
|
||||
/// A deck whose ids are used up (a hostile file can arrive with next_id at the top of the
|
||||
/// range) refuses to grow instead of handing out duplicate ids (#407).
|
||||
#[test]
|
||||
fn an_exhausted_id_space_refuses_growth() {
|
||||
let mut s = session();
|
||||
s.execute("slide.new", &json!({})).unwrap();
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).next_id = u32::MAX;
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
assert!(s.execute("slide.new", &json!({})).is_err(), "a new slide is refused");
|
||||
assert!(s.execute("shape.insert", &json!({"preset": "rect", "rect": [10, 10, 100, 50]})).is_err());
|
||||
assert!(s.execute("slide.duplicate", &json!({})).is_err());
|
||||
assert!(s.execute("edit.duplicate", &json!({})).is_err());
|
||||
// What the deck holds is untouched: same slides, same ids, no duplicates.
|
||||
let after = s.doc().unwrap().doc.clone();
|
||||
assert_eq!(after.slides.len(), before.slides.len());
|
||||
let mut ids = vec![];
|
||||
deckcraft_model::walk(&after.slides[0].shapes, &mut |sh, _| ids.push(sh.id.0));
|
||||
ids.push(after.slides[0].id.0);
|
||||
ids.push(after.next_id);
|
||||
let mut sorted = ids.clone();
|
||||
sorted.sort_unstable();
|
||||
sorted.dedup();
|
||||
assert_eq!(sorted.len(), ids.len(), "no id repeats");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user