#3692: confined automation adds or removes fonts only when the font store is inside its write root

Finding 44: font.add stored the file in the user font folder and
font.remove deleted stored files directly, bypassing the automation
write root (#769). Both now check the user font folder against the
write policy in force before touching the database or the disk; with no
folder (headless) a font still lasts for the session only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Martin Ball
2026-10-25 21:02:01 -04:00
committed by Dylan M. Taylor
co-authored by Claude Opus 5.5
parent b246ea166a
commit bcce02ab36
+35
View File
@@ -43,6 +43,15 @@ fn read_font_file(path: &str) -> Result<Vec<u8>, CmdError> {
crate::io::read_file_capped(path, wordcraft_fonts::MAX_FONT_FILE_BYTES as u64, "font file").map_err(CmdError::Failed)
}
/// Adding or removing a font writes to the user font folder: automation confined to a write root
/// (#769) may do that only when the folder is inside it.
fn check_font_store(dir: Option<&std::path::Path>) -> Result<(), CmdError> {
match dir {
Some(dir) => crate::file_access::check_write(&dir.to_string_lossy()).map_err(CmdError::Failed),
None => Ok(()),
}
}
fn add(s: &mut Session, v: &Value) -> CmdResult {
if p::str(v, "url").is_some() {
return Err(CmdError::Failed(
@@ -71,6 +80,7 @@ fn add(s: &mut Session, v: &Value) -> CmdResult {
s.push_ui_request(json!({"open": "addFont"}));
return Ok(json!({"pending": true}));
};
check_font_store(FontDb::global().user_font_dir().as_deref())?;
let r = FontDb::global().install_font(&name, &bytes).map_err(|e| CmdError::Failed(e.to_string()))?;
// Text already set in the family (shown in a fallback until now) takes the new font.
s.relayout();
@@ -100,8 +110,33 @@ fn list(_: &mut Session, v: &Value) -> CmdResult {
fn remove(s: &mut Session, v: &Value) -> CmdResult {
let family = p::str(v, "family").filter(|f| !f.trim().is_empty()).ok_or_else(|| CmdError::Params("family is required".into()))?;
check_font_store(FontDb::global().user_font_dir().as_deref())?;
let gone = FontDb::global().remove_user_font(family.trim()).map_err(|e| CmdError::Failed(e.to_string()))?;
s.relayout();
s.status = format!("Removed {}", family.trim());
Ok(json!({"removed": gone.iter().map(font_json).collect::<Vec<_>>()}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn confined_automation_changes_only_a_font_store_inside_its_write_root() {
let root = std::env::temp_dir().join(format!("wordcraft-font-store-{}", std::process::id()));
let outside = std::env::temp_dir().join(format!("wordcraft-font-store-out-{}", std::process::id()));
std::fs::create_dir_all(&root).unwrap();
std::fs::create_dir_all(&outside).unwrap();
let write = crate::file_access::AutomationRoots::new(None, Some(&root)).unwrap();
let read_only = crate::file_access::AutomationRoots::new(Some(&root), None).unwrap();
crate::file_access::confine(write.as_ref(), || {
assert!(check_font_store(None).is_ok(), "no folder: the font lasts for the session");
assert!(check_font_store(Some(&root.join("fonts"))).is_ok());
assert!(check_font_store(Some(&outside)).is_err());
});
crate::file_access::confine(read_only.as_ref(), || assert!(check_font_store(Some(&root)).is_err()));
assert!(check_font_store(Some(&outside)).is_ok(), "unconfined: the person at the keyboard");
let _ = std::fs::remove_dir_all(&root);
let _ = std::fs::remove_dir_all(&outside);
}
}