From 01753f90f3a13b1d96f76e8d954019a4eb5f71e4 Mon Sep 17 00:00:00 2001 From: storybored379 Date: Sat, 3 Oct 2026 09:31:40 -0700 Subject: [PATCH] =?UTF-8?q?File:=20file.serialize=20returns=20the=20file's?= =?UTF-8?q?=20bytes=20(base64)=20=E2=80=94=20web=20Save=20wrote=20corrupt?= =?UTF-8?q?=20documents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/engine/src/cmd/file.rs | 5 ++++- crates/engine/src/tests.rs | 13 +++++++++++++ crates/ui-egui/src/menus.rs | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 3924035..98feab4 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -41,7 +41,10 @@ pub fn specs() -> Vec { 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); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 5cf4e74..31f6c7e 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -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(); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 0e81fb6..5996698 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1325,7 +1325,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option Result { 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); }