mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 08:56:08 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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`"),
|
||||
|
||||
Reference in New Issue
Block a user