Plug-ins: diffPath is a file key and Slice Options' url is not; the command sweep uses these keys

The sweeps in command_sweep.rs kept their own list of file and folder
keys (path, diffPath, folder, destinationFolder) in testkit's
safe_value and again in the temp-folder test, so a command whose file
key was not on the list was sent raw junk strings, and a key written as
`{path | name ...}` (document.open) was never swept.

- The engine exports the plug-in call rules' file keys as
  `cmd::key_location`, `cmd::location_in` and `cmd::location_keys`,
  which read every key a params description names, the keys of the
  commands its `...` entries name, and the schema. `diffPath` is a file
  key, so a plug-in call that carries it is refused; Slice Options'
  `url` and Export for Screens' `folders` name part of the document, so
  a call to object.slice.options with a top-level `url` is accepted.
- testkit's `param_keys` reads the keys the engine reads, and
  `junk_params_for_command` and `safe_value_for_command` send every
  string under a file or folder key, in arrays and in objects inside
  them, as a location in the sweep's temp folder. Each such key also
  gets a list of paths and a list of `{ids, path}` objects.
- Every catalog sweep, the serializability sweep included, uses them,
  and the temp-folder test checks every key the catalog names, at any
  depth.
This commit is contained in:
dotCooCoo
2026-11-12 06:36:56 -08:00
parent 71b889605d
commit bf9baa0b85
5 changed files with 314 additions and 63 deletions
+85 -2
View File
@@ -102,7 +102,53 @@ const FILE_WORDS: &[&str] = &[
];
/// Param keys that name a file or a folder.
const FILE_KEYS: &[&str] = &["path", "paths", "file", "files", "folder", "folders", "dir", "directory", "destinationFolder", "sourceFolder", "url"];
const FILE_KEYS: &[&str] =
&["path", "paths", "file", "files", "folder", "folders", "dir", "directory", "destinationFolder", "sourceFolder", "url", "diffPath"];
/// The keys in [`FILE_KEYS`] that name a folder.
const FOLDER_KEYS: &[&str] = &["folder", "folders", "dir", "directory", "destinationFolder", "sourceFolder"];
/// What a param key that names a place on disk names there.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Location {
/// A file (or a list of files).
File,
/// A folder (or a list of folders).
Folder,
}
/// What `key` names on disk when a command takes it ([`FILE_KEYS`]), or `None` when it never
/// names a file or a folder.
pub fn key_location(key: &str) -> Option<Location> {
if FOLDER_KEYS.contains(&key) {
Some(Location::Folder)
} else if FILE_KEYS.contains(&key) {
Some(Location::File)
} else {
None
}
}
/// What `key` names on disk in command `spec`: [`key_location`], or `None` when [`NOT_FILES`]
/// names it for `spec` (it names part of the document there).
pub fn location_in(spec: &CommandSpec, key: &str) -> Option<Location> {
if NOT_FILES.contains(&(spec.id, key)) { None } else { key_location(key) }
}
/// The param keys of `spec` that name a file or a folder, with what they name: the keys in
/// [`FILE_KEYS`] that its params description names ([`super::params::doc_keys`]), with those of the
/// commands its `…` entries name ([`super::params::takes_key`]), and its schema's params, less those
/// [`NOT_FILES`] names for it. Sorted, each key once.
pub fn location_keys(spec: &CommandSpec) -> Vec<(&'static str, Location)> {
let described = super::params::doc_keys(spec.params).map(|p| p.keys).unwrap_or_default();
let in_schema = spec.schema.map(|s| s.params.iter().map(|p| p.name)).into_iter().flatten();
let taken = FILE_KEYS.iter().copied().filter(|k| super::params::takes_key(spec.params, k));
let mut keys: Vec<(&'static str, Location)> =
described.into_iter().chain(in_schema).chain(taken).filter_map(|k| Some((k, location_in(spec, k)?))).collect();
keys.sort_unstable_by_key(|(k, _)| *k);
keys.dedup();
keys
}
/// Commands and the param key in [`FILE_KEYS`] that, in them, names part of the document (a path of
/// its art, a setting, an attribute it stores or a result field), not a file.
@@ -119,6 +165,8 @@ pub(crate) const NOT_FILES: &[(&str, &str)] = &[
("attributes.set", "url"),
("svgInteractivity.info", "files"),
("addon.cleanup.rejoin", "paths"),
("object.slice.options", "url"),
("document.exportForScreens", "folders"),
];
/// Is command id `id` one a plug-in may never call, from its id alone ([`DENIED_PREFIXES`],
@@ -466,7 +514,8 @@ mod tests {
}
for (id, key) in NOT_FILES {
let Some(spec) = crate::cmd::find_command(id) else { panic!("no command {id}") };
assert!(names_key(spec.params, key), "{id} has no `{key}` param");
let described = crate::cmd::params::doc_keys(spec.params).is_ok_and(|p| p.keys.contains(key));
assert!(names_key(spec.params, key) || described, "{id} has no `{key}` param");
}
let leaks: Vec<&str> = specs
.iter()
@@ -565,6 +614,40 @@ mod tests {
read.unwrap();
}
/// [`location_keys`] names every key in [`FILE_KEYS`] that a command's params description or
/// schema names, in every way a description writes one (`path | name`, `folder? (…)`), less
/// those [`NOT_FILES`] names for it; folder keys name folders. No command a plug-in may call
/// has one.
#[test]
fn location_keys_come_from_the_command_catalog() {
let find = |id: &str| crate::cmd::find_command(id).unwrap_or_else(|| panic!("no command {id}"));
let keys = |id: &str| location_keys(find(id));
assert!(keys("document.open").contains(&("path", Location::File)), "{:?}", keys("document.open"));
assert!(keys("document.compare").contains(&("diffPath", Location::File)), "{:?}", keys("document.compare"));
assert!(keys("addon.document.saveTimestampedCopy").contains(&("folder", Location::Folder)));
let relink = keys("links.relink");
for k in [("path", Location::File), ("paths", Location::File), ("folder", Location::Folder)] {
assert!(relink.contains(&k), "{relink:?}");
}
assert!(keys("action.batch").contains(&("destinationFolder", Location::Folder)));
for (id, key) in NOT_FILES {
assert!(!keys(id).iter().any(|(k, _)| k == key), "{id} `{key}` names a file");
}
for spec in crate::cmd::command_specs() {
let found = location_keys(spec);
let described = crate::cmd::params::doc_keys(spec.params).map(|p| p.keys).unwrap_or_default();
let in_schema = spec.schema.map(|s| s.params.iter().map(|p| p.name).collect::<Vec<_>>()).unwrap_or_default();
for k in described.into_iter().chain(in_schema).filter(|k| FILE_KEYS.contains(k) && !NOT_FILES.contains(&(spec.id, *k))) {
assert!(found.iter().any(|(f, _)| *f == k), "{}: `{k}` is missing from {found:?}", spec.id);
}
assert!(found.iter().all(|(k, l)| (*l == Location::Folder) == FOLDER_KEYS.contains(k)), "{}: {found:?}", spec.id);
assert!(denied(spec).is_some() || found.is_empty(), "{} names {found:?} but a plug-in may call it", spec.id);
}
assert_eq!(key_location("url"), Some(Location::File));
assert_eq!(key_location("sourceFolder"), Some(Location::Folder));
assert_eq!(key_location("pathData"), None);
}
#[test]
fn keys_are_found_as_whole_keys() {
assert!(names_key("{path: a .wasm file}", "path"));