mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-04 17:41:13 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9171c6cc | ||
|
|
d72798a58e |
@@ -71,6 +71,8 @@ pub fn write_atomic_with(path: &Path, permissions: Option<Permissions>, fill: im
|
||||
let (mut file, tmp) = create_temp(&dir, permissions.as_ref())?;
|
||||
let write = (|| {
|
||||
if let Some(permissions) = permissions {
|
||||
#[cfg(unix)]
|
||||
let permissions = keep_group(&file, path, permissions);
|
||||
file.set_permissions(permissions)?;
|
||||
}
|
||||
fill(&mut file)?;
|
||||
@@ -90,6 +92,20 @@ pub fn write_atomic_with(path: &Path, permissions: Option<Permissions>, fill: im
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `permissions` for the new file when `path` already exists: the group bits mean "this group" only
|
||||
/// if the file is still in the old file's group, so the group is restored first. When it can't be
|
||||
/// (the user is not a member of it), the group bits are dropped rather than handed to another group.
|
||||
#[cfg(unix)]
|
||||
fn keep_group(file: &File, path: &Path, permissions: Permissions) -> Permissions {
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt, fchown};
|
||||
let Ok(old) = std::fs::symlink_metadata(path) else { return permissions };
|
||||
// A file already in the old group needs no change (and a restrictive syscall policy may refuse a no-op fchown).
|
||||
if !old.is_file() || file.metadata().is_ok_and(|m| m.gid() == old.gid()) || fchown(file, None, Some(old.gid())).is_ok() {
|
||||
return permissions;
|
||||
}
|
||||
Permissions::from_mode(permissions.mode() & !0o070)
|
||||
}
|
||||
|
||||
/// Flush `dir` so a rename just made in it survives a crash or power loss (POSIX records a
|
||||
/// rename in the directory, not in the file). Best effort: some file systems (network shares)
|
||||
/// refuse to sync a directory, and the file itself is already on disk.
|
||||
@@ -231,6 +247,33 @@ mod tests {
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// The replacement keeps the old file's group (when the user is in another group, as `id -G`
|
||||
/// says) together with its mode, so the group bits never name a different group.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn keeps_the_group_and_mode_of_the_file_it_replaces() {
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt, chown};
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-aw-grp-{}-{}", std::process::id(), SEQ.fetch_add(1, Ordering::Relaxed)));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let dest = dir.join("out.bin");
|
||||
std::fs::write(&dest, b"old").unwrap();
|
||||
std::fs::set_permissions(&dest, Permissions::from_mode(0o640)).unwrap();
|
||||
let own = std::fs::metadata(&dest).unwrap().gid();
|
||||
let other = std::process::Command::new("id")
|
||||
.arg("-G")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8_lossy(&o.stdout).split_whitespace().filter_map(|g| g.parse::<u32>().ok()).find(|g| *g != own));
|
||||
if let Some(gid) = other {
|
||||
chown(&dest, None, Some(gid)).unwrap();
|
||||
}
|
||||
let want = std::fs::metadata(&dest).unwrap();
|
||||
write_atomic(&dest, b"new").unwrap();
|
||||
let got = std::fs::metadata(&dest).unwrap();
|
||||
assert_eq!((got.gid(), got.mode() & 0o777), (want.gid(), 0o640));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn does_not_follow_a_planted_temp_symlink() {
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
.noundo(),
|
||||
CommandSpec::new("-xref", "External References", run_dash_xref)
|
||||
.alias(&["-xr"])
|
||||
.params("{option: \"?\"|bind|detach|path|unload|reload|overlay|attach|type, names?: \"A,B*\" (default *), exact?: bool (names are literal xref names, no wildcards or commas), path? (path, attach, overlay), insert?: bool (bind: Insert type), overlay?: bool (type), at?, scale?, rotation? (attach)}")
|
||||
.params("{option: \"?\"|bind|detach|path|unload|reload|overlay|attach|type, names?: \"A,B*\" (default *), path? (path, attach, overlay), insert?: bool (bind: Insert type), overlay?: bool (type), at?, scale?, rotation? (attach)}")
|
||||
.interactive(|_| Ok(Box::new(XrefM::new("-XREF", XStage::Option)))),
|
||||
CommandSpec::new("xbind", "Bind...", run_xbind)
|
||||
.menu(&["Modify", "Object", "External Reference", "Bind..."])
|
||||
@@ -192,19 +192,6 @@ fn run_xattach(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// The xrefs a JSON call names (`names`, default all); an error when none match.
|
||||
fn named(s: &Session, cmd: &str, p: &Value) -> Result<Vec<String>> {
|
||||
if p.get("exact").and_then(Value::as_bool) == Some(true) {
|
||||
let lits: Vec<&str> = match p.get("names") {
|
||||
Some(Value::Array(a)) => a.iter().filter_map(Value::as_str).collect(),
|
||||
Some(Value::String(t)) => vec![t.as_str()],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let d = s.doc()?;
|
||||
let names: Vec<String> = xrefs(d).filter(|b| lits.iter().any(|l| l.eq_ignore_ascii_case(&b.name))).map(|b| b.name.clone()).collect();
|
||||
if names.is_empty() {
|
||||
return Err(bad(cmd, format!("no xref named \"{}\"", lits.join(","))));
|
||||
}
|
||||
return Ok(names);
|
||||
}
|
||||
let pattern = match p.get("names") {
|
||||
Some(Value::Array(a)) => a.iter().filter_map(Value::as_str).collect::<Vec<_>>().join(","),
|
||||
Some(Value::String(t)) => t.clone(),
|
||||
|
||||
@@ -601,6 +601,21 @@ pub fn save_keeps_current(_path: &std::path::Path) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The mode of a new backup: no more open than the drawing (`src_mode`, group `src_gid`) nor than the
|
||||
/// `.bak` it replaces (`old`: mode and group). The backup keeps the old `.bak`'s group, so when that
|
||||
/// differs from the drawing's group the group bits are dropped: that group could not read the drawing.
|
||||
#[cfg(unix)]
|
||||
fn backup_mode(src_mode: u32, src_gid: u32, old: Option<(u32, u32)>) -> u32 {
|
||||
let mut mode = src_mode & 0o777;
|
||||
if let Some((old_mode, old_gid)) = old {
|
||||
mode &= old_mode;
|
||||
if old_gid != src_gid {
|
||||
mode &= !0o070;
|
||||
}
|
||||
}
|
||||
mode
|
||||
}
|
||||
|
||||
/// Before `path` is replaced: copy it to its `.bak`, streamed and written atomically too
|
||||
/// ([`crate::atomic_write::write_atomic_with`]). Nothing
|
||||
/// to keep when there is no file yet. A file that doesn't look like a drawing
|
||||
@@ -625,14 +640,10 @@ fn backup(path: &std::path::Path) -> std::io::Result<()> {
|
||||
// written through.
|
||||
#[cfg(unix)]
|
||||
let permissions = {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut mode = src.metadata()?.permissions().mode() & 0o777;
|
||||
if let Ok(m) = std::fs::symlink_metadata(&bak)
|
||||
&& m.is_file()
|
||||
{
|
||||
mode &= m.permissions().mode();
|
||||
}
|
||||
Some(std::fs::Permissions::from_mode(mode))
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
let sm = src.metadata()?;
|
||||
let old = std::fs::symlink_metadata(&bak).ok().filter(|m| m.is_file());
|
||||
Some(std::fs::Permissions::from_mode(backup_mode(sm.mode(), sm.gid(), old.map(|m| (m.mode(), m.gid())))))
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let permissions = None;
|
||||
@@ -993,6 +1004,19 @@ mod tests {
|
||||
use crate::Session;
|
||||
use crate::temp_dir::TempDir;
|
||||
|
||||
/// A backup is never more open than the drawing or the old `.bak`, and loses the group bits when
|
||||
/// it would keep the old `.bak`'s group while the drawing is in another.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn backup_mode_drops_group_bits_for_a_different_group() {
|
||||
use super::backup_mode;
|
||||
assert_eq!(backup_mode(0o640, 10, None), 0o640);
|
||||
assert_eq!(backup_mode(0o640, 10, Some((0o664, 10))), 0o640);
|
||||
assert_eq!(backup_mode(0o640, 10, Some((0o664, 20))), 0o600);
|
||||
assert_eq!(backup_mode(0o644, 10, Some((0o644, 20))), 0o604);
|
||||
assert_eq!(backup_mode(0o600, 10, Some((0o664, 10))), 0o600);
|
||||
}
|
||||
|
||||
fn active_title(s: &Session) -> String {
|
||||
s.state().unwrap().title.clone()
|
||||
}
|
||||
|
||||
@@ -26,14 +26,7 @@ const MAX_PURGE_PASSES: usize = 64;
|
||||
|
||||
/// The file a SAveas typed at the command line suggests: `<layout>.dwt` beside the drawing.
|
||||
pub(super) fn default_path(s: &Session, layout: &str) -> String {
|
||||
// A layout name from a file may hold path separators or `..`; the suggestion is one file name
|
||||
// beside the drawing, so those become `_`.
|
||||
let name: String = layout.chars().map(|c| if matches!(c, '/' | '\\' | ':') || c.is_control() { '_' } else { c }).collect();
|
||||
// Windows reserves device names (`CON`, `NUL`, `COM1`...) with any extension, so prefix them.
|
||||
let base = name.split('.').next().unwrap_or("").trim_end().to_ascii_uppercase();
|
||||
let reserved = matches!(base.as_str(), "CON" | "PRN" | "AUX" | "NUL")
|
||||
|| (base.len() == 4 && (base.starts_with("COM") || base.starts_with("LPT")) && base.ends_with(|c: char| ('1'..='9').contains(&c)));
|
||||
let file = if reserved { format!("_{name}.dwt") } else { format!("{name}.dwt") };
|
||||
let file = format!("{layout}.dwt");
|
||||
match s.state().ok().and_then(|st| st.path.clone()) {
|
||||
Some(p) => std::path::Path::new(&p).with_file_name(&file).to_string_lossy().into_owned(),
|
||||
None => file,
|
||||
@@ -140,32 +133,3 @@ fn store(path: &str, bytes: &[u8]) -> Result<()> {
|
||||
super::super::exchange::deliver_file("layout.saveas", path, bytes, super::super::file::deliver())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod default_path_tests {
|
||||
use super::*;
|
||||
|
||||
/// A layout name with separators suggests a file beside the drawing, not a path elsewhere.
|
||||
#[test]
|
||||
fn layout_name_is_one_file_name() {
|
||||
let s = Session::empty();
|
||||
for name in ["../../etc/x", "a/b", "a\\b", "C:x"] {
|
||||
let p = default_path(&s, name);
|
||||
assert!(!p.contains('/') && !p.contains('\\') && !p.contains(':'), "{p}");
|
||||
assert!(p.ends_with(".dwt"));
|
||||
}
|
||||
assert_eq!(default_path(&s, "Layout1"), "Layout1.dwt");
|
||||
}
|
||||
|
||||
/// Windows device names are prefixed so the suggestion is never a device.
|
||||
#[test]
|
||||
fn reserved_device_names_are_prefixed() {
|
||||
let s = Session::empty();
|
||||
for name in ["CON", "nul", "Com1", "LPT9", "aux.x"] {
|
||||
assert_eq!(default_path(&s, name), format!("_{name}.dwt"));
|
||||
}
|
||||
for name in ["COM0", "CONSOLE", "COM10"] {
|
||||
assert_eq!(default_path(&s, name), format!("{name}.dwt"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +108,8 @@ fn property(d: &Drawing, e: &Entity, name: &str) -> Option<Value> {
|
||||
|
||||
/// Glob match, case-insensitive: `*` any run, `?` one character, `#` one digit.
|
||||
pub fn wildcard(pat: &str, text: &str) -> bool {
|
||||
// Only the characters that can match are lower-cased, so a huge text isn't copied whole.
|
||||
let p: Vec<char> = pat.chars().take(1024).flat_map(char::to_lowercase).collect();
|
||||
let t: Vec<char> = text.chars().take(4096).flat_map(char::to_lowercase).collect();
|
||||
let p: Vec<char> = pat.to_lowercase().chars().take(1024).collect();
|
||||
let t: Vec<char> = text.to_lowercase().chars().take(4096).collect();
|
||||
// Iterative matcher with single back-track point (linear-ish, no recursion).
|
||||
let (mut pi, mut ti) = (0usize, 0usize);
|
||||
let mut star: Option<(usize, usize)> = None;
|
||||
|
||||
@@ -27,8 +27,7 @@ pub(super) fn open(ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispError>
|
||||
if !writing {
|
||||
match read_text_file(&path) {
|
||||
Some(t) => {
|
||||
// The lines are separate Strings: their headers cost memory the text length hides.
|
||||
charge_bytes(t.len().saturating_add(t.lines().count().saturating_mul(std::mem::size_of::<String>())))?;
|
||||
charge_bytes(t.len())?;
|
||||
f.lines = t.lines().map(str::to_string).collect();
|
||||
}
|
||||
None => return Ok(Flow::Value(Value::Nil)),
|
||||
@@ -101,19 +100,8 @@ fn can_write(_path: &str) -> bool {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn write_text_file(path: &str, text: &str, append: bool) {
|
||||
use std::io::Write;
|
||||
// Opened without blocking and without truncating, so a FIFO with no reader fails at once
|
||||
// instead of stalling the interpreter; only a regular file is then truncated and written.
|
||||
let mut o = std::fs::OpenOptions::new();
|
||||
o.create(true).write(true).append(append);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
o.custom_flags(libc::O_NONBLOCK);
|
||||
}
|
||||
if let Ok(mut f) = o.open(path)
|
||||
&& f.metadata().is_ok_and(|m| m.is_file())
|
||||
&& (append || f.set_len(0).is_ok())
|
||||
{
|
||||
let f = std::fs::OpenOptions::new().create(true).write(true).append(append).truncate(!append).open(path);
|
||||
if let Ok(mut f) = f {
|
||||
let _ = f.write_all(text.as_bytes());
|
||||
}
|
||||
}
|
||||
@@ -130,29 +118,3 @@ fn file_exists(path: &str) -> bool {
|
||||
fn file_exists(_path: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Writing to a FIFO with no reader returns at once, and a regular file is still replaced.
|
||||
#[test]
|
||||
fn write_to_a_fifo_without_a_reader_does_not_block() {
|
||||
// A fresh directory: `create_dir` fails if the name exists, so no stale pipe or planted symlink is reused.
|
||||
let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-lispfifo-{}-{nanos}", std::process::id()));
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
let fifo = dir.join("pipe");
|
||||
assert!(std::process::Command::new("mkfifo").arg(&fifo).status().unwrap().success());
|
||||
let started = std::time::Instant::now();
|
||||
write_text_file(fifo.to_str().unwrap(), "x", false);
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(5));
|
||||
let f = dir.join("f.txt");
|
||||
std::fs::write(&f, "long old text").unwrap();
|
||||
write_text_file(f.to_str().unwrap(), "new", false);
|
||||
assert_eq!(std::fs::read_to_string(&f).unwrap(), "new");
|
||||
write_text_file(f.to_str().unwrap(), "+", true);
|
||||
assert_eq!(std::fs::read_to_string(&f).unwrap(), "new+");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,11 +189,6 @@ fn reload_unload_path_and_detach() {
|
||||
assert_eq!(objects(&s, "site"), 3, "reload reads the file again");
|
||||
assert!(s.doc().unwrap().layer("site|Walls").unwrap().frozen);
|
||||
|
||||
// `exact`: a name is literal, so `#` or `*` in it doesn't reach other xrefs.
|
||||
assert!(s.execute("-xref", &json!({ "option": "reload", "names": "s#te", "exact": true })).is_err());
|
||||
assert!(s.execute("-xref", &json!({ "option": "reload", "names": "*", "exact": true })).is_err());
|
||||
s.execute("-xref", &json!({ "option": "reload", "names": "SITE", "exact": true })).unwrap();
|
||||
|
||||
s.execute("-xref", &json!({ "option": "unload", "names": "site" })).unwrap();
|
||||
assert_eq!((status(&s, "site"), objects(&s, "site")), (Some(XrefStatus::Unloaded), 0));
|
||||
assert!(s.doc().unwrap().block("site|Tree").is_none());
|
||||
|
||||
+3
-28
@@ -43,16 +43,13 @@ fn craft_fonts(dir: &std::path::Path) -> Result<String, String> {
|
||||
rerun_if_changed(&manifest)?;
|
||||
// Resolve the checkout root once so every manifest file can be confined inside it.
|
||||
let root = dir.canonicalize().map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
// The manifest itself is confined too (a symlink in the checkout can't point it elsewhere), must
|
||||
// be a regular file, and is read through a limit so `/dev/zero` can't fill memory.
|
||||
let text = read_manifest(&resolve_font(&root, "fonts/manifest.txt")?)?;
|
||||
let text = std::fs::read_to_string(&manifest).map_err(|e| format!("{}: {e}", manifest.display()))?;
|
||||
let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32");
|
||||
let mut out = String::new();
|
||||
for (n, line) in text.lines().enumerate().map(|(n, l)| (n + 1, l.trim())).filter(|(_, l)| !l.is_empty() && !l.starts_with('#')) {
|
||||
for line in text.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')) {
|
||||
let f: Vec<&str> = line.split(" | ").map(str::trim).collect();
|
||||
// The line's text is not repeated: it comes from the checkout and goes to the build output.
|
||||
let [family, style, file, scripts, ..] = f.as_slice() else {
|
||||
return Err(format!("malformed manifest line {n}"));
|
||||
return Err(format!("malformed manifest line: {line}"));
|
||||
};
|
||||
if BARRED_FAMILIES.iter().any(|barred| family.starts_with(barred)) {
|
||||
println!("cargo::warning=craft-fonts: not embedding {family} {style} (AGENTS.md: no Adobe fonts)");
|
||||
@@ -76,28 +73,6 @@ fn craft_fonts(dir: &std::path::Path) -> Result<String, String> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The most a manifest may hold; the real one is a few KiB.
|
||||
const MAX_MANIFEST_BYTES: u64 = 1 << 20;
|
||||
|
||||
/// Read the regular file `path` as text, at most [`MAX_MANIFEST_BYTES`].
|
||||
fn read_manifest(path: &std::path::Path) -> Result<String, String> {
|
||||
use std::io::Read as _;
|
||||
// stat never blocks, but opening a FIFO with no writer does, so refuse non-regular files first.
|
||||
if !std::fs::metadata(path).is_ok_and(|m| m.is_file()) {
|
||||
return Err(format!("{}: not a regular file", path.display()));
|
||||
}
|
||||
let file = std::fs::File::open(path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
if !file.metadata().is_ok_and(|m| m.is_file()) {
|
||||
return Err(format!("{}: not a regular file", path.display()));
|
||||
}
|
||||
let mut text = String::new();
|
||||
file.take(MAX_MANIFEST_BYTES + 1).read_to_string(&mut text).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
if text.len() as u64 > MAX_MANIFEST_BYTES {
|
||||
return Err(format!("{}: larger than {MAX_MANIFEST_BYTES} bytes", path.display()));
|
||||
}
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
/// Resolve a manifest `file` field to a real path *confined to the checkout* `root` (CWE-22).
|
||||
///
|
||||
/// The field is rejected if it is absolute or contains a parent (`..`) component, and the
|
||||
|
||||
@@ -160,14 +160,12 @@ fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
|
||||
/// Every `.rs` file under `dir`, at any depth, so code split into submodules stays scanned.
|
||||
fn module_files(dir: &std::path::Path, files: &mut Vec<std::path::PathBuf>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return };
|
||||
// `file_type` doesn't follow links, so a symlink (a directory alias or a file) is skipped,
|
||||
// not walked again as a new subtree.
|
||||
let mut paths: Vec<_> = entries.flatten().filter_map(|e| Some((e.path(), e.file_type().ok()?))).collect();
|
||||
paths.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
for (path, kind) in paths {
|
||||
if kind.is_dir() {
|
||||
let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
|
||||
paths.sort();
|
||||
for path in paths {
|
||||
if path.is_dir() {
|
||||
module_files(&path, files);
|
||||
} else if kind.is_file() && path.extension().is_some_and(|x| x == "rs") {
|
||||
} else if path.extension().is_some_and(|x| x == "rs") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ pub fn section(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
fn details(ui: &mut egui::Ui, r: &Row, path_id: egui::Id, act: &mut Option<Act>) {
|
||||
let name = r.name.clone();
|
||||
let loaded = r.status == "Loaded" || r.status == "Unreferenced";
|
||||
let op = |option: &str| json!({ "option": option, "names": name, "exact": true });
|
||||
let op = |option: &str| json!({ "option": option, "names": name });
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.add_space(8.0);
|
||||
if ui.button(crate::tl!("Reload")).clicked() {
|
||||
@@ -216,7 +216,7 @@ fn details(ui: &mut egui::Ui, r: &Row, path_id: egui::Id, act: &mut Option<Act>)
|
||||
.on_hover_text(crate::tl!("Make it a block of this drawing, keeping its layer names"))
|
||||
.clicked()
|
||||
{
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "bind", "names": name, "exact": true, "insert": true })));
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "bind", "names": name, "insert": true })));
|
||||
}
|
||||
if ui.add_enabled(r.status != "Not Found", egui::Button::new(crate::tl!("Open"))).clicked() {
|
||||
*act = Some(Act::Run("xopen", json!({ "name": name })));
|
||||
@@ -230,7 +230,7 @@ fn details(ui: &mut egui::Ui, r: &Row, path_id: egui::Id, act: &mut Option<Act>)
|
||||
let a = ui.radio_value(&mut overlay, false, crate::tl!("Attach")).clicked();
|
||||
let o = ui.radio_value(&mut overlay, true, crate::tl!("Overlay")).clicked();
|
||||
if (a || o) && overlay != r.overlay {
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "type", "names": name, "exact": true, "overlay": overlay })));
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "type", "names": name, "overlay": overlay })));
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
@@ -243,7 +243,7 @@ fn details(ui: &mut egui::Ui, r: &Row, path_id: egui::Id, act: &mut Option<Act>)
|
||||
let apply = ui.add_enabled(!text.trim().is_empty() && text != r.path, egui::Button::new(crate::tl!("Apply"))).clicked()
|
||||
|| (edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && text != r.path);
|
||||
if apply {
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "path", "names": name, "exact": true, "path": text.trim() })));
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "path", "names": name, "path": text.trim() })));
|
||||
ui.data_mut(|d| d.remove::<String>(path_id));
|
||||
} else if text != r.path {
|
||||
ui.data_mut(|d| d.insert_temp(path_id, text));
|
||||
|
||||
+4
-14
@@ -136,18 +136,6 @@ pub fn run(_root: &Path, exact: bool) -> Result<(), String> {
|
||||
mod tests {
|
||||
use super::{Counts, count_tests};
|
||||
|
||||
/// A new private directory in the temp folder. It is created, never reused: an existing path
|
||||
/// (planted by another user, or a symlink) makes the test fail instead of redirecting its writes.
|
||||
fn fresh_dir(tag: &str) -> std::path::PathBuf {
|
||||
let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos());
|
||||
let p = std::env::temp_dir().join(format!("cadcraft-{tag}-{}-{nanos}", std::process::id()));
|
||||
let mut b = std::fs::DirBuilder::new();
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::DirBuilderExt::mode(&mut b, 0o700);
|
||||
b.create(&p).unwrap();
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_attributes_and_proptest_blocks() {
|
||||
let src = "#[test]\nfn a() {}\n// #[test] commented\n #[test]\nfn b() {}\nproptest! {\n}\n";
|
||||
@@ -159,7 +147,8 @@ mod tests {
|
||||
#[test]
|
||||
fn walk_does_not_follow_directory_symlinks() {
|
||||
use std::os::unix::fs::symlink;
|
||||
let base = fresh_dir("xtask");
|
||||
let base = std::env::temp_dir().join(format!("cadcraft-xtask-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
let real = base.join("real");
|
||||
std::fs::create_dir_all(&real).unwrap();
|
||||
std::fs::write(real.join("a.rs"), "fn a() {}\n").unwrap();
|
||||
@@ -173,7 +162,8 @@ mod tests {
|
||||
/// Traversal reports an exhausted entry budget and accepts a tree that exactly fits it.
|
||||
#[test]
|
||||
fn walk_reports_budget_exhaustion() {
|
||||
let base = fresh_dir("stats-budget");
|
||||
let base = std::env::temp_dir().join(format!("cadcraft-stats-budget-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&base).unwrap();
|
||||
std::fs::write(base.join("a.rs"), "fn a() {}\n").unwrap();
|
||||
let error = super::walk_inner(&base, &mut Counts::default(), &mut 0).unwrap_err();
|
||||
assert!(error.contains("budget exhausted"));
|
||||
|
||||
Reference in New Issue
Block a user