File: file.serialize returns the file's bytes (base64) — web Save wrote corrupt documents

file.serialize returned {json: String::from_utf8_lossy(to_bytes(doc))},
but since M6.1 the native format is a zip (deflated document.json + raw
assets), not JSON text. from_utf8_lossy turns every invalid UTF-8
sequence into U+FFFD, so the result is not the file: on the web, File ›
Save downloads exactly those bytes (download_document), and the
downloaded .designcraft can't be opened again ("invalid Zip archive").
Agents calling file.serialize got the same unusable string.

Return {base64, bytes} like the other byte-producing commands
(file.exportIdml, file.exportPdf, snippet.export) and decode it in
download_document.

Test: serialized_document_opens_again.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
storybored379andClaude Opus 5.5 authored and Brandon Thomas committed 2026-10-04 09:51:34 -04:00
1 parent 5294c8f677
commit 01753f90f3
3 files changed
+18 -2

No files matched your search

+4 -1
View File
@@ -41,7 +41,10 @@ pub fn specs() -> Vec<CommandSpec> {
let dir = recovery_dir(s, p)?;
Ok(json!({"opened": crate::recovery::open(s, &dir)?}))
}),
cmd!(query "file.serialize", "Serialize", [], None, "{} → {json}", has_doc, |s, _| Ok(json!({"json": String::from_utf8_lossy(&to_bytes(&s.doc()?.doc)).to_string()}))),
cmd!(query "file.serialize", "Serialize", [], None, "{} → {base64, bytes} the .designcraft file", has_doc, |s, _| {
let bytes = to_bytes(&s.doc()?.doc);
Ok(json!({"base64": base64_encode(&bytes), "bytes": bytes.len()}))
}),
cmd!(noundo "file.close", "Close", ["File"], Some("Cmd+W"), "{index?}", has_doc, |s, p| {
let i = p.get("index").and_then(Value::as_u64).map(|v| v as usize).or(s.active_index()).unwrap_or(0);
s.close_document(i);
+13
View File
@@ -241,6 +241,19 @@ fn hyperlinks_and_bookmarks_reach_the_pdf() {
assert_eq!(back.bookmarks.len(), 2);
}
#[test]
fn serialized_document_opens_again() {
// The web app saves (downloads) what `file.serialize` returns: it must be the file's exact bytes.
let mut s = session();
s.execute("frame.create", &json!({"rect": [36, 36, 300, 200], "content": "text", "text": "Saved on the web"})).unwrap();
let r = s.execute("file.serialize", &json!({})).unwrap();
let bytes = cmd::base64_decode(r["base64"].as_str().expect("base64"));
assert_eq!(bytes, cmd::to_bytes(&s.doc().unwrap().doc));
s.execute("file.openBytes", &json!({"name": "copy", "base64": r["base64"]})).unwrap();
let d = s.execute("document.inspect", &json!({})).unwrap();
assert_eq!(d["stories"][0]["preview"], "Saved on the web");
}
#[test]
fn step_and_repeat_grid() {
let mut s = session();
+1 -1
View File
@@ -1325,7 +1325,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
fn download_document(app: &mut DesignApp) -> Result<Value, String> {
let name = app.session.active().map(|d| format!("{}.designcraft", d.doc.title)).ok_or("no document")?;
let ser = app.run("file.serialize", json!({}))?;
let bytes = ser.get("json").and_then(Value::as_str).unwrap_or_default().as_bytes().to_vec();
let bytes = designcraft_engine::cmd::base64_decode(ser["base64"].as_str().unwrap_or_default());
if let Some(download) = app.services.download.as_mut() {
download(&name, &bytes);
}