mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
Merge pull request #7264 from dotCooCoo/fix/confined-svg-export-script-paths
MCP: SVG export by a confined caller refuses script links outside its read roots
This commit is contained in:
@@ -123,7 +123,7 @@ mod formats;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use access::list_dir;
|
||||
pub use access::{write_file_with, write_save_file, write_save_file_with};
|
||||
pub use access::{check_linked_scripts, write_file_with, write_save_file, write_save_file_with};
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests_readonly;
|
||||
|
||||
@@ -121,6 +121,24 @@ pub(crate) fn naming_in_message(msg: &str, path: &str, name: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// The error for an SVG of a document that links a JavaScript file outside the read roots.
|
||||
pub(crate) const SCRIPT_OUTSIDE: &str = "a linked JavaScript file is outside the read roots";
|
||||
|
||||
/// `cmd` may write `st`'s document in format `format` (a format id): any format but SVG and SVGZ,
|
||||
/// and those too unless read roots are in force and the document links a JavaScript file outside
|
||||
/// them (`svg_scripts`, a relative path taken from the document's folder; a URL is not checked),
|
||||
/// which the file's `<script>` element would name. The error names no file or folder.
|
||||
pub fn check_linked_scripts(cmd: &str, format: &str, st: &crate::DocState) -> Result<()> {
|
||||
if !matches!(format, "svg" | "svgz") || crate::file_access::current().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let doc_path = st.path.as_deref();
|
||||
if st.doc.svg_scripts.iter().any(|f| crate::cmd::links::script_hidden(f, doc_path)) {
|
||||
return Err(bad(cmd, SCRIPT_OUTSIDE));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `path` checked for writing against the automation roots in force.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn may_write(path: &str) -> Result<()> {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,7 +12,8 @@ use crate::DocState;
|
||||
use super::super::{Result, Session, bad, check_switches, edit, number_or, switch_or, text_param};
|
||||
use super::encode::single_artboard;
|
||||
use super::{
|
||||
ARTBOARD_PARAMS, Format, default_name, encode_all, encode_with_warnings, merge, with_black_output, writable, write_encoded, write_or_return,
|
||||
ARTBOARD_PARAMS, Format, check_linked_scripts, default_name, encode_all, encode_with_warnings, merge, with_black_output, writable, write_encoded,
|
||||
write_or_return,
|
||||
};
|
||||
|
||||
/// The keys the export commands take for every format, beside the path, the format and the
|
||||
@@ -53,6 +54,7 @@ pub fn unused_option_notes(cmd: &str, f: &Format, p: &Value) -> Vec<String> {
|
||||
|
||||
pub(super) fn serialize(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let f = writable("document.serialize", Some(text_param(p, "format", "document.serialize")?.unwrap_or("vectorcraft")), None)?;
|
||||
check_linked_scripts("document.serialize", f.id, s.doc()?)?;
|
||||
let unused = unused_option_notes("document.serialize", f, p);
|
||||
let expanded = expand_presets(s, "document.serialize", p)?;
|
||||
let p = with_black_output(s, &expanded);
|
||||
@@ -96,6 +98,7 @@ pub(super) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
super::check_not_lossy_overwrite(s.doc()?, path, p, "document.export")?;
|
||||
}
|
||||
let f = writable("document.export", text_param(p, "format", "document.export")?, path)?;
|
||||
check_linked_scripts("document.export", f.id, s.doc()?)?;
|
||||
let unused = unused_option_notes("document.export", f, p);
|
||||
let expanded = expand_presets(s, "document.export", p)?;
|
||||
let p = with_black_output(s, &expanded);
|
||||
@@ -355,6 +358,7 @@ pub(super) fn export_selection(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
super::check_not_lossy_overwrite(s.doc()?, path, p, C)?;
|
||||
}
|
||||
let f = writable(C, text_param(p, "format", C)?, path)?;
|
||||
check_linked_scripts(C, f.id, s.doc()?)?;
|
||||
let unused = unused_option_notes(C, f, p);
|
||||
let expanded = expand_presets(s, C, p)?;
|
||||
let p = with_black_output(s, &expanded);
|
||||
|
||||
@@ -822,6 +822,7 @@ pub(crate) fn job_for(s: &Session, st: &DocState, plan: SavePlan) -> Result<Save
|
||||
|
||||
fn job_for_stamped(s: &Session, st: &DocState, plan: SavePlan, stamped: Option<i64>) -> Result<SaveJob> {
|
||||
let cmd = plan.mode.command();
|
||||
super::check_linked_scripts(cmd, plan.format.id, st)?;
|
||||
let mut own = doc_to_save(st, plan.format);
|
||||
// The file the document is saved as (else its own) titles it if New's name is all it has.
|
||||
let file = plan.path.as_deref().filter(|_| plan.retargets()).or(st.path.as_deref());
|
||||
@@ -1238,7 +1239,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
"Save Document",
|
||||
[],
|
||||
None,
|
||||
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file and EPS (below): a .ai file is a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS takes the options of document.exportEps: by default one file whose bounding box fits the visible art (the first artboard when no art shows), carrying the whole editable document; useArtboards: true (also when artboard, artboards or range names artboards; file.formatOptions then reports it true) makes the chosen artboard the page, drawn with its background color (without Use Artboards, only the first artboard's page when no art shows has its color), and with two or more chosen the file at the path is the central file of the visible art (again the first artboard when none shows) and each artboard is also written to <name>_<artboard>.eps beside it, holding that artboard; selectedOnly is refused; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. Under automation read roots, a reply leaves out the document's own file outside them when no path is given (files and linked then name the files by their file names), and a suggested folder outside them. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused. A terminal filesystem permission/read-only refusal returns READ_ONLY_LOCATION: followed by the reason; recovery-copy failures keep their original error. Non-UI callers never open a save panel",
|
||||
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file and EPS (below): a .ai file is a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS takes the options of document.exportEps: by default one file whose bounding box fits the visible art (the first artboard when no art shows), carrying the whole editable document; useArtboards: true (also when artboard, artboards or range names artboards; file.formatOptions then reports it true) makes the chosen artboard the page, drawn with its background color (without Use Artboards, only the first artboard's page when no art shows has its color), and with two or more chosen the file at the path is the central file of the visible art (again the first artboard when none shows) and each artboard is also written to <name>_<artboard>.eps beside it, holding that artboard; selectedOnly is refused; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. Under automation read roots, a reply leaves out the document's own file outside them when no path is given (files and linked then name the files by their file names), and a suggested folder outside them; an svg or svgz save (a document whose own format is SVG or SVGZ included) is refused, before any file is written, while the document links a JavaScript file outside them (svgInteractivity; a URL is not checked), also by file.saveAs and file.saveCopy. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused. A terminal filesystem permission/read-only refusal returns READ_ONLY_LOCATION: followed by the reason; recovery-copy failures keep their original error. Non-UI callers never open a save panel",
|
||||
has_doc,
|
||||
|s, p| save(s, SaveMode::Save, p)
|
||||
),
|
||||
|
||||
@@ -1030,6 +1030,10 @@ pub(crate) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
o.one_source()?;
|
||||
// `<filename>`: the document's file, as Save names it, or its title before it is saved.
|
||||
let st = s.doc()?;
|
||||
// Before the folder is made or any row is written.
|
||||
for r in &rows {
|
||||
super::check_linked_scripts(C, r.format.id, st)?;
|
||||
}
|
||||
let naming = Naming::of(&o, st.path.as_deref().unwrap_or(&st.doc.title))?;
|
||||
let selection = o.selection_mode()?;
|
||||
let selected = if selection.is_some() { selected_artwork(s)? } else { vec![] };
|
||||
|
||||
@@ -253,3 +253,91 @@ fn events_and_files_survive_a_native_round_trip_and_reach_svg() {
|
||||
assert!(svg.contains(" onclick=\"show("menu") && x < 1\""), "{svg}");
|
||||
assert!(svg.contains("<script xlink:href=\"menu.js\"/>"), "{svg}");
|
||||
}
|
||||
|
||||
/// A caller confined to read roots: every SVG and SVGZ output of a document that links a
|
||||
/// JavaScript file outside them is refused before a file or folder is written, with an error that
|
||||
/// names no file or folder, and the document keeps the link.
|
||||
#[test]
|
||||
fn confined_svg_output_refuses_a_linked_javascript_file_outside_the_read_roots() {
|
||||
let away = crate::tests_links::Folder::new("svg-script-away");
|
||||
let home = crate::tests_links::Folder::new("svg-script-home");
|
||||
let script = away.file("menu.js");
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0);
|
||||
run(&mut s, "svgInteractivity.addFile", json!({"file": script}));
|
||||
run(&mut s, "assets.add", json!({"ids": [a.0]}));
|
||||
let roots = crate::file_access::AutomationRoots::new(&[&home.0], &[&home.0]).unwrap().unwrap();
|
||||
let hidden = [away.0.to_string_lossy().into_owned(), home.0.to_string_lossy().into_owned(), "menu.js".to_string()];
|
||||
let refused = |s: &mut Session, cmd: &str, p: Value| {
|
||||
let e = crate::file_access::confine(Some(&roots), || s.execute(cmd, &p)).expect_err(cmd).to_string();
|
||||
assert!(e.contains("a linked JavaScript file is outside the read roots"), "{cmd} {p}: {e}");
|
||||
for h in &hidden {
|
||||
assert!(!e.contains(h.as_str()), "{cmd} {p} names {h}: {e}");
|
||||
}
|
||||
};
|
||||
let out = home.file("out");
|
||||
for format in ["svg", "svgz"] {
|
||||
let file = |name: &str| home.file(&format!("{name}.{format}"));
|
||||
refused(&mut s, "document.serialize", json!({"format": format}));
|
||||
refused(&mut s, "document.export", json!({"format": format}));
|
||||
refused(&mut s, "document.export", json!({"format": format, "path": file("export")}));
|
||||
refused(&mut s, "document.export", json!({"format": format, "selectedOnly": true}));
|
||||
refused(&mut s, "document.exportSelection", json!({"format": format, "path": file("selection")}));
|
||||
refused(&mut s, "document.save", json!({"path": file("save")}));
|
||||
refused(&mut s, "file.saveAs", json!({"format": format, "path": file("save-as")}));
|
||||
refused(&mut s, "file.saveCopy", json!({"format": format, "path": file("copy")}));
|
||||
refused(&mut s, "document.exportForScreens", json!({"folder": out, "formats": [{"format": "png"}, {"format": format}]}));
|
||||
refused(&mut s, "document.exportForScreens", json!({"zip": true, "formats": [{"format": format}]}));
|
||||
refused(&mut s, "assets.export", json!({"folder": out, "formats": [{"format": format}]}));
|
||||
}
|
||||
// A document saved as SVG saves as SVG again without a path: its file is left as it was.
|
||||
let own = home.file("own.svg");
|
||||
run(&mut s, "file.saveAs", json!({"path": own}));
|
||||
let before = std::fs::read(&own).unwrap();
|
||||
rect(&mut s, 50.0);
|
||||
refused(&mut s, "document.save", json!({}));
|
||||
assert_eq!(std::fs::read(&own).unwrap(), before);
|
||||
let written: Vec<_> = std::fs::read_dir(&home.0).unwrap().map(|e| e.unwrap().file_name()).collect();
|
||||
assert_eq!(written, ["own.svg"]);
|
||||
assert_eq!(s.doc().unwrap().doc.svg_scripts, [script]);
|
||||
}
|
||||
|
||||
/// A confined caller's SVG keeps every linked JavaScript file it may read, as stored: a file inside
|
||||
/// the read roots, a path relative to the document's folder that leads inside them, and a URL. A
|
||||
/// caller without roots writes a file outside them too.
|
||||
#[test]
|
||||
fn svg_output_keeps_linked_javascript_files_a_caller_may_read() {
|
||||
let away = crate::tests_links::Folder::new("svg-script-kept-away");
|
||||
let home = crate::tests_links::Folder::new("svg-script-kept-home");
|
||||
let inside = home.file("menu.js");
|
||||
let url = "https://example.com/scripts/menu.js";
|
||||
let mut s = session();
|
||||
rect(&mut s, 10.0);
|
||||
for file in [inside.as_str(), "lib/tools.js", url] {
|
||||
run(&mut s, "svgInteractivity.addFile", json!({"file": file}));
|
||||
}
|
||||
run(&mut s, "file.saveAs", json!({"path": home.file("doc.vectorcraft")}));
|
||||
let roots = crate::file_access::AutomationRoots::new(&[&home.0], &[&home.0]).unwrap().unwrap();
|
||||
let svg = |s: &mut Session, format: &str| {
|
||||
let r = crate::file_access::confine(Some(&roots), || s.execute("document.serialize", &json!({"format": format}))).unwrap();
|
||||
match r["text"].as_str() {
|
||||
Some(text) => text.to_string(),
|
||||
None => {
|
||||
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
|
||||
assert!(vectorcraft_svg::is_svgz(&bytes), "gzip magic");
|
||||
vectorcraft_svg::text_of(&bytes).unwrap().into_owned()
|
||||
}
|
||||
}
|
||||
};
|
||||
for format in ["svg", "svgz"] {
|
||||
let text = svg(&mut s, format);
|
||||
for file in [inside.as_str(), "lib/tools.js", url] {
|
||||
assert!(text.contains(&format!("<script xlink:href=\"{file}\"/>")), "{format} {file}: {text}");
|
||||
}
|
||||
}
|
||||
// Without roots, a file outside them is written as stored.
|
||||
let outside = away.file("menu.js");
|
||||
run(&mut s, "svgInteractivity.addFile", json!({"file": outside}));
|
||||
let text = run(&mut s, "document.serialize", json!({"format": "svg"}))["text"].as_str().unwrap().to_string();
|
||||
assert!(text.contains(&format!("<script xlink:href=\"{outside}\"/>")), "{text}");
|
||||
}
|
||||
|
||||
@@ -1330,7 +1330,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"file.exportAs",
|
||||
"Export As…",
|
||||
"",
|
||||
"{format?, useArtboards?, range?} opens Export As (then the format's options); with {path, …document.export options} writes the file(s) → {path, warnings, files?}",
|
||||
"{format?, useArtboards?, range?} opens Export As (then the format's options); with {path, …document.export options} writes the file(s) → {path, warnings, files?}; under automation read roots, an SVG or SVGZ is refused while the document links a JavaScript file outside them, as document.export refuses it",
|
||||
),
|
||||
(
|
||||
"ui.fileInfoDialog",
|
||||
|
||||
@@ -272,12 +272,16 @@ fn code_view(app: &VectorcraftApp, ui: &mut egui::Ui, d: &Dialog) {
|
||||
}
|
||||
// One cached text, replaced when the document or the options change.
|
||||
let key = egui::Id::unique("svg-code");
|
||||
let sig = egui::Id::unique((st.uid, st.revision, opts.to_string())).value();
|
||||
// The read roots in force and the document's folder decide whether the text may be shown.
|
||||
let roots = vectorcraft_engine::file_access::current().map(|r| r.describe());
|
||||
let sig = egui::Id::unique((st.uid, st.revision, opts.to_string(), roots, st.path.as_deref())).value();
|
||||
let code: Arc<String> = match ui.ctx().data(|m| m.get_temp::<(u64, Arc<String>)>(key)).filter(|(s, _)| *s == sig) {
|
||||
Some((_, c)) => c,
|
||||
None => {
|
||||
const MAX: usize = 200_000;
|
||||
let mut text = match vectorcraft_engine::cmd::fileio::encode_all(&st.doc, "svg", &json!({ "svg": opts })) {
|
||||
let encoded = vectorcraft_engine::cmd::fileio::check_linked_scripts("document.export", "svg", st)
|
||||
.and_then(|()| vectorcraft_engine::cmd::fileio::encode_all(&st.doc, "svg", &json!({ "svg": opts })));
|
||||
let mut text = match encoded {
|
||||
// Shown as text whatever the file's encoding.
|
||||
Ok(enc) => enc
|
||||
.files
|
||||
@@ -383,6 +387,38 @@ mod tests {
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().f64("decimals", 0.0), 1.0);
|
||||
}
|
||||
|
||||
/// Show Code under read roots draws no SVG of a document that links a JavaScript file outside
|
||||
/// them: it shows the error, which names no file or folder. Without roots it shows the link.
|
||||
#[test]
|
||||
fn show_code_leaves_out_a_linked_javascript_file_outside_the_read_roots() {
|
||||
let tmp = std::env::temp_dir();
|
||||
let root = tmp.join(format!("vc-svg-code-root-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let script = tmp.join(format!("vc-svg-code-away-{}", std::process::id())).join("menu.js").to_string_lossy().into_owned();
|
||||
let (mut app, _) = app("/tmp/code.svg");
|
||||
app.run("svgInteractivity.addFile", json!({"file": script})).unwrap();
|
||||
app.run("file.export.svg", Value::Null).unwrap();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("showCode".into(), json!(true));
|
||||
let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap().unwrap();
|
||||
// The text the code view shows after two frames in one context, with `roots` in force.
|
||||
let code = |app: &mut VectorcraftApp, roots: Option<&Arc<vectorcraft_engine::file_access::AutomationRoots>>| {
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
for _ in 0..2 {
|
||||
let mut out =
|
||||
vectorcraft_engine::file_access::confine(roots, || ctx.run_ui(Default::default(), |ui| super::super::show(app, ui.ctx())));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
ctx.data(|m| m.get_temp::<(u64, Arc<String>)>(egui::Id::unique("svg-code"))).map(|(_, c)| c.to_string()).unwrap_or_default()
|
||||
};
|
||||
let confined = code(&mut app, Some(&roots));
|
||||
assert!(confined.contains("a linked JavaScript file is outside the read roots"), "{confined}");
|
||||
assert!(!confined.contains("menu.js") && !confined.contains("vc-svg-code"), "{confined}");
|
||||
let shown = code(&mut app, None);
|
||||
assert!(shown.contains(&format!("<script xlink:href=\"{script}\"/>")), "{shown}");
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
/// A dialog a save panel opened carries the dialog layer's own `__` fields; the save is given
|
||||
/// only the SVG options, so it is not refused for an unknown key.
|
||||
#[test]
|
||||
|
||||
@@ -41,6 +41,7 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option<Strin
|
||||
// A `.ai` file names the view's active artboard.
|
||||
remember_active_artboard(app);
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
fileio::check_linked_scripts("file.exportAs", f.id, st).map_err(|e| e.to_string())?;
|
||||
let doc = match fileio::export_doc(st, f, params).map_err(|e| e.to_string())? {
|
||||
std::borrow::Cow::Borrowed(_) => st.doc.clone(),
|
||||
std::borrow::Cow::Owned(d) => std::sync::Arc::new(d),
|
||||
|
||||
@@ -1331,7 +1331,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"file.exportAs",
|
||||
"Export As…",
|
||||
"",
|
||||
"{format?, useArtboards?, range?} opens Export As (then the format's options); with {path, …document.export options} writes the file(s) → {path, warnings, files?}",
|
||||
"{format?, useArtboards?, range?} opens Export As (then the format's options); with {path, …document.export options} writes the file(s) → {path, warnings, files?}; under automation read roots, an SVG or SVGZ is refused while the document links a JavaScript file outside them, as document.export refuses it",
|
||||
),
|
||||
(
|
||||
"ui.fileInfoDialog",
|
||||
|
||||
@@ -242,6 +242,36 @@ fn control_export_refuses_an_option_no_format_takes() {
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// `app.export`, and `file.exportAs` run as a command, write no SVG of a document that links a
|
||||
/// JavaScript file outside the read root, and the error names no file or folder; the person at the
|
||||
/// keyboard exports it with the link as stored.
|
||||
#[test]
|
||||
fn control_svg_export_refuses_a_linked_javascript_file_outside_the_read_root() {
|
||||
let (mut app, tx, base) = confined("svg-script");
|
||||
app.run("file.new", json!({"width": 100, "height": 50})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
let script = s(base.join("outside/menu.js"));
|
||||
app.run("svgInteractivity.addFile", json!({"file": script})).unwrap();
|
||||
let path = s(base.join("out/a.svg"));
|
||||
for (method, params) in [
|
||||
("app.export", json!({"path": path, "format": "svg", "wait": true})),
|
||||
("app.export", json!({"path": s(base.join("out/a.svgz")), "wait": true})),
|
||||
("app.export", json!({"format": "svg"})),
|
||||
("engine.execute", json!({"command": "file.exportAs", "params": {"path": path, "format": "svg"}})),
|
||||
] {
|
||||
let r = request(&mut app, &tx, method, params.clone());
|
||||
let e = r["error"].as_str().unwrap_or_default();
|
||||
assert!(e.contains("a linked JavaScript file is outside the read roots"), "{method} {params}: {r}");
|
||||
assert!(!e.contains("menu.js") && !e.contains(&s(base.clone())), "{method} {params}: {r}");
|
||||
}
|
||||
assert_eq!(std::fs::read_dir(base.join("out")).unwrap().count(), 0, "nothing written");
|
||||
// The person at the keyboard isn't confined.
|
||||
app.run("file.exportAs", json!({"path": path, "format": "svg"})).unwrap();
|
||||
let svg = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(svg.contains(&format!("<script xlink:href=\"{script}\"/>")), "{svg}");
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// `app.export` with a path writes the file when given an option only another format reads, and
|
||||
/// its warnings name that option, as `document.export`'s do without a path. `file.exportAs` run
|
||||
/// as a command does the same; an export that automation did not ask for gets no such note.
|
||||
|
||||
+10
-1
@@ -234,6 +234,14 @@ the suite) keep the agent to the folders you give it:
|
||||
file outside the read roots by its file name alone in `files`, as `document.json` does, and `removeFile` takes that
|
||||
name for it (see SVG interactivity). In a frame a request drives, the JavaScript Files dialog lists such a file by
|
||||
its file name, and `ui_inspect` gives the file selected in the dialog the same way.
|
||||
- An SVG or SVGZ of a document that links a JavaScript file outside the read roots (SVG Interactivity's files: a
|
||||
path, a `file:` URL, or a relative path taken from the document's folder) is refused with "a linked JavaScript
|
||||
file is outside the read roots", which names no file or folder: `document.serialize`, `document.export` (MCP's
|
||||
`export`), `document.exportSelection`, `document.exportForScreens` and `assets.export` (before the folder is
|
||||
made or any row is written), `document.save`, `file.save`, `file.saveAs` and `file.saveCopy` (also a save
|
||||
without a path of a document whose own format is SVG or SVGZ), `file.exportAs`, and the SVG Options dialog's
|
||||
Show Code. A file given as another URL (`https://…`) is not checked. The document keeps the link, and the
|
||||
person at the keyboard exports it as stored.
|
||||
- `plugin.list`, `plugin.safeMode`, `file.recovery.*` and `ui_inspect`'s `scriptReport` leave out the paths
|
||||
outside the read roots, also those the person or an earlier request put there. In `plugin.list`, the plug-in
|
||||
folder last loaded is `null` in `folder.folder`, and a failed file in `folder.failed` and a plug-in's `source`
|
||||
@@ -4985,7 +4993,8 @@ more than one linked file (two such files, or such a file and a file linked as t
|
||||
error and unlinks nothing. `ui.javascriptFilesDialog {}`
|
||||
opens the JavaScript Files dialog, whose Choose… puts a picked file in as a `file:///` URL. SVG export writes each event
|
||||
as an event attribute on the object's element (`onclick="…"`; with the SVG Tiny 1.2 profile it leaves them out with a
|
||||
warning) and each file as `<script xlink:href>`. SVG import reads the event attributes the panel lists and the
|
||||
warning) and each file as `<script xlink:href>`, as stored; for a caller with automation read roots, an SVG or SVGZ
|
||||
of a document that links a file outside them is refused (see the read roots above). SVG import reads the event attributes the panel lists and the
|
||||
`<script>` links back as text; it never reads a script file or runs a script, and leaves inline scripts and the links
|
||||
after the first 64 out with a warning. An unnamed `<g>` with event attributes of its own stays a group, apart from the object in
|
||||
it. The native format keeps the events and the files.
|
||||
|
||||
@@ -816,6 +816,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- `ui.inspect` under `--automation-read-root`: the documents still opening, the recent files, library panels' files and folder preferences (saved, and in an open Preferences dialog) outside the read roots are left out of the reply, a library loaded from outside them shows by its file name in `ui.inspect` and `ui.docking`, and the app's state is not changed. `crates/ui-egui/src/tests/automation.rs` (`ui_inspect_leaves_out_paths_outside_the_read_root`).
|
||||
- Link replies under `--automation-read-root`: `links.list`, `links.check`, `links.info` and `document.open`'s link rows leave out the path, found path and folder of a linked file outside the read roots, and the stored size and date of it in `links.info`; each row keeps its id, file name and status, and the document's links are not changed. `crates/engine/src/tests/linkspanel.rs` (`link_paths_outside_the_read_roots_are_left_out`), `crates/engine/src/tests/placed_document.rs` (`link_info_leaves_out_a_placed_documents_path_outside_the_read_roots`).
|
||||
- Document replies under `--automation-read-root`: `document.inspect` gives `path` as null for a document file outside the read roots, and `document.json` names a linked image or placed file outside them by its file name alone, without its relative path, size or date, brush art included, leaves out a relative path that leads outside them and a stored brush entry that does not read as a brush, names a data set's image value outside them by its file name, and leaves an Export for Screens folder outside them out of `export_settings`; the document is not changed. `crates/engine/src/tests/placed_document.rs` (`document_replies_leave_out_paths_outside_the_read_roots`, `document_json_checks_brush_art_and_relative_paths_against_the_read_roots`).
|
||||
- SVG output under `--automation-read-root`: `document.serialize`, `document.export`, `document.exportSelection`, `document.exportForScreens`, `assets.export`, the SVG and SVGZ saves, `file.exportAs` and the SVG Options dialog's Show Code refuse an SVG or SVGZ of a document that links a JavaScript file outside the read roots, before any file or folder is written, with an error that names no file or folder; a URL, a file inside the roots and a relative path that leads inside them are written as stored, and the person at the keyboard exports every link as stored. `crates/engine/src/tests/interactivity.rs` (`confined_svg_output_refuses_a_linked_javascript_file_outside_the_read_roots`, `svg_output_keeps_linked_javascript_files_a_caller_may_read`), `crates/ui-egui/src/tests/automation.rs` (`control_svg_export_refuses_a_linked_javascript_file_outside_the_read_root`), `crates/ui-egui/src/dialogs/svg_options.rs` (`show_code_leaves_out_a_linked_javascript_file_outside_the_read_roots`).
|
||||
- Linked-file and font-file searches under `--automation-read-root`: `links.findFiles` and `text.findFontFiles` replies, also later polls and stops of a search another client started, leave out the files found, a link's saved path and the folder searched outside the caller's read roots, and a failed search gives its reason without the path; `image.info` gives `link: null` for a linked file outside them; `text.addFontFiles` gives `folder: null` and the copies' file names when the Fonts folder is outside them. `crates/engine/src/tests/linkfiles.rs`, `crates/engine/src/tests/fontfiles.rs`, `crates/engine/tests/font_folder.rs`.
|
||||
- Swatch, brush, symbol and graphic style library lists under `--automation-read-root`: every library stays listed, the paths of library files and the user library folder outside the read roots are `null`, a library loaded from outside them is listed and found as `loaded/<file name>` and keeps that id for the session unless a library loaded later without a file shows that id itself, which moves it to the next free `loaded/<file name> (N)`, and the app's own list is not changed. `crates/engine/src/tests/library_roots.rs`.
|
||||
- Preferences commands under `--automation-read-root`: `prefs.get`, `prefs.list`, `prefs.set` and `prefs.reset` show folder preferences outside the read roots as `null` and leave out an export preset's folder outside them, and the preferences are not changed. `crates/engine/src/tests/prefs_roots.rs`.
|
||||
|
||||
Reference in New Issue
Block a user