mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 17:12:52 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a02d1d386 | ||
|
|
6af917e869 |
@@ -48,8 +48,6 @@ 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,
|
||||
@@ -137,16 +135,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,9 @@ impl Dictionary {
|
||||
["COMPOUNDWORDMAX", n, ..] => d.compound_max = n.parse().unwrap_or(0),
|
||||
["REP", n] => {
|
||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
||||
// The count is the file's claim: stop when the records run out.
|
||||
let Some(line) = lines.next() else { break };
|
||||
let r: Vec<&str> = line.split_whitespace().collect();
|
||||
if let [_, a, b, ..] = r.as_slice() {
|
||||
d.rep.push((a.replace('_', " "), b.replace('_', " ")));
|
||||
}
|
||||
@@ -182,7 +184,8 @@ impl Dictionary {
|
||||
let Some(flag) = one_flag(Some(flag), mode) else { continue };
|
||||
let cross = cross.eq_ignore_ascii_case("y");
|
||||
for _ in 0..n.parse::<usize>().unwrap_or(0) {
|
||||
let r: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
|
||||
let Some(line) = lines.next() else { break };
|
||||
let r: Vec<&str> = line.split_whitespace().collect();
|
||||
let [_, _, strip, add, rest @ ..] = r.as_slice() else { continue };
|
||||
let (add, cont) = match add.split_once('/') {
|
||||
Some((a, c)) => (a, parse_flags(c, mode)),
|
||||
@@ -576,6 +579,15 @@ fn cache() -> &'static Cache {
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// Language strings remembered by [`dictionary`]; a string past it is resolved again on each call.
|
||||
const MAX_CACHED_LANGS: usize = 256;
|
||||
|
||||
/// Parsed dictionaries by their affix file, so aliases of one language share it.
|
||||
fn by_files() -> &'static Mutex<HashMap<PathBuf, Arc<Dictionary>>> {
|
||||
static M: OnceLock<Mutex<HashMap<PathBuf, Arc<Dictionary>>>> = OnceLock::new();
|
||||
M.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// The dictionary for `lang` (a BCP-47 tag such as `en-US`), loaded once per process.
|
||||
pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||
let cache = cache();
|
||||
@@ -586,10 +598,23 @@ pub fn dictionary(lang: &str) -> Option<Arc<Dictionary>> {
|
||||
return d.clone();
|
||||
}
|
||||
let loaded = find_in(&search_dirs(), lang).and_then(|(a, d)| {
|
||||
let (a, d) = (std::fs::read(a).ok()?, std::fs::read(d).ok()?);
|
||||
Some(Arc::new(Dictionary::parse(&a, &d)))
|
||||
// Tags that resolve to the same files share one parsed dictionary.
|
||||
if let Ok(m) = by_files().lock()
|
||||
&& let Some(dict) = m.get(&a)
|
||||
{
|
||||
return Some(dict.clone());
|
||||
}
|
||||
let (bytes_a, bytes_d) = (std::fs::read(&a).ok()?, std::fs::read(d).ok()?);
|
||||
let dict = Arc::new(Dictionary::parse(&bytes_a, &bytes_d));
|
||||
// A concurrent loader of the same files may have inserted first: share its copy.
|
||||
if let Ok(mut m) = by_files().lock() {
|
||||
return Some(m.entry(a).or_insert(dict).clone());
|
||||
}
|
||||
Some(dict)
|
||||
});
|
||||
if let Ok(mut c) = cache.lock() {
|
||||
if let Ok(mut c) = cache.lock()
|
||||
&& c.len() < MAX_CACHED_LANGS
|
||||
{
|
||||
c.insert(key, loaded.clone());
|
||||
}
|
||||
loaded
|
||||
@@ -880,6 +905,14 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
assert!(d.suggest("workingslides", 5).contains(&"working slides".to_string()));
|
||||
}
|
||||
|
||||
/// An affix header can claim billions of records; parsing stops where the file does.
|
||||
#[test]
|
||||
fn a_header_count_past_the_records_ends_with_them() {
|
||||
let aff = b"SFX S Y 4000000000\nSFX S 0 s .\nREP 4000000000\nREP a b\n";
|
||||
let d = Dictionary::parse(aff, b"1\ncat/S\n".as_slice());
|
||||
assert!(d.check("cats"));
|
||||
}
|
||||
|
||||
/// #1175: compound splitting is quadratic in the word's length, so a word longer than
|
||||
/// `MAX_CHECK_CHARS` is not checked; shorter ones still are.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user