mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-29 13:06:56 -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:
@@ -104,9 +104,9 @@ fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm {
|
||||
Xfrm::new((size.width - w) / 2.0, (size.height - h) / 2.0, w, h)
|
||||
}
|
||||
|
||||
pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Result<ShapeId> {
|
||||
pub(crate) fn add_shape(s: &mut Session, cmd: &str, mut shape: Shape, select: bool) -> Result<ShapeId> {
|
||||
s.edit(|doc, sel| {
|
||||
shape.id = ShapeId(doc.alloc_id());
|
||||
shape.id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
|
||||
if shape.name.is_empty() {
|
||||
let base = match &shape.kind {
|
||||
ShapeKind::Picture { .. } => "Picture".to_string(),
|
||||
@@ -176,7 +176,7 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let (Some(look), false) = (&s.default_look, line_like) {
|
||||
super::format::apply_painted(&mut shape, look);
|
||||
}
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "shape.insert", shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ fn text_box(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
t.body.wrap = Some(false);
|
||||
}
|
||||
}
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.textBox", shape, true)?;
|
||||
super::text::fit_text_box(s, id)?;
|
||||
if text.is_empty() {
|
||||
// Start typing immediately.
|
||||
@@ -341,7 +341,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
_ => [0.0; 4],
|
||||
};
|
||||
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
|
||||
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("insert.picture", NO_ID_SPACE)))?;
|
||||
let shape = Shape {
|
||||
xfrm: Some(rect),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media, crop, ..Default::default() } },
|
||||
@@ -368,7 +368,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})?;
|
||||
return Ok(json!({"id": ph_id}));
|
||||
}
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.picture", shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ fn table(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Table(t), ..Default::default() }, true)?;
|
||||
let id = add_shape(s, "insert.table", Shape { xfrm: Some(rect), kind: ShapeKind::Table(t), ..Default::default() }, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -446,7 +446,7 @@ fn chart(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let size = s.doc()?.doc.slide_size;
|
||||
let rect = rect_param(p, "rect").unwrap_or(Xfrm::new(size.width * 0.15, size.height * 0.15, size.width * 0.7, size.height * 0.7));
|
||||
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }, true)?;
|
||||
let id = add_shape(s, "insert.chart", Shape { xfrm: Some(rect), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -498,12 +498,15 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result<Value> {
|
||||
});
|
||||
let data = std::sync::Arc::try_unwrap(bytes).unwrap_or_else(|a| (*a).clone());
|
||||
let (media, poster) = s.edit(|doc, _| {
|
||||
let media = doc.add_media(&name, ct, data);
|
||||
let poster = poster_png.map(|png| doc.add_media(&format!("{name}-poster.png"), "image/png", png));
|
||||
let media = doc.add_media(&name, ct, data).ok_or_else(|| bad("insert.media", NO_ID_SPACE))?;
|
||||
let poster = match poster_png {
|
||||
Some(png) => Some(doc.add_media(&format!("{name}-poster.png"), "image/png", png).ok_or_else(|| bad("insert.media", NO_ID_SPACE))?),
|
||||
None => None,
|
||||
};
|
||||
Ok((media, poster))
|
||||
})?;
|
||||
let clip = MediaClip { media, video, poster, volume: 1.0, duration_ms, width, height, ..Default::default() };
|
||||
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Media(clip), descr: name, ..Default::default() }, true)?;
|
||||
let id = add_shape(s, "insert.media", Shape { xfrm: Some(rect), kind: ShapeKind::Media(clip), descr: name, ..Default::default() }, true)?;
|
||||
if let Some(w) = &warning {
|
||||
s.ui_requests.push(crate::UiRequest::Message { text: w.clone() });
|
||||
}
|
||||
@@ -545,7 +548,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.wordArt", shape, true)?;
|
||||
super::text::fit_text_box(s, id)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
@@ -624,7 +627,7 @@ fn action_button(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
click: action.map(|a| Hyperlink { action: a, tooltip: String::new(), highlight_click: true }),
|
||||
..Default::default()
|
||||
};
|
||||
let id = add_shape(s, shape, true)?;
|
||||
let id = add_shape(s, "insert.actionButton", shape, true)?;
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
@@ -790,7 +793,7 @@ fn smart_art(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
sh.text = Some(body);
|
||||
ids.push(add_shape(s, sh, false)?.0);
|
||||
ids.push(add_shape(s, "insert.smartArt", sh, false)?.0);
|
||||
}
|
||||
let g = s.execute("arrange.group", &json!({"ids": ids}))?;
|
||||
if let Some(gid) = g.get("id").and_then(Value::as_u64) {
|
||||
|
||||
Reference in New Issue
Block a user