mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 14:46:03 +00:00
Scripts: File and Folder exists checks the kind, encode keeps slashes, every call is counted
- `File.encode` and `Folder.encode` leave `/` unescaped, as the File object reference lists it among the characters kept (https://extendscript.docsforadobe.dev/file-system-access/file-object/). - `exists` is true for a `File` only when the path names something other than a folder, and for a `Folder` only when it names a folder. - Fields of the records the app returns are read as own properties, so a property a script puts on `Object.prototype` does not turn a successful `open` into a failure. - `File.encode`, `File.decode`, `File.isEncodingAvailable` and the `Folder` locations count as file calls and charge the host-call limit. - On the web the locations are `null` and `File.tempName` names a file at the root, without asking the platform for a temporary folder.
This commit is contained in:
@@ -183,11 +183,15 @@ fn home_folder() -> String {
|
||||
}
|
||||
|
||||
/// The folders `Folder.temp`, `Folder.desktop` and the others name, `null` where there is none.
|
||||
/// On the web every one is `null`, and no platform folder is looked up.
|
||||
fn locations(home: &str) -> Data {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
return Data::Object(PLACES.iter().map(|k| (k.to_string(), Data::Null)).collect());
|
||||
}
|
||||
let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty());
|
||||
let join = |a: &str, b: &str| Path::new(a).join(b).to_string_lossy().into_owned();
|
||||
let exe = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_string_lossy().into_owned()));
|
||||
let temp = std::env::temp_dir().to_string_lossy().into_owned();
|
||||
let temp = temp_folder().to_string_lossy().into_owned();
|
||||
let (user_data, app_data, common, system, trash) = if WINDOWS {
|
||||
(env("APPDATA"), env("PROGRAMDATA"), env("CommonProgramFiles"), env("SystemRoot"), None)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
@@ -203,7 +207,6 @@ fn locations(home: &str) -> Data {
|
||||
(Some(data.clone()), Some("/usr/share".to_string()), Some("/usr/share".to_string()), Some("/".to_string()), Some(join(&data, "Trash")))
|
||||
};
|
||||
let s = |v: Option<String>| v.map_or(Data::Null, Data::String);
|
||||
let web = cfg!(target_arch = "wasm32");
|
||||
let places = vec![
|
||||
("appData", app_data),
|
||||
("appPackage", exe.clone()),
|
||||
@@ -216,7 +219,15 @@ fn locations(home: &str) -> Data {
|
||||
("trash", trash),
|
||||
("userData", user_data),
|
||||
];
|
||||
Data::Object(places.into_iter().map(|(k, v)| (k.to_string(), if web { Data::Null } else { s(v) })).collect())
|
||||
Data::Object(places.into_iter().map(|(k, v)| (k.to_string(), s(v))).collect())
|
||||
}
|
||||
|
||||
/// The names [`locations`] gives.
|
||||
const PLACES: [&str; 10] = ["appData", "appPackage", "commonFiles", "desktop", "myDocuments", "startup", "system", "temp", "trash", "userData"];
|
||||
|
||||
/// The folder for temporary files: the platform's, or on the web the root, where nothing is read.
|
||||
fn temp_folder() -> std::path::PathBuf {
|
||||
if cfg!(target_arch = "wasm32") { std::path::PathBuf::from("/") } else { std::env::temp_dir() }
|
||||
}
|
||||
|
||||
/// The start of an absolute path.
|
||||
@@ -343,12 +354,12 @@ impl Pathname {
|
||||
}
|
||||
}
|
||||
|
||||
/// `s` with every character but letters, digits and `-_.!~*'()` written as `%xx` escapes of its
|
||||
/// `s` with every character but letters, digits and `/-_.!~*'()` written as `%xx` escapes of its
|
||||
/// UTF-8 bytes (RFC 2396).
|
||||
fn encode(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for b in s.bytes() {
|
||||
if b.is_ascii_alphanumeric() || b"-_.!~*'()".contains(&b) {
|
||||
if b.is_ascii_alphanumeric() || b"/-_.!~*'()".contains(&b) {
|
||||
out.push(b as char);
|
||||
} else {
|
||||
out.push_str(&format!("%{b:02X}"));
|
||||
@@ -835,14 +846,30 @@ fn temp_name(host: &Host, _: &[Data]) -> Result<Data, HostError> {
|
||||
let mut disk = host.disk.borrow_mut();
|
||||
disk.temps += 1;
|
||||
let name = format!("vc{:05}.tmp", disk.temps);
|
||||
Ok(Data::String(std::env::temp_dir().join(name).to_string_lossy().into_owned()))
|
||||
Ok(Data::String(temp_folder().join(name).to_string_lossy().into_owned()))
|
||||
}
|
||||
|
||||
fn locations_of(host: &Host, _: &[Data]) -> Result<Data, HostError> {
|
||||
Ok(locations(&host.disk.borrow().home))
|
||||
}
|
||||
|
||||
fn encode_text(_: &Host, a: &[Data]) -> Result<Data, HostError> {
|
||||
Ok(Data::String(encode(&arg_text(a, 0))))
|
||||
}
|
||||
|
||||
fn decode_text(_: &Host, a: &[Data]) -> Result<Data, HostError> {
|
||||
Ok(Data::String(decode(&arg_text(a, 0))))
|
||||
}
|
||||
|
||||
fn encoding_available(_: &Host, a: &[Data]) -> Result<Data, HostError> {
|
||||
Ok(Data::Bool(Encoding::parse(&arg_text(a, 0)).is_some()))
|
||||
}
|
||||
|
||||
/// Adds `File` and `Folder` to the realm.
|
||||
pub(super) fn install(realm: &mut Realm, host: &Rc<Host>) -> Result<(), RunError> {
|
||||
let (start, home) = {
|
||||
let start = {
|
||||
let disk = host.disk.borrow();
|
||||
(disk.pathname(&disk.start, &disk.start, false).fs(), disk.home.clone())
|
||||
disk.pathname(&disk.start, &disk.start, false).fs()
|
||||
};
|
||||
let value = |s: &str| HostItem::Value(Data::String(s.to_string()));
|
||||
let text = HostItem::Object(vec![
|
||||
@@ -851,7 +878,6 @@ pub(super) fn install(realm: &mut Realm, host: &Rc<Host>) -> Result<(), RunError
|
||||
("noPicker".into(), value(NO_PICKER)),
|
||||
("badMode".into(), value(BAD_MODE)),
|
||||
]);
|
||||
let pure = |f: fn(&str) -> Data| HostItem::Function(Rc::new(move |a| Ok(f(&arg_text(&a, 0)))));
|
||||
let system = if WINDOWS {
|
||||
"Windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
@@ -864,10 +890,10 @@ pub(super) fn install(realm: &mut Realm, host: &Rc<Host>) -> Result<(), RunError
|
||||
("system".into(), value(system)),
|
||||
("lineFeed".into(), value(if WINDOWS { "Windows" } else { "Unix" })),
|
||||
("current".into(), HostItem::Function(Rc::new(move |_| Ok(Data::String(start.clone()))))),
|
||||
("locations".into(), HostItem::Function(Rc::new(move |_| Ok(locations(&home))))),
|
||||
("encode".into(), pure(|s| Data::String(encode(s)))),
|
||||
("decode".into(), pure(|s| Data::String(decode(s)))),
|
||||
("encodingAvailable".into(), pure(|s| Data::Bool(Encoding::parse(s).is_some()))),
|
||||
("locations".into(), call(host, locations_of)),
|
||||
("encode".into(), call(host, encode_text)),
|
||||
("decode".into(), call(host, decode_text)),
|
||||
("encodingAvailable".into(), call(host, encoding_available)),
|
||||
("parse".into(), call(host, parse)),
|
||||
("relative".into(), call(host, relative)),
|
||||
("tempName".into(), call(host, temp_name)),
|
||||
@@ -935,7 +961,7 @@ mod tests {
|
||||
#[test]
|
||||
fn uri_escapes_are_utf8_both_ways() {
|
||||
assert_eq!(encode("caf\u{e9} 1"), "caf%C3%A9%201");
|
||||
assert_eq!(encode("a-_.!~*'()/b"), "a-_.!~*'()%2Fb");
|
||||
assert_eq!(encode("a-_.!~*'()/b"), "a-_.!~*'()/b");
|
||||
assert_eq!(decode("caf%C3%A9%20x"), "caf\u{e9} x");
|
||||
assert_eq!(decode("100%"), "100%");
|
||||
assert_eq!(decode("%E9"), "%E9", "a lone Latin-1 escape is not UTF-8");
|
||||
|
||||
@@ -55,6 +55,42 @@ fn file_constructor_identity_and_folder_path() {
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// `exists` is true only when the path names an entry of the object's own kind.
|
||||
#[test]
|
||||
fn script_file_exists_checks_the_kind() {
|
||||
let base = folder("kind");
|
||||
write(&base.join("main/data.txt"), b"x");
|
||||
std::fs::create_dir_all(base.join("main/sub")).unwrap();
|
||||
let log =
|
||||
logged(&base, "vc.log([new File('sub').exists, new Folder('data.txt').exists, new File('data.txt').exists, new Folder('sub').exists]);");
|
||||
assert_eq!(log, json!([[false, false, true, true]]));
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// A property a script puts on `Object.prototype` is not read as part of what the app returns.
|
||||
#[test]
|
||||
fn script_file_ignores_inherited_result_fields() {
|
||||
let base = folder("inherited");
|
||||
write(&base.join("main/data.txt"), b"data");
|
||||
let log = logged(
|
||||
&base,
|
||||
"Object.prototype.error = 'inherited'; Object.prototype.exists = true;\n\
|
||||
var f = new File('data.txt'); var opened = f.open('r');\n\
|
||||
vc.log([f.exists, new File('nope').exists, opened, f.error, f.read()]);",
|
||||
);
|
||||
assert_eq!(log, json!([[true, false, true, "", "data"]]));
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// `File.encode`, `File.decode`, `File.isEncodingAvailable` and the `Folder` locations are counted as
|
||||
/// calls to the app.
|
||||
#[test]
|
||||
fn script_file_class_functions_are_counted() {
|
||||
let source = "File.encode('x'); File.decode('y'); Folder.isEncodingAvailable('UTF-8'); Folder.temp;";
|
||||
let r = Session::new().execute("script.run", &json!({"source": source})).unwrap();
|
||||
assert_eq!(r["hostCallCounts"]["fileCalls"], 4, "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_names_are_uri_encoded_and_current_relative() {
|
||||
let base = folder("names");
|
||||
@@ -74,7 +110,7 @@ fn file_names_are_uri_encoded_and_current_relative() {
|
||||
assert_eq!(
|
||||
log,
|
||||
json!([
|
||||
["caf%C3%A9%20x.txt", "caf\u{e9} x.txt", "caf%C3%A9%20x.txt", true, "%C3%A9%20%2F", "\u{e9}", "sub/y.txt", true, true],
|
||||
["caf%C3%A9%20x.txt", "caf\u{e9} x.txt", "caf%C3%A9%20x.txt", true, "%C3%A9%20/", "\u{e9}", "sub/y.txt", true, true],
|
||||
["sub", "w", "w"],
|
||||
["literal%20name.txt", "literal%2520name.txt", "literal%2520name.txt", true, "p%41", "p%41"]
|
||||
])
|
||||
|
||||
@@ -67,8 +67,13 @@ fn message(i: &mut Interp, name: &str) -> JsResult<Value> {
|
||||
i.get_value(&text, &key(name))
|
||||
}
|
||||
|
||||
/// The field `name` of a record the host gave: its own data property, `undefined` when it has none,
|
||||
/// so that a property a script put on `Object.prototype` is not read as part of it.
|
||||
fn field(i: &mut Interp, v: &Value, name: &str) -> JsResult<Value> {
|
||||
i.get_value(v, &key(name))
|
||||
match v.as_object() {
|
||||
Some(o) => slot(i, o, name),
|
||||
None => Ok(Value::Undefined),
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of the own data property `name` of `o`, `undefined` when it has none.
|
||||
@@ -305,8 +310,19 @@ fn parent(i: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value>
|
||||
instance(i, proto, up)
|
||||
}
|
||||
|
||||
/// `exists`: whether the path names something of the object's own kind, a folder for a `Folder`
|
||||
/// and anything else for a `File`.
|
||||
fn exists(i: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
Ok(Value::Bool(stat_field(i, &this, "exists", "exists", Value::Bool(false))?.truthy()))
|
||||
let folder = classes(i)?.folder_proto;
|
||||
let want_dir = match this.as_object() {
|
||||
Some(o) => i.object(o)?.proto == Some(folder),
|
||||
None => false,
|
||||
};
|
||||
let Some(r) = stat(i, &this, "exists")? else { return Ok(Value::Bool(false)) };
|
||||
i.rooted(std::slice::from_ref(&r), |i| {
|
||||
let dir = field(i, &r, "dir")?.truthy();
|
||||
Ok(Value::Bool(field(i, &r, "exists")?.truthy() && dir == want_dir))
|
||||
})
|
||||
}
|
||||
|
||||
fn created(i: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
@@ -600,7 +616,7 @@ fn get_files(i: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<V
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let entries = field(i, &r, "entries")?;
|
||||
let length = field(i, &entries, "length")?;
|
||||
let length = i.get_value(&entries, &key("length"))?;
|
||||
let n = i.to_number(&length)?;
|
||||
i.reserve_heap(OBJECT_BYTES)?;
|
||||
let proto = i.intrinsics.array_proto;
|
||||
|
||||
@@ -1819,6 +1819,8 @@ script reads files with them:
|
||||
objects for the same path are different objects.
|
||||
- `name`, `displayName`, `fsName`, `absoluteURI`, `fullName`, `relativeURI`, `path`, `parent` and `toString()` come
|
||||
from the path. `exists`, `length`, `created`, `modified`, `hidden`, `readonly` and `alias` read the file system.
|
||||
`exists` is true for a `File` only when the path names something other than a folder, and for a `Folder` only
|
||||
when it names a folder.
|
||||
`File.encode`, `File.decode` and `File.isEncodingAvailable` (and the same on `Folder`), `File.fs`, and the folders
|
||||
`Folder.temp`, `Folder.desktop`, `Folder.myDocuments`, `Folder.userData` and the others are there too.
|
||||
- `open('r')` opens a file to read, and `read([chars])`, `readln()`, `readch()`, `seek(pos[, mode])`, `tell()`, `eof`
|
||||
|
||||
Reference in New Issue
Block a user