mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 17:12:52 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f9ad15c64 |
@@ -421,30 +421,23 @@ fn add_to_dictionary(_s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// Replace whole-word `from` with `to` in `text`; how many were replaced.
|
||||
fn replace_word(text: &mut String, from: &str, to: &str, all: bool) -> usize {
|
||||
let is_word = |c: char| c.is_alphanumeric() || c == '\'' || c == '’';
|
||||
// The result is built in one pass; the text is not edited in place for every match.
|
||||
let mut out = String::new();
|
||||
let (mut n, mut at, mut copied) = (0, 0, 0);
|
||||
let mut n = 0;
|
||||
let mut at = 0;
|
||||
while let Some(i) = text.get(at..).and_then(|t| t.find(from)).map(|i| i + at) {
|
||||
let end = i + from.len();
|
||||
// The character before the match as the edited text would have it.
|
||||
let before = text.get(copied..i).and_then(|t| t.chars().next_back()).or_else(|| out.chars().next_back()).is_some_and(is_word);
|
||||
let before = text.get(..i).and_then(|t| t.chars().next_back()).is_some_and(is_word);
|
||||
let after = text.get(end..).and_then(|t| t.chars().next()).is_some_and(is_word);
|
||||
if before || after {
|
||||
at = end;
|
||||
continue;
|
||||
}
|
||||
out.push_str(text.get(copied..i).unwrap_or_default());
|
||||
out.push_str(to);
|
||||
text.replace_range(i..end, to);
|
||||
n += 1;
|
||||
(at, copied) = (end, end);
|
||||
at = i + to.len();
|
||||
if !all {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
out.push_str(text.get(copied..).unwrap_or_default());
|
||||
*text = out;
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
@@ -771,49 +764,6 @@ fn end_compare(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// The replacement as it ran before: edited in place for every match.
|
||||
fn replace_word_in_place(text: &mut String, from: &str, to: &str, all: bool) -> usize {
|
||||
let is_word = |c: char| c.is_alphanumeric() || c == '\'' || c == '’';
|
||||
let mut n = 0;
|
||||
let mut at = 0;
|
||||
while let Some(i) = text.get(at..).and_then(|t| t.find(from)).map(|i| i + at) {
|
||||
let end = i + from.len();
|
||||
let before = text.get(..i).and_then(|t| t.chars().next_back()).is_some_and(is_word);
|
||||
let after = text.get(end..).and_then(|t| t.chars().next()).is_some_and(is_word);
|
||||
if before || after {
|
||||
at = end;
|
||||
continue;
|
||||
}
|
||||
text.replace_range(i..end, to);
|
||||
n += 1;
|
||||
at = i + to.len();
|
||||
if !all {
|
||||
break;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_word_matches_the_in_place_version() {
|
||||
let cases = [
|
||||
("a a b a", "a", "x", true),
|
||||
("aa a. a", "a", "bb", true),
|
||||
("a-a -a-", "-", "ab", true),
|
||||
("foo. foo.", "foo.", "", true),
|
||||
("it's it", "it", "so", true),
|
||||
("x.x. x.", "x.", "y", true),
|
||||
("a a a", "a", "b", false),
|
||||
("none", "z", "q", true),
|
||||
("é é", "é", "e", true),
|
||||
];
|
||||
for (text, from, to, all) in cases {
|
||||
let (mut a, mut b) = (text.to_string(), text.to_string());
|
||||
let (na, nb) = (replace_word(&mut a, from, to, all), replace_word_in_place(&mut b, from, to, all));
|
||||
assert_eq!((na, a), (nb, b), "{text:?} {from:?} -> {to:?}");
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ pub fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
// A short fixed-size name: appending to the target's name could exceed the file name limit.
|
||||
let tmp_name = format!(".deckcraft.{}.{}.saving", std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed));
|
||||
let tmp = target_path.with_file_name(tmp_name);
|
||||
// The sibling must pass the same check: when `path` is the write root itself it lands outside.
|
||||
crate::file_access::check_write(&tmp.to_string_lossy())?;
|
||||
match std::fs::OpenOptions::new().write(true).create_new(true).open(&tmp) {
|
||||
Ok(f) => break (tmp, f),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists && attempts < 16 => attempts += 1,
|
||||
@@ -135,4 +137,16 @@ mod tests {
|
||||
|
||||
let _ = std::fs::remove_file(&file);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_file_to_the_write_root_itself_creates_no_sibling_outside_it() {
|
||||
let base = std::env::temp_dir().join(format!("deckcraft_io_root_test_{}", std::process::id()));
|
||||
let root = base.join("root");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let roots = crate::file_access::AutomationRoots::new(&[], std::slice::from_ref(&root)).unwrap().unwrap();
|
||||
let res = crate::file_access::confine(Some(&roots), || write_file(root.to_str().unwrap(), b"x"));
|
||||
assert!(res.unwrap_err().contains("outside the write root"));
|
||||
assert_eq!(std::fs::read_dir(&base).unwrap().count(), 1, "only the root is there");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user