mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
Audit: automation roots follow file.showPackage, the Place queue, headless file methods, clipboard files, placed-document workers and caches, folder searches and the recovery folder
This commit is contained in:
committed by
bflatastic
parent
8743f63f50
commit
e95e8418ed
@@ -979,6 +979,10 @@ fn may_write(path: &str) -> Result<()> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn read_file(path: &str) -> Result<Vec<u8>> {
|
||||
may_read(path)?;
|
||||
// A device or pipe may never end: only a regular file is read to its end.
|
||||
if !std::fs::metadata(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?.is_file() {
|
||||
return Err(EngineError::Other(format!("{path}: not a file")));
|
||||
}
|
||||
std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
|
||||
}
|
||||
|
||||
|
||||
@@ -259,6 +259,10 @@ pub(crate) fn start_in(
|
||||
}
|
||||
// The walkers follow no links, so a folder automation may read holds all they read.
|
||||
crate::file_access::check_read(&folder.to_string_lossy())?;
|
||||
// The check above reads a lossy string: under roots, a path it would alter names another folder.
|
||||
if folder.to_str().is_none() && crate::file_access::current().is_some() {
|
||||
return Err("automation path rejected: the folder's name isn't valid text".into());
|
||||
}
|
||||
let seconds = if limits.seconds.is_finite() { limits.seconds.clamp(0.0, 3600.0) } else { 60.0 };
|
||||
let n = pool.reserve(threads.clamp(1, MAX_THREADS));
|
||||
if n == 0 {
|
||||
@@ -279,6 +283,7 @@ pub(crate) fn start_in(
|
||||
walk: Mutex::new(Walk { root: Some(folder), alive: n, found: vec![0; items], ..Walk::default() }),
|
||||
wake: Condvar::new(),
|
||||
pool,
|
||||
scope: crate::file_access::ScopeHandle::capture(),
|
||||
});
|
||||
let mut spawned = 0;
|
||||
for _ in 0..n {
|
||||
@@ -325,6 +330,8 @@ struct Shared {
|
||||
/// Woken when folders are queued, a walker finishes a folder, or the search ends.
|
||||
wake: Condvar,
|
||||
pool: &'static Pool,
|
||||
/// The automation roots in force when the search started, for the folders the walkers add.
|
||||
scope: crate::file_access::ScopeHandle,
|
||||
}
|
||||
|
||||
/// The state walkers change under the lock.
|
||||
@@ -490,7 +497,8 @@ impl Shared {
|
||||
}
|
||||
queue.push((path, depth + 1));
|
||||
}
|
||||
let home = rules.home_below(&listed);
|
||||
// The home folder is added by a rule, not picked: it needs the picked folder's authority.
|
||||
let home = rules.home_below(&listed).filter(|h| !rules.is_refused(h) && self.scope.clone().run(|| crate::file_access::check_read(&h.to_string_lossy())).is_ok());
|
||||
{
|
||||
let mut w = self.lock();
|
||||
w.files += files;
|
||||
|
||||
@@ -159,7 +159,11 @@ pub(crate) fn read_document(p: &PlacedDocument, bytes: &[u8]) -> Option<(Documen
|
||||
/// `p`'s file read again, while it is the file it was read from (how the document model gets it
|
||||
/// when only the preview is stored, [`vectorcraft_doc::placed_document::set_file_reader`]).
|
||||
pub(crate) fn read_file_again(p: &PlacedDocument) -> Option<Vec<u8>> {
|
||||
let bytes = fileio::read_file(&p.link.path).ok()?;
|
||||
// One byte over the limit tells a file that is too big, without reading it all.
|
||||
let bytes = fileio::read_file_head(&p.link.path, MAX_FILE + 1).ok()?;
|
||||
if bytes.len() > MAX_FILE {
|
||||
return None;
|
||||
}
|
||||
if p.link.hash.as_ref().is_some_and(|h| *h != hash_bytes(&bytes)) {
|
||||
return None;
|
||||
}
|
||||
@@ -186,7 +190,10 @@ pub(crate) struct Source {
|
||||
pub(crate) fn source(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bounding: bool, page_box: PageBox, cmd: &str) -> Result<Source> {
|
||||
// The same document placed the same way is read once (placing it again is free).
|
||||
let key = key_for(&hash_bytes(bytes), page, bounding, page_box);
|
||||
if let Some(s) = lock_memo().iter().find(|s| s.key == key).cloned() {
|
||||
// Resolving its links depends on where the file is and on what the caller may read, so a hit
|
||||
// needs the same path, and nothing is shared with or from a confined (automation) caller.
|
||||
let confined = crate::file_access::current().is_some();
|
||||
if !confined && let Some((_, s)) = lock_memo().iter().find(|(at, s)| s.key == key && at.as_deref() == path).cloned() {
|
||||
return Ok(s);
|
||||
}
|
||||
let a = document_art(name, bytes, path, page, bounding, page_box, cmd)?;
|
||||
@@ -206,11 +213,13 @@ pub(crate) fn source(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bo
|
||||
// Drawing it needn't read it again.
|
||||
vectorcraft_doc::placed_document::prime(&shown, blob.bytes.clone(), (a.doc, art, a.frame));
|
||||
let s = Source { blob, key, page_box, frame: a.frame, warnings: a.warnings };
|
||||
let mut m = lock_memo();
|
||||
if m.len() >= MEMO_SIZE {
|
||||
m.remove(0);
|
||||
if !confined {
|
||||
let mut m = lock_memo();
|
||||
if m.len() >= MEMO_SIZE {
|
||||
m.remove(0);
|
||||
}
|
||||
m.push((path.map(str::to_string), s.clone()));
|
||||
}
|
||||
m.push(s.clone());
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
@@ -218,9 +227,9 @@ pub(crate) fn source(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bo
|
||||
const MEMO_SIZE: usize = 32;
|
||||
|
||||
/// Files read lately for placing, by key.
|
||||
static MEMO: std::sync::Mutex<Vec<Source>> = std::sync::Mutex::new(Vec::new());
|
||||
static MEMO: std::sync::Mutex<Vec<(Option<String>, Source)>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
fn lock_memo() -> std::sync::MutexGuard<'static, Vec<Source>> {
|
||||
fn lock_memo() -> std::sync::MutexGuard<'static, Vec<(Option<String>, Source)>> {
|
||||
// Entries are only pushed and removed: a panic while held leaves it whole.
|
||||
MEMO.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
@@ -350,7 +350,12 @@ fn load_folder(folder: &str) -> Value {
|
||||
return json!({"folder": folder, "error": e});
|
||||
}
|
||||
// A file in it may be a link to one elsewhere.
|
||||
match registry::load_folder_where(std::path::Path::new(folder), |f| crate::file_access::check_read(&f.to_string_lossy())) {
|
||||
match registry::load_folder_where(std::path::Path::new(folder), |f| match f.to_str() {
|
||||
Some(f) => crate::file_access::check_read(f),
|
||||
// A lossy name would check another file than the one loaded.
|
||||
None if crate::file_access::current().is_none() => Ok(()),
|
||||
None => Err(format!("automation path rejected: {} isn't valid text", f.display())),
|
||||
}) {
|
||||
Ok(r) => {
|
||||
json!({"folder": folder, "loaded": r.loaded, "failed": r.failed.iter().map(|(f, e)| json!({"file": f, "error": e})).collect::<Vec<_>>()})
|
||||
}
|
||||
|
||||
@@ -472,13 +472,14 @@ impl Prefs {
|
||||
|
||||
/// The folders the app reads (fonts, plug-ins) or writes (Data Recovery copies, the Templates
|
||||
/// folder it makes for its dialogs) on its own, with the access each needs.
|
||||
fn folders_mut(&mut self) -> [(&mut String, crate::file_access::Access); 4] {
|
||||
fn folders_mut(&mut self) -> [(&mut String, &'static [crate::file_access::Access]); 4] {
|
||||
use crate::file_access::Access;
|
||||
[
|
||||
(&mut self.fonts_folder, Access::Read),
|
||||
(&mut self.plugins_folder, Access::Read),
|
||||
(&mut self.recovery_folder, Access::Write),
|
||||
(&mut self.templates_folder, Access::Write),
|
||||
(&mut self.fonts_folder, &[Access::Read][..]),
|
||||
(&mut self.plugins_folder, &[Access::Read][..]),
|
||||
// Data Recovery lists and restores what is in its folder too.
|
||||
(&mut self.recovery_folder, &[Access::Write, Access::Read][..]),
|
||||
(&mut self.templates_folder, &[Access::Write][..]),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -494,7 +495,7 @@ impl Prefs {
|
||||
if folder.is_empty() || folder == old.trim() {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = roots.check(folder, access) {
|
||||
if let Err(e) = access.iter().try_for_each(|a| roots.check(folder, *a)) {
|
||||
refused.push(e);
|
||||
*new = old.clone();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user