diff --git a/crates/engine/src/cmd/design.rs b/crates/engine/src/cmd/design.rs index ddae3f58..599e084c 100644 --- a/crates/engine/src/cmd/design.rs +++ b/crates/engine/src/cmd/design.rs @@ -152,7 +152,7 @@ fn browse_theme(s: &mut Session, p: &Value) -> Result { #[cfg(not(target_arch = "wasm32"))] { let path = str_param(p, "path").ok_or_else(|| bad("design.browseTheme", "give `path` or `data`"))?; - std::fs::read(path).map_err(|e| crate::EngineError::Other(format!("{path}: {e}")))? + crate::io::read_file(path).map_err(crate::EngineError::Other)? } #[cfg(target_arch = "wasm32")] return Err(bad("design.browseTheme", "give `data`")); diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 8bcb5c5c..4765da60 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -476,7 +476,7 @@ fn info(s: &mut Session, _p: &Value) -> Result { let st = s.doc()?; let doc = &st.doc; #[cfg(not(target_arch = "wasm32"))] - let bytes = st.path.as_ref().and_then(|p| std::fs::metadata(p).ok()).map(|m| m.len()); + let bytes = st.path.as_deref().and_then(crate::io::file_len); #[cfg(target_arch = "wasm32")] let bytes: Option = None; Ok(json!({ diff --git a/crates/engine/src/file_access.rs b/crates/engine/src/file_access.rs index d5ee0a99..bcda8492 100644 --- a/crates/engine/src/file_access.rs +++ b/crates/engine/src/file_access.rs @@ -460,7 +460,11 @@ mod tests { for path in files { let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); // `io` and this module are the checked gateway; `tests*.rs` are test-only modules. - if path.parent() == Some(src.as_path()) && matches!(name, "io.rs" | "file_access.rs" | "recovery.rs") || name.starts_with("tests") { + // `recovery` and `spell` touch only the app's own folders (AutoRecover files, the + // system and user spelling dictionaries), never a path a command names. + if path.parent() == Some(src.as_path()) && matches!(name, "io.rs" | "file_access.rs" | "recovery.rs" | "spell.rs") + || name.starts_with("tests") + { continue; } let text = std::fs::read_to_string(&path).unwrap(); diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index 0ae00c3e..aaba8d42 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -33,3 +33,16 @@ pub fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> { pub fn write_file(path: &str, _bytes: &[u8]) -> Result<(), String> { Err(format!("{path}: saving to a path is not available in the browser")) } + +/// The size in bytes of the file at `path`; `None` when it can't be read (or is outside the +/// automation read root, so File › Info doesn't reveal files the agent may not read). +#[cfg(not(target_arch = "wasm32"))] +pub fn file_len(path: &str) -> Option { + crate::file_access::check_read(path).ok()?; + std::fs::metadata(path).ok().map(|m| m.len()) +} + +#[cfg(target_arch = "wasm32")] +pub fn file_len(_path: &str) -> Option { + None +} diff --git a/crates/mcp/tests/automation_roots.rs b/crates/mcp/tests/automation_roots.rs index f8595ff9..356c3704 100644 --- a/crates/mcp/tests/automation_roots.rs +++ b/crates/mcp/tests/automation_roots.rs @@ -134,7 +134,7 @@ fn every_writing_command_is_refused_outside_the_write_root() { refused(&mut server, "export", json!({"path": outside("x.png")}), "outside the write root"); refused(&mut server, "export", json!({"path": outside("x.txt")}), "outside the write root"); refused(&mut server, "export", json!({"path": outside("all.png"), "all": true}), "outside the write root"); - refused(&mut server, "render_slide", json!({"path": outside("r.png")}), "outside the write root"); + refused(&mut server, "call_app", json!({"method": "ui.render", "params": {"path": outside("r.png")}}), "outside the write root"); refused(&mut server, "call_app", json!({"method": "app.save", "params": {"path": outside("y.deckcraft")}}), "outside the write root"); refused(&mut server, "call_app", json!({"method": "app.export", "params": {"path": outside("y.pdf")}}), "outside the write root"); for command in ["file.save", "file.saveAs", "file.export"] { @@ -162,7 +162,7 @@ fn every_writing_command_is_refused_outside_the_write_root() { ok(&mut server, "export", json!({"path": inside("x.pptx")})); ok(&mut server, "export", json!({"path": inside("x.pdf")})); ok(&mut server, "export", json!({"path": inside("all.png"), "all": true})); - ok(&mut server, "render_slide", json!({"path": inside("r.png")})); + ok(&mut server, "call_app", json!({"method": "ui.render", "params": {"path": inside("r.png")}})); for f in ["x.deckcraft", "x.pptx", "x.pdf", "all1.png", "all2.png", "r.png"] { assert!(base.join("out").join(f).exists(), "{f}"); } @@ -176,7 +176,7 @@ fn one_root_grants_only_its_own_access() { ok(&mut read_only, "open_presentation", json!({"path": s(&base.join("in/a.txt"))})); refused(&mut read_only, "save_presentation", json!({"path": s(&base.join("out/x.deckcraft"))}), "write authority is absent"); refused(&mut read_only, "export", json!({"path": s(&base.join("out/x.pdf"))}), "write authority is absent"); - refused(&mut read_only, "render_slide", json!({"path": s(&base.join("out/r.png"))}), "write authority is absent"); + refused(&mut read_only, "call_app", json!({"method": "ui.render", "params": {"path": s(&base.join("out/r.png"))}}), "write authority is absent"); // Calls that return bytes instead of writing a path keep working. let (error, _) = call(&mut read_only, "render_slide", json!({"scale": 0.25})); assert!(!error);