mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-28 20:47:15 -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:
@@ -114,8 +114,9 @@ pub fn load(bytes: &[u8]) -> Result<Presentation> {
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// Make a loaded presentation safe to edit: a master exists, every slide's layout exists,
|
||||
/// ids are unique, tables are rectangular.
|
||||
/// Make a loaded presentation safe to edit: a master exists, every slide's layout exists, tables
|
||||
/// are rectangular, and duplicate ids are re-id'd — unless the deck has used every id, in which
|
||||
/// case a duplicate stays (growth is refused; see the comment in the loop).
|
||||
pub fn repair(p: &mut Presentation) {
|
||||
if p.masters.is_empty() {
|
||||
let fresh = defaults::blank_presentation(p.slide_size, Default::default(), false);
|
||||
@@ -128,7 +129,10 @@ pub fn repair(p: &mut Presentation) {
|
||||
let needs_layout = p.slides.get(i).is_some_and(|s| p.layout(s.layout).is_none());
|
||||
let dup = p.slides.get(i).is_some_and(|s| !seen.insert(s.id));
|
||||
if needs_layout || dup {
|
||||
let new_id = if dup { Some(deckcraft_model::SlideId(p.alloc_id())) } else { None };
|
||||
// An exhausted id space leaves a duplicate slide as it is: the engine refuses to
|
||||
// grow such a deck (edits by id stay ambiguous for the duplicate), and the duplicate
|
||||
// stays visible rather than the file failing to open (#407).
|
||||
let new_id = if dup { p.alloc_id().map(deckcraft_model::SlideId) } else { None };
|
||||
if let Some(s) = p.slides.get_mut(i) {
|
||||
let s = Arc::make_mut(s);
|
||||
if needs_layout {
|
||||
@@ -169,7 +173,8 @@ pub fn outline_to_slides(p: &mut Presentation, text: &str) -> usize {
|
||||
let line = raw.trim().trim_start_matches(['-', '*', '•']).trim();
|
||||
if level == 0 {
|
||||
flush(p, cur.take(), &mut made);
|
||||
let mut s = defaults::new_slide(p, lay);
|
||||
// An exhausted id space makes no new slides: what fit is kept (#407).
|
||||
let Some(mut s) = defaults::new_slide(p, lay) else { continue };
|
||||
if let Some(t) = s.shapes.first_mut() {
|
||||
t.text = Some(TextBody::from_text(line));
|
||||
}
|
||||
@@ -221,7 +226,7 @@ mod tests {
|
||||
#[test]
|
||||
fn roundtrip_with_media() {
|
||||
let mut p = Presentation::default();
|
||||
let id = p.add_media("a.png", "image/png", vec![1, 2, 3, 4]);
|
||||
let id = p.add_media("a.png", "image/png", vec![1, 2, 3, 4]).expect("media");
|
||||
let bytes = save(&p).unwrap();
|
||||
assert!(sniff(&bytes));
|
||||
let q = load(&bytes).unwrap();
|
||||
@@ -251,6 +256,17 @@ mod tests {
|
||||
assert_eq!(p.slide_size.width, 960.0);
|
||||
}
|
||||
|
||||
/// A duplicate slide in a deck whose ids are used up cannot be re-id'd: it stays as it is
|
||||
/// (the engine refuses edits to such a deck) instead of a duplicated fresh id (#407).
|
||||
#[test]
|
||||
fn repair_keeps_a_duplicate_it_cannot_reid() {
|
||||
let mut p = Presentation { next_id: u32::MAX, ..Default::default() };
|
||||
p.slides.push(p.slides[0].clone());
|
||||
repair(&mut p);
|
||||
assert_eq!(p.slides.len(), 2, "the duplicate stays visible");
|
||||
assert_eq!(p.slides[0].id, p.slides[1].id, "no fresh id was invented");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_roundtrip() {
|
||||
let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false);
|
||||
|
||||
Reference in New Issue
Block a user