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:
Sazid Al Bayazid
2026-10-13 21:13:25 +06:00
parent 457406a7a5
commit e70fce996b
25 changed files with 176 additions and 103 deletions
+5 -2
View File
@@ -354,8 +354,11 @@ fn sanitized(p: &Presentation) -> std::borrow::Cow<'_, Presentation> {
}
for m in q.masters.iter_mut() {
if m.layouts.is_empty() {
let id = LayoutId(q.next_id.saturating_add(1));
q.next_id = id.0;
// An exhausted id space exports without the synthesized layout rather than with a
// duplicated one (#407).
let Some(raw) = q.next_id.max(1).checked_add(1) else { continue };
let id = LayoutId(raw);
q.next_id = raw;
Arc::make_mut(m).layouts.push(Layout {
id,
name: "Blank".into(),