Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ vectorcraft-eps = { path = "crates/eps" }
vectorcraft-metafile = { path = "crates/metafile" }
vectorcraft-plugins = { path = "crates/plugins" }
vectorcraft-affinity = { path = "crates/affinity" }
vectorcraft-scriptcheck = { path = "crates/scriptcheck" }

# foundation
kurbo = { version = "0.13", features = ["serde"] }
Expand Down
1 change: 1 addition & 0 deletions crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ vectorcraft-eps = { workspace = true }
vectorcraft-metafile = { workspace = true }
vectorcraft-plugins = { workspace = true }
vectorcraft-affinity = { workspace = true }
vectorcraft-scriptcheck = { workspace = true }
image = { workspace = true }
flate2 = { workspace = true }
kurbo = { workspace = true }
Expand Down
22 changes: 21 additions & 1 deletion crates/engine/src/cmd/fileio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ fn format_filters() -> impl Iterator<Item = (&'static str, &'static [&'static st

/// Open-dialog filters: "All readable files" first, then one per readable format, then swatch
/// libraries (which open in the library panel), Libraries panel libraries, flattener, PDF and print
/// presets (imported).
/// presets (imported), scripts (checked without running them) and plug-ins (installed).
pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
std::iter::once(("All readable files", OPEN_EXTS))
.chain(format_filters())
Expand All @@ -683,6 +683,7 @@ pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static
.chain(std::iter::once(("Flattener presets", super::flatten::PRESET_EXTS)))
.chain(std::iter::once(("PDF presets", super::pdfcmds::PRESET_EXTS)))
.chain(std::iter::once(("Print presets", super::printpresets::PRESET_EXTS)))
.chain(std::iter::once(("Scripts", super::scripts::EXTS)))
.chain(std::iter::once(("Plug-ins", super::plugin::EXTS)))
}

Expand Down Expand Up @@ -766,6 +767,20 @@ pub(crate) fn read_file(path: &str) -> Result<Vec<u8>> {
std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
}

/// The first `max` bytes of the file at `path` (all of it when it is shorter). Anything but a
/// regular file (a folder, a pipe, a device) is refused, also through a link.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn read_file_head(path: &str, max: usize) -> Result<Vec<u8>> {
use std::io::Read;
may_read(path)?;
if !std::fs::metadata(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?.is_file() {
return Err(EngineError::Other(format!("{path}: not a file")));
}
let mut out = vec![];
std::fs::File::open(path).and_then(|f| f.take(max as u64).read_to_end(&mut out)).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
Ok(out)
}

#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn write_file(path: &str, bytes: &[u8]) -> Result<()> {
may_write(path)?;
Expand Down Expand Up @@ -944,6 +959,11 @@ pub(crate) fn read_file(path: &str) -> Result<Vec<u8>> {
Err(no_fs(path))
}

#[cfg(target_arch = "wasm32")]
pub(crate) fn read_file_head(path: &str, _: usize) -> Result<Vec<u8>> {
Err(no_fs(path))
}

#[cfg(target_arch = "wasm32")]
pub(crate) fn write_file(path: &str, _: &[u8]) -> Result<()> {
Err(no_fs(path))
Expand Down
5 changes: 3 additions & 2 deletions crates/engine/src/cmd/fileio/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,10 @@ fn open_exts_cover_every_readable_format() {
assert_eq!(filters[0], ("All readable files", OPEN_EXTS));
assert_eq!(
filters.len(),
7 + FORMATS.iter().filter(|f| f.read).count(),
"and swatch libraries, libraries, flattener, PDF and print presets, plug-ins"
8 + FORMATS.iter().filter(|f| f.read).count(),
"and swatch libraries, libraries, flattener, PDF and print presets, scripts, plug-ins"
);
assert!(filters.contains(&("Scripts", crate::cmd::scripts::EXTS)), "File › Open checks scripts");
assert_eq!(filters.last(), Some(&("Plug-ins", crate::cmd::plugin::EXTS)), "File › Open installs plug-ins");
}

Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub mod printtiling;
pub mod rasterfx;
mod recolor;
pub mod recovery;
pub mod scripts;
mod select;
pub(crate) mod shaper;
pub(crate) mod slices;
Expand Down Expand Up @@ -278,6 +279,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(printpresets::specs());
v.extend(printtiling::specs());
v.extend(plugin::specs());
v.extend(scripts::specs());
v.extend(cut::specs());
v.extend(halftone::specs());
v.extend(perspgrid::specs());
Expand Down
159 changes: 159 additions & 0 deletions crates/engine/src/cmd/scripts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//! `script.check`: a script written for the reference app (`.jsx`, `.jsxinc`, `.js`, or a compiled
//! `.jsxbin`) checked without running any of it, by `vectorcraft-scriptcheck`: what kind of file it
//! is, what it needs, its directives and includes, the platform features and document-model member
//! chains it uses, the menu commands it runs, what it does outside the document, and its problems.
//! With a path and `includes`, the files its `#include` lines name are read from inside its folder,
//! each through the same checks as every other file a command reads.

use serde_json::{Value, json};
use vectorcraft_scriptcheck::{Limits, Loaded, Loader, Report};

/// The ids a reply can hold, for the windows that show it.
pub use vectorcraft_scriptcheck::{IncludeStatus, Need, OUTSIDE, PROBLEMS};

use super::*;

/// The extensions of the scripts File › Open shows a check for.
pub const EXTS: &[&str] = &["jsx", "jsxinc", "jsxbin"];

pub fn specs() -> Vec<CommandSpec> {
vec![cmd!(
query "script.check",
"Check Script",
[],
None,
"{path? | data?: the script's text | dataBase64?, name?: the file's name (default: the path's; with a path, only the reply's name), includes?: false (with a path: also check the files its #include lines name, inside the script's folder; not on the web)} check a script written for the reference app (.jsx, .jsxinc, .js, or a compiled .jsxbin) without running any of it → {name, kind: script|compiled|module|notText|tooLarge, needs: [language|dom|files|menuCommands|dialogs|hostOnly], lines, targets: [{value, app, instance?, version?, locale?, line, file?}], directives: [{name, value, line, form: \"#\"|\"//@\", file?}], includes: [{value, line, status: checked|missing|notFollowed|cycle|notRead|limit|unreadable, path?, file?}], features: [{id, count, lines}], dom: {members: [{path, count}], types: [{name, count}]}, menuCommands: {strings: [{value, count, lines}], dynamic}, outside: [{id: filesWritten|filesOpened|network|nativeCode|otherApps|environment|unseenCode|preferences, count, lines, modes?}], problems: [{code, line?, file?, detail?}], truncated} (file: the included file it is in; lines: at most 20 of the script's own, while counts include the included files; at most 8 MiB a file and 32 MiB in all, 64 included files 16 deep, 1,024 paths looked up for them, 1,000 entries a list, text echoed cut to 120 characters)",
always,
check
)]
}

/// The folder a script is in, which its included files are read from.
struct Folder {
dir: std::path::PathBuf,
/// The folder with its links followed (none where the file system can't tell).
real: Option<std::path::PathBuf>,
}

impl Folder {
/// The folder of the script at `path`.
fn of(path: &str) -> Self {
let dir = std::path::Path::new(path).parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(std::path::Path::new("."));
Folder { real: std::fs::canonicalize(dir).ok(), dir: dir.to_path_buf() }
}
}

impl Loader for Folder {
fn load(&mut self, path: &str, max: usize) -> Loaded {
let file = self.dir.join(path);
// A link that leads out of the script's folder isn't followed.
if let (Some(real), Ok(target)) = (&self.real, std::fs::canonicalize(&file))
&& !target.starts_with(real)
{
return Loaded::Refused;
}
let file = file.to_string_lossy();
if crate::file_access::check_read(&file).is_err() {
return Loaded::Refused;
}
super::fileio::read_file_head(&file, max).map_or(Loaded::Missing, Loaded::Bytes)
}
}

/// The bytes the base64 text `b64` holds, up to `max` and a few more: only the text they take is
/// decoded.
fn decode_head(b64: &str, max: usize) -> Option<Vec<u8>> {
let chars = max.div_ceil(3).saturating_mul(4);
let mut kept = 0usize;
let end = b64
.bytes()
.position(|b| {
kept += usize::from(!b.is_ascii_whitespace());
kept > chars
})
.unwrap_or(b64.len());
vectorcraft_format::base64_decode(b64.get(..end)?)
}

fn check(_: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "script.check";
let limits = Limits::default();
// One byte more than a file may have shows that the file is too large.
let max = limits.file_bytes.saturating_add(1);
let path = str_param(p, "path");
let bytes: std::borrow::Cow<[u8]> = match (path, str_param(p, "data"), str_param(p, "dataBase64")) {
(Some(path), ..) => super::fileio::read_file_head(path, max)?.into(),
(None, Some(text), _) => text.as_bytes().get(..max).unwrap_or(text.as_bytes()).into(),
(None, None, Some(b64)) => decode_head(b64, max).ok_or_else(|| bad(C, "bad dataBase64"))?.into(),
_ => return Err(bad(C, "give `path`, `data` or `dataBase64`")),
};
let name = super::fileio::file_name(str_param(p, "name").or(path).unwrap_or_default());
// With a path, the file is checked under its own name, which its includes are followed from;
// `name` names the reply.
let file = path.map(super::fileio::file_name).unwrap_or_else(|| name.clone());
let mut folder = path.filter(|_| bool_or(p, "includes", false)).map(Folder::of);
let report = vectorcraft_scriptcheck::check(&file, &bytes, folder.as_mut().map(|f| f as &mut dyn Loader), &limits);
Ok(report_json(&name, &report))
}

/// Object `v` with `key: value` added when there is a value.
fn with(mut v: Value, key: &str, value: Option<impl serde::Serialize>) -> Value {
if let (Some(o), Some(x)) = (v.as_object_mut(), value) {
o.insert(key.into(), json!(x));
}
v
}

/// The reply for report `r` on the script `name`.
fn report_json(name: &str, r: &Report) -> Value {
let targets: Vec<Value> = r
.targets
.iter()
.map(|t| {
let v = json!({"value": t.value, "app": t.app, "line": t.line});
let v = with(v, "instance", t.instance.as_deref());
let v = with(v, "version", t.version.as_deref());
let v = with(v, "locale", t.locale.as_deref());
with(v, "file", t.file.as_deref())
})
.collect();
let directives: Vec<Value> = r
.directives
.iter()
.map(|d| with(json!({"name": d.name, "value": d.value, "line": d.line, "form": d.form}), "file", d.file.as_deref()))
.collect();
let includes: Vec<Value> = r
.includes
.iter()
.map(|i| with(with(json!({"value": i.value, "line": i.line, "status": i.status.id()}), "path", i.path.as_deref()), "file", i.file.as_deref()))
.collect();
let features: Vec<Value> = r.features.iter().map(|f| json!({"id": f.id, "count": f.count, "lines": f.lines})).collect();
let members: Vec<Value> = r.members.iter().map(|m| json!({"path": m.name, "count": m.count})).collect();
let types: Vec<Value> = r.types.iter().map(|t| json!({"name": t.name, "count": t.count})).collect();
let strings: Vec<Value> = r.menu_strings.iter().map(|s| json!({"value": s.value, "count": s.count, "lines": s.lines})).collect();
let outside: Vec<Value> = r
.outside
.iter()
.map(|o| with(json!({"id": o.id, "count": o.count, "lines": o.lines}), "modes", Some(&o.modes).filter(|m| !m.is_empty())))
.collect();
let problems: Vec<Value> = r
.problems
.iter()
.map(|p| with(with(with(json!({"code": p.code}), "line", p.line), "file", p.file.as_deref()), "detail", p.detail.as_deref()))
.collect();
json!({
"name": name,
"kind": r.kind.id(),
"needs": r.needs.iter().map(|n| n.id()).collect::<Vec<_>>(),
"lines": r.lines,
"targets": targets,
"directives": directives,
"includes": includes,
"features": features,
"dom": {"members": members, "types": types},
"menuCommands": {"strings": strings, "dynamic": r.menu_dynamic},
"outside": outside,
"problems": problems,
"truncated": r.truncated,
})
}
2 changes: 2 additions & 0 deletions crates/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,8 @@ mod tests_saveoptions;
#[cfg(test)]
mod tests_scalestrokes;
#[cfg(test)]
mod tests_scripts;
#[cfg(test)]
mod tests_shaper;
#[cfg(test)]
mod tests_slices;
Expand Down
Loading
Loading