Merge pull request #7255 from dotCooCoo/fix/folder-prefs-out-of-recorded-steps

Actions: keep folder preferences out of saved sets and confined replies
This commit is contained in:
BFlat
2026-11-14 08:40:15 +07:00
committed by GitHub
38 changed files with 306 additions and 52 deletions
+21 -13
View File
@@ -793,7 +793,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Export Preferences",
[],
None,
"{path?} write the preferences as a JSON file {format: \"vectorcraft-preferences\", version: 1, prefs: {key: value, …}}: every preference prefs.list lists and the eyedropper, perspectiveWidget, pathfinder, glyphSnap and pixelSnap groups, without the folder preferences fontsFolder, pluginsFolder, recoveryFolder and templatesFolder and without saved presets; a file over 1 MB, which prefs.import would refuse, is an error and is not written → {path, count}; without path → {data: the file's text, count}",
"{path?} write the preferences as a JSON file {format: \"vectorcraft-preferences\", version: 1, prefs: {key: value, …}}: every preference prefs.list lists and the eyedropper, perspectiveWidget, pathfinder, glyphSnap and pixelSnap groups, without the folder preferences fontsFolder, pluginsFolder, scratchPrimary, scratchSecondary, recoveryFolder and templatesFolder and without saved presets; a file over 1 MB, which prefs.import would refuse, is an error and is not written → {path, count}; without path → {data: the file's text, count}",
always,
export
),
@@ -802,7 +802,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Import Preferences",
[],
None,
"{path? | data?: the file's text} apply the preferences of a file prefs.export wrote (at most 1 MB). Every value is checked first, as prefs.set checks it: another format, a newer version, an unknown preference, a value of the wrong type or out of range, or a folder preference is an error, and nothing changes. Preferences the file leaves out keep their values; the open document's units stay as they are; no undo step; not a command.batch step → {imported: count}",
"{path? | data?: the file's text} apply the preferences of a file prefs.export wrote (at most 1 MB). Every value is checked first, as prefs.set checks it: another format, a newer version, an unknown preference, a value of the wrong type or out of range, or a folder preference (for scratchPrimary and scratchSecondary, a folder; the choices Startup and None are read) is an error, and nothing changes. Preferences the file leaves out keep their values; the open document's units stay as they are; no undo step; not a command.batch step → {imported: count}",
always,
import
),
@@ -818,8 +818,20 @@ pub const PREFS_VERSION: u64 = 1;
/// The largest preferences file `prefs.import` reads.
const MAX_PREFS_FILE_BYTES: usize = 1 << 20;
/// Folder preferences, which name folders on one computer: a preferences file leaves them out.
const FOLDER_PREFS: &[&str] = &["fontsFolder", "pluginsFolder", "recoveryFolder", "templatesFolder"];
/// Folder preferences, which name folders on one computer: a preferences file leaves them out, and
/// under automation read roots one outside them is not shown.
pub const FOLDER_PREFS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"];
/// The scratch disk choices that are not folders.
pub const SCRATCH_CHOICES: [&str; 2] = ["Startup", "None"];
/// Does `value`, set as preference `key`, name a folder: is `key` a folder preference
/// ([`FOLDER_PREFS`]) and `value`, trimmed as the app uses it, neither empty (the default folder)
/// nor a scratch disk choice ([`SCRATCH_CHOICES`])?
pub fn names_a_folder(key: &str, value: &str) -> bool {
let path = value.trim();
FOLDER_PREFS.contains(&key) && !path.is_empty() && !(key.starts_with("scratch") && SCRATCH_CHOICES.contains(&path))
}
/// The preferences `prefs.export` writes: every [`PREF_SPECS`] key but the [`FOLDER_PREFS`], and
/// the [`PREF_GROUPS`].
@@ -868,7 +880,9 @@ fn read_prefs_file(bytes: &[u8]) -> std::result::Result<Map<String, Value>, Stri
if prefs.is_empty() {
return Err("the file holds no preferences".into());
}
if let Some(key) = prefs.keys().find(|k| FOLDER_PREFS.contains(&k.as_str())) {
// A scratch disk choice or an empty scratch disk names no folder, and is read.
let folder = |k: &str, v: &Value| FOLDER_PREFS.contains(&k) && (!k.starts_with("scratch") || v.as_str().is_some_and(|v| names_a_folder(k, v)));
if let Some(key) = prefs.iter().find(|(k, v)| folder(k, v)).map(|(k, _)| k) {
return Err(format!("`{key}` names a folder, which preferences files leave out"));
}
Ok(prefs.clone())
@@ -893,14 +907,8 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "imported": values.len() }))
}
/// The preferences that name a folder: the [`FOLDER_PREFS`] and the scratch disks.
const PATH_PREFS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"];
/// The scratch disk choices that are not folders.
const SCRATCH_CHOICES: [&str; 2] = ["Startup", "None"];
/// The preferences as `prefs.get`, `prefs.list`, `prefs.set` and `prefs.reset` show them to the
/// request being served. Under automation read roots, a folder preference ([`PATH_PREFS`]) outside
/// request being served. Under automation read roots, a folder preference ([`FOLDER_PREFS`]) outside
/// them is `null`, as `ui.inspect` leaves it out, and an export preset's `folder` outside them is
/// left out. An empty value (the default folder) and a scratch
/// disk choice ([`SCRATCH_CHOICES`]) are shown. Without roots the preferences are shown as they are.
@@ -911,7 +919,7 @@ fn shown_prefs(prefs: &Prefs) -> Value {
}
// The app trims a folder before it uses it.
let hidden = |path: &str| !path.trim().is_empty() && crate::file_access::check_read(path.trim()).is_err();
for key in PATH_PREFS {
for key in FOLDER_PREFS {
let Some(path) = all[key].as_str() else { continue };
if hidden(path) && !(key.starts_with("scratch") && SCRATCH_CHOICES.contains(&path.trim())) {
all[key] = Value::Null;
+10 -1
View File
@@ -12,7 +12,7 @@ fn is_bad_params(r: Result<Value>) -> bool {
}
/// Folder preferences, which a preferences file leaves out.
const FOLDERS: [&str; 4] = ["fontsFolder", "pluginsFolder", "recoveryFolder", "templatesFolder"];
const FOLDERS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"];
/// A preferences file holding `prefs`, as `prefs.export` writes one.
fn file(prefs: Value) -> String {
@@ -46,7 +46,9 @@ fn in_file(s: &Session) -> Value {
fn exported_preferences_import_unchanged() {
let mut s = changed();
s.prefs.fonts_folder = "/fonts/here".into();
s.prefs.scratch_primary = "/scratch/here".into();
let r = s.execute("prefs.export", &json!({})).unwrap();
assert!(!r["data"].as_str().unwrap().contains("/scratch/here"), "a scratch disk folder is not written");
let text = r["data"].as_str().unwrap().to_string();
let written: Value = serde_json::from_str(&text).unwrap();
assert_eq!((written["format"].as_str(), written["version"].as_u64()), (Some(PREFS_FORMAT), Some(1)));
@@ -71,6 +73,7 @@ fn exported_preferences_import_unchanged() {
assert_eq!(t.doc().unwrap().doc.units, Unit::Millimeters, "the document's units stay as they are");
assert_eq!(t.doc().unwrap().history.undo.len(), undo, "no undo step");
assert_eq!(t.prefs.fonts_folder, "", "folders are not carried over");
assert_eq!(t.prefs.scratch_primary, "Startup", "nor is a scratch disk");
// Through a file, too.
let path = std::env::temp_dir().join(format!("vc-prefs-{}.json", std::process::id())).to_string_lossy().to_string();
@@ -96,6 +99,10 @@ fn a_hand_written_file_sets_what_it_names_and_leaves_the_rest() {
assert_eq!(s.prefs.keyboard_increment, 3.0);
assert_eq!(s.prefs.guide_color, "#abcdef");
assert_eq!(s.prefs.history_states, 77, "a preference the file leaves out keeps its value");
// A scratch disk choice names no folder, and is read.
let choices = file(json!({"scratchPrimary": "None", "scratchSecondary": "Startup"}));
assert_eq!(s.execute("prefs.import", &json!({"data": choices})).unwrap(), json!({"imported": 2}));
assert_eq!((s.prefs.scratch_primary.as_str(), s.prefs.scratch_secondary.as_str()), ("None", "Startup"));
}
#[test]
@@ -119,6 +126,8 @@ fn a_bad_preferences_file_is_refused_and_nothing_changes() {
(json!({"data": file(json!({"uiAccent": "blue", "historyStates": 5000}))}), "`historyStates` must be a whole number between 5 and 1000"),
(json!({"data": file(json!({"uiAccent": "blue", "eyedropper": 5}))}), "`eyedropper` must be an object"),
(json!({"data": file(json!({"uiAccent": "blue", "fontsFolder": "/fonts"}))}), "`fontsFolder` names a folder"),
(json!({"data": file(json!({"uiAccent": "blue", "scratchPrimary": "/scratch"}))}), "`scratchPrimary` names a folder"),
(json!({"data": file(json!({"uiAccent": "blue", "scratchSecondary": " scratch "}))}), "`scratchSecondary` names a folder"),
(json!({"data": too_large}), "larger than 1 MB"),
(json!({"data": 5}), "`data` must be a string"),
(json!({"path": "/a.json", "data": file(json!({"uiAccent": "blue"}))}), "give `path` or `data`"),