5d4a2c8fe1
- Explorer: every non-hidden file is listed with its own icon and tab kind (image, video, audio, pdf, code, text, other); settings show_all_types, hidden_extensions and show_annotation_files; kinds without a viewer open a placeholder card, never the text editor - crates/annotate: non-destructive image annotations (strokes, lines, rectangles, ellipses, text) with strict sidecar JSON validation, hit testing, zoom/pan transforms and undo - engine: file.info, file.hash, file.neighbours, image.annotations.load and save (a corrupt sidecar is kept as .bad, the image is never modified), header-only image sizing for PNG, JPEG, GIF, WebP, BMP and SVG - sync server: publish notes or folders as static HTML (raw HTML escaped, strict CSP, publish: false honoured, no attachments) and expiring revocable share links, with per-route rate limits and audit rows - ui-egui: "Last edit - name, time ago" in the status bar from server.file_meta - specs and designs: file viewers spec, 20 PaperCraft exports rated 8.2 of 10 (design/files/RATING.md), annotate and viewer briefs Verified with cargo xtask ci (fmt, clippy -D warnings, workspace tests, layers, tree, i18n). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
111 lines
5.8 KiB
Rust
111 lines
5.8 KiB
Rust
//! The three file-viewer settings (SPEC `specs/file-viewers/spec.md` item 2):
|
|
//! `show_all_types`, `hidden_extensions` and `show_annotation_files` default the
|
|
//! way the design says, survive an older `config.toml`, repair hostile lists
|
|
//! and travel through `settings.set` / `settings.get` like every other key.
|
|
|
|
mod common;
|
|
|
|
use common::temp_config;
|
|
use notescraft_engine::{AppSettings, Registry, Session, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
/// A throw-away vault with its own settings folder and the core commands.
|
|
fn preview_session(name: &str) -> (common::TempVault, Registry<Session>, Session) {
|
|
let vault = temp_config(name);
|
|
let config = temp_config(&format!("{name}-cfg"));
|
|
let mut session = Session::open(vault.path()).expect("open the vault");
|
|
session.use_settings_dir(config.path());
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register the core commands");
|
|
(config, registry, session)
|
|
}
|
|
|
|
/// The `session.files` group as the settings document holds it.
|
|
fn saved_files(config: &common::TempVault) -> Value {
|
|
let (settings, notes) = notescraft_engine::load_at(config.path());
|
|
assert_eq!(notes, &[] as &[String], "{notes:?}");
|
|
serde_json::to_value(&settings).expect("serialize settings").pointer("/session/files").cloned().unwrap_or_default()
|
|
}
|
|
|
|
/// The three new keys default the way the design says.
|
|
#[test]
|
|
fn the_three_viewer_keys_have_the_design_defaults() {
|
|
let defaults = serde_json::to_value(AppSettings::default()).expect("the default document");
|
|
let defaults = defaults.pointer("/session/files").cloned().unwrap_or_default();
|
|
assert_eq!(defaults.pointer("/show_all_types"), Some(&json!(true)));
|
|
assert_eq!(defaults.pointer("/hidden_extensions"), Some(&json!([])));
|
|
assert_eq!(defaults.pointer("/show_annotation_files"), Some(&json!(false)));
|
|
}
|
|
|
|
/// A `config.toml` written before these keys existed loads with the defaults.
|
|
#[test]
|
|
fn the_viewer_keys_default_for_older_config_files() {
|
|
let fix = temp_config("files-viewers-old");
|
|
std::fs::write(fix.path().join("config.toml"), b"schema_version = 1\n\n[session]\ndaily_folder = \"Notes\"\n\n[session.files]\nmax_attachment_mib = 8\n")
|
|
.expect("write the old file");
|
|
let (settings, notes) = notescraft_engine::load_at(fix.path());
|
|
assert_eq!(notes, &[] as &[String], "{notes:?}");
|
|
assert!(settings.session.files.show_all_types, "all file types on");
|
|
assert_eq!(settings.session.files.hidden_extensions, Vec::<String>::new());
|
|
assert!(!settings.session.files.show_annotation_files);
|
|
}
|
|
|
|
/// `settings.set` and `settings.get` accept all three keys and save them.
|
|
#[test]
|
|
fn the_viewer_keys_round_trip_through_settings_set() {
|
|
let (config, registry, mut session) = preview_session("files-viewers-set");
|
|
let set = |session: &mut Session, key: &str, value: Value| {
|
|
let params = json!({ "key": key, "value": value });
|
|
registry.run("settings.set", session, ¶ms)
|
|
};
|
|
for (key, value) in [
|
|
("session.files.show_all_types", json!(false)),
|
|
("session.files.hidden_extensions", json!(["bin", "tmp"])),
|
|
("session.files.show_annotation_files", json!(true)),
|
|
] {
|
|
let result = set(&mut session, key, value.clone());
|
|
assert!(result.is_ok(), "{key} must be settable: {result:?}");
|
|
let field = key.trim_start_matches("session.files.");
|
|
assert_eq!(saved_files(&config).pointer(&format!("/{field}")), Some(&value), "{key} in config.toml");
|
|
}
|
|
let got = registry.run("settings.get", &mut session, &Value::Null).expect("get");
|
|
let files = got.to_string();
|
|
assert!(files.contains("show_all_types") && files.contains("hidden_extensions"), "{files}");
|
|
}
|
|
|
|
/// A hostile list is repaired (lowered, cleaned, capped) through `settings.set`
|
|
/// and refused by the session's own `validate` before that.
|
|
#[test]
|
|
fn a_hostile_hidden_list_is_repaired_through_settings_set() {
|
|
let (_config, registry, mut session) = preview_session("files-viewers-repair");
|
|
// 74 valid entries in upper case (the repair keeps 64, lowered), plus hostile ones it drops.
|
|
let mut hostile: Vec<Value> = (0..74).map(|n| json!(format!("E{n:02}"))).collect();
|
|
hostile.extend([json!("bad!"), json!(""), json!("way-too-long-an-extension"), json!("a b")]);
|
|
let params = json!({ "key": "session.files.hidden_extensions", "value": hostile });
|
|
let result = registry.run("settings.set", &mut session, ¶ms);
|
|
assert!(result.is_ok(), "the set repairs: {result:?}");
|
|
let applied = registry.run("settings.get", &mut session, &Value::Null).expect("get");
|
|
let kept: Vec<String> = applied
|
|
.pointer("/settings/session/files/hidden_extensions")
|
|
.and_then(Value::as_array)
|
|
.map(|list| list.iter().filter_map(|entry| entry.as_str().map(str::to_owned)).collect())
|
|
.unwrap_or_default();
|
|
assert_eq!(kept.len(), 64, "the list is cut to 64 entries");
|
|
assert_eq!(kept.first().map(String::as_str), Some("e00"), "entries are lowered");
|
|
assert_eq!(kept.last().map(String::as_str), Some("e63"), "the cut keeps the first 64");
|
|
assert!(kept.iter().all(|entry| !entry.chars().any(|c| c.is_ascii_uppercase())), "nothing of the raw case survives");
|
|
assert!(!kept.iter().any(|entry| entry.contains('!') || entry.contains(' ') || entry.is_empty()), "hostile entries are dropped");
|
|
}
|
|
|
|
/// The session refuses a hidden list that is not valid before any repair ran,
|
|
/// such as entries with hostile characters.
|
|
#[test]
|
|
fn a_session_refuses_a_hostile_hidden_list() {
|
|
let vault = temp_config("files-viewers-refuse");
|
|
let mut session = Session::open(vault.path()).expect("open the vault");
|
|
let mut config = session.config().clone();
|
|
config.files.hidden_extensions = vec!["exe;".to_owned()];
|
|
let refused = session.set_config(config);
|
|
assert!(refused.is_err(), "hostile entries are refused: {refused:?}");
|
|
}
|