mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 08:52:05 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52e553446f |
@@ -234,9 +234,10 @@ fn tables(d: &mut Drawing, out: &mut Vec<Issue>) {
|
||||
missing("table style", "Standard", out);
|
||||
d.table_styles.insert(0, TableStyle::default());
|
||||
}
|
||||
let linetypes: Vec<String> = d.linetypes.iter().map(|l| l.name.clone()).collect();
|
||||
// Name sets built once: both tables come from the drawing, so a scan per reference is quadratic.
|
||||
let linetypes: HashSet<String> = d.linetypes.iter().map(|l| l.name.to_ascii_lowercase()).collect();
|
||||
for l in &mut d.layers {
|
||||
if !linetypes.iter().any(|n| n.eq_ignore_ascii_case(&l.linetype)) {
|
||||
if !linetypes.contains(&l.linetype.to_ascii_lowercase()) {
|
||||
out.push(issue(
|
||||
"missingLinetype",
|
||||
"tables",
|
||||
@@ -247,8 +248,8 @@ fn tables(d: &mut Drawing, out: &mut Vec<Issue>) {
|
||||
l.linetype = "Continuous".into();
|
||||
}
|
||||
}
|
||||
let styles: Vec<String> = d.text_styles.iter().map(|s| s.name.clone()).collect();
|
||||
let known = |n: &str| styles.iter().any(|s| s.eq_ignore_ascii_case(n));
|
||||
let styles: HashSet<String> = d.text_styles.iter().map(|s| s.name.to_ascii_lowercase()).collect();
|
||||
let known = |n: &str| styles.contains(&n.to_ascii_lowercase());
|
||||
for s in &mut d.dim_styles {
|
||||
if !known(&s.text_style) {
|
||||
out.push(issue(
|
||||
|
||||
@@ -405,11 +405,10 @@ mod native {
|
||||
pub fn prune_locks(dir: &Path) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return };
|
||||
let names: Vec<String> = entries.flatten().take(10_000).map(|e| e.file_name().to_string_lossy().to_string()).collect();
|
||||
// The instances that own autosave files, parsed once rather than once per lock file.
|
||||
let with_files: std::collections::HashSet<&str> = names.iter().filter_map(|f| parse_file_name(f)).map(|(_, i, _)| i).collect();
|
||||
for n in &names {
|
||||
let Some(instance) = parse_lock_name(n) else { continue };
|
||||
if !with_files.contains(instance) && !owner_alive(dir, instance) {
|
||||
let has_files = names.iter().any(|f| parse_file_name(f).is_some_and(|(_, i, _)| i == instance));
|
||||
if !has_files && !owner_alive(dir, instance) {
|
||||
let _ = std::fs::remove_file(dir.join(n));
|
||||
}
|
||||
}
|
||||
@@ -436,22 +435,7 @@ mod native {
|
||||
}
|
||||
// Create a fresh temporary file exclusively and rename it into place, so a planted
|
||||
// `<path>.tmp` symlink in a shared autosave directory cannot redirect the write (CWE-59).
|
||||
// The file holds unsaved work: owner-only from its creation, whatever the umask.
|
||||
crate::atomic_write::write_atomic_with(Path::new(path), private_mode(), |f| f.write_all(&bytes)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Owner read/write only (`None` where file modes don't exist).
|
||||
fn private_mode() -> Option<std::fs::Permissions> {
|
||||
#[cfg(unix)]
|
||||
return Some(std::os::unix::fs::PermissionsExt::from_mode(0o600));
|
||||
#[cfg(not(unix))]
|
||||
None
|
||||
}
|
||||
|
||||
/// Write this app's lock file at `path` without following a link planted there: the file is
|
||||
/// replaced (exclusive temporary file, then rename), never written through (CWE-59).
|
||||
fn write_lock(path: &Path, id: u32) -> std::io::Result<()> {
|
||||
crate::atomic_write::write_atomic_with(path, private_mode(), |f| f.write_all(id.to_string().as_bytes()))
|
||||
crate::atomic_write::write_atomic(Path::new(path), &bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
impl Autosaver {
|
||||
@@ -602,7 +586,7 @@ mod native {
|
||||
for (held, file) in &self.locks {
|
||||
let refreshed = match file {
|
||||
Some(file) => file.set_modified(SystemTime::now()),
|
||||
None => write_lock(held, id),
|
||||
None => std::fs::write(held, id.to_string()),
|
||||
};
|
||||
if held == path {
|
||||
current = Some(refreshed);
|
||||
@@ -611,22 +595,10 @@ mod native {
|
||||
if let Some(refreshed) = current {
|
||||
return refreshed;
|
||||
}
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true).create(true).truncate(false);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
// A symlink planted at the lock path must not be opened (and truncated) through,
|
||||
// nor a FIFO wait for a reader.
|
||||
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK).mode(0o600);
|
||||
}
|
||||
let mut file = options.open(path)?;
|
||||
if !file.metadata()?.is_file() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "the lock file is not a plain file"));
|
||||
}
|
||||
let mut file = OpenOptions::new().write(true).create(true).truncate(false).open(path)?;
|
||||
if file.try_lock().is_err() {
|
||||
self.locks.push((path.to_path_buf(), None));
|
||||
return write_lock(path, id);
|
||||
return std::fs::write(path, id.to_string());
|
||||
}
|
||||
file.set_len(0)?;
|
||||
file.write_all(format!("{id}{LOCK_HELD}").as_bytes())?;
|
||||
@@ -655,30 +627,6 @@ mod native {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A symlink planted at the lock path is neither opened through nor written through.
|
||||
#[test]
|
||||
fn lock_writes_do_not_follow_a_planted_link() {
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-lock-link-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let victim = dir.join("victim.txt");
|
||||
std::fs::write(&victim, b"precious").unwrap();
|
||||
let link = dir.join("1-a.lock");
|
||||
std::os::unix::fs::symlink(&victim, &link).unwrap();
|
||||
let mut a = Autosaver::new("1-a");
|
||||
assert!(a.refresh_lock(&link).is_err());
|
||||
assert_eq!(std::fs::read(&victim).unwrap(), b"precious");
|
||||
write_lock(&link, 42).unwrap();
|
||||
assert_eq!(std::fs::read(&victim).unwrap(), b"precious");
|
||||
assert!(!std::fs::symlink_metadata(&link).unwrap().file_type().is_symlink());
|
||||
assert_eq!(std::fs::read(&link).unwrap(), b"42");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -408,12 +408,24 @@ fn run_state_restore(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("layerstate.restore", "`name` is required"))?;
|
||||
let d = s.doc_mut()?;
|
||||
let st = d.layer_states.iter().find(|st| st.name == name).cloned().ok_or_else(|| bad("layerstate.restore", "no such layer state"))?;
|
||||
for saved in st.layers {
|
||||
if let Some(l) = d.layer_mut(&saved.name) {
|
||||
*l = saved;
|
||||
restore_layers(d, st.layers);
|
||||
ok()
|
||||
}
|
||||
|
||||
/// Put the saved layers back over the drawing's layers of the same name (without regard to case);
|
||||
/// saved layers the drawing no longer has are skipped. Both lists can come from a file, so the
|
||||
/// drawing's layers are looked up in an index rather than scanned per saved layer.
|
||||
pub(crate) fn restore_layers(d: &mut cadcraft_doc::Drawing, saved: Vec<Layer>) {
|
||||
let mut index: std::collections::HashMap<String, usize> = std::collections::HashMap::with_capacity(d.layers.len());
|
||||
for (i, l) in d.layers.iter().enumerate() {
|
||||
// The first of names that differ only in case, as `Drawing::layer_mut` finds it.
|
||||
index.entry(l.name.to_ascii_lowercase()).or_insert(i);
|
||||
}
|
||||
for layer in saved {
|
||||
if let Some(l) = index.get(&layer.name.to_ascii_lowercase()).and_then(|&i| d.layers.get_mut(i)) {
|
||||
*l = layer;
|
||||
}
|
||||
}
|
||||
ok()
|
||||
}
|
||||
|
||||
fn run_state_list(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
@@ -461,6 +473,22 @@ mod tests {
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// Restoring many saved layers, most of them missing, takes one pass and matches without case.
|
||||
#[test]
|
||||
fn restore_layers_matches_by_name_in_linear_time() {
|
||||
let mut d = cadcraft_doc::Drawing::new_imperial();
|
||||
d.layers = (0..20_000).map(|i| cadcraft_doc::Layer::new(&format!("L{i}"))).collect();
|
||||
let mut saved: Vec<cadcraft_doc::Layer> = (0..20_000).map(|i| cadcraft_doc::Layer::new(&format!("missing{i}"))).collect();
|
||||
let mut back = cadcraft_doc::Layer::new("l7");
|
||||
back.frozen = true;
|
||||
saved.push(back);
|
||||
let started = std::time::Instant::now();
|
||||
super::restore_layers(&mut d, saved);
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(2));
|
||||
assert!(d.layer("L7").is_some_and(|l| l.frozen));
|
||||
assert_eq!(d.layers.len(), 20_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_states_save_list_restore_delete() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! The CAL / QuickCalc evaluator: numbers and points (`[x,y]`, `[d<a]`), arithmetic and vector
|
||||
//! arithmetic, numeric and geometric functions; unit conversion; values picked from the drawing.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cadcraft_geom::Vec2;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
@@ -67,6 +69,9 @@ struct Parser<'a> {
|
||||
s: &'a [u8],
|
||||
i: usize,
|
||||
vars: &'a Map<String, Value>,
|
||||
/// `vars` by ASCII-lower-cased name (the first of names that differ only in case), so a
|
||||
/// case-insensitive lookup is not a scan of the map per reference.
|
||||
folded: HashMap<String, &'a Value>,
|
||||
depth: usize,
|
||||
unary_depth: usize,
|
||||
}
|
||||
@@ -230,7 +235,7 @@ impl Parser<'_> {
|
||||
match name.as_str() {
|
||||
"pi" => Ok(Val::Num(std::f64::consts::PI)),
|
||||
"e" => Ok(Val::Num(std::f64::consts::E)),
|
||||
_ => var(self.vars, &name).ok_or_else(|| format!("unknown name `{name}`")),
|
||||
_ => var(self.vars, &self.folded, &name).ok_or_else(|| format!("unknown name `{name}`")),
|
||||
}
|
||||
}
|
||||
_ => Err("expected a number".into()),
|
||||
@@ -239,8 +244,8 @@ impl Parser<'_> {
|
||||
}
|
||||
|
||||
/// A variable: a number, or a point as `[x, y]`. Names are case-insensitive.
|
||||
fn var(vars: &Map<String, Value>, name: &str) -> Option<Val> {
|
||||
let v = vars.get(name).or_else(|| vars.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)).map(|(_, v)| v))?;
|
||||
fn var(vars: &Map<String, Value>, folded: &HashMap<String, &Value>, name: &str) -> Option<Val> {
|
||||
let v = vars.get(name).or_else(|| folded.get(&name.to_ascii_lowercase()).copied())?;
|
||||
if let Some(n) = v.as_f64() {
|
||||
return Some(Val::Num(n));
|
||||
}
|
||||
@@ -411,7 +416,11 @@ pub fn eval_val(expr: &str, vars: &Map<String, Value>) -> Result<Val, String> {
|
||||
if expr.len() > MAX_LEN {
|
||||
return Err("expression too long".into());
|
||||
}
|
||||
let mut p = Parser { s: expr.as_bytes(), i: 0, vars, depth: 0, unary_depth: 0 };
|
||||
let mut folded = HashMap::with_capacity(vars.len());
|
||||
for (k, v) in vars {
|
||||
folded.entry(k.to_ascii_lowercase()).or_insert(v);
|
||||
}
|
||||
let mut p = Parser { s: expr.as_bytes(), i: 0, vars, folded, depth: 0, unary_depth: 0 };
|
||||
let v = p.expr()?;
|
||||
if p.peek().is_some() {
|
||||
return Err(format!("unexpected `{}`", expr.get(p.i..).unwrap_or("")));
|
||||
@@ -587,6 +596,21 @@ mod tests {
|
||||
eval_val(s, &Map::new())
|
||||
}
|
||||
|
||||
/// Variables are found without regard to case; an exact match wins over a case variant.
|
||||
/// (Names are read lower-cased, so `X` finds `x` too.)
|
||||
#[test]
|
||||
fn variables_ignore_case_and_prefer_exact_names() {
|
||||
let mut vars = Map::new();
|
||||
for i in 0..10_000 {
|
||||
vars.insert(format!("v{i}"), json!(i));
|
||||
}
|
||||
vars.insert("X".into(), json!(1));
|
||||
vars.insert("x".into(), json!(2));
|
||||
assert!(matches!(eval_val("V9999 + 1", &vars), Ok(Val::Num(n)) if n == 10_000.0));
|
||||
assert!(matches!(eval_val("x", &vars), Ok(Val::Num(n)) if n == 2.0));
|
||||
assert!(eval_val("nope", &vars).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn points_and_vectors() {
|
||||
assert_eq!(ev("[1,2]+[3,4]*2"), Ok(Val::Pt(Vec2::new(7.0, 10.0))));
|
||||
|
||||
@@ -185,13 +185,7 @@ fn restore(s: &mut Session, name: &str) -> Result<Value> {
|
||||
if let Some(state) = &v.layer_state {
|
||||
let d = s.doc_mut()?;
|
||||
match d.layer_states.iter().find(|l| l.name.eq_ignore_ascii_case(state)).cloned() {
|
||||
Some(saved) => {
|
||||
for layer in saved.layers {
|
||||
if let Some(l) = d.layer_mut(&layer.name) {
|
||||
*l = layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(saved) => super::super::layer::restore_layers(d, saved.layers),
|
||||
None => msg.push_str(&format!(" Its layer snapshot \"{state}\" is missing.")),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user