From 68c044990e7a1575134688e1ec5bb94cfef3eee5 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 16:51:35 -0400 Subject: [PATCH] LRC-LIB-KEYWORDS: keyword lists LightCraft can't read say what to do A list saved as UTF-16, as Windows' Notepad can, now says to save it as UTF-8 instead of only "not UTF-8 text". Text passed to keyword.import is capped at 16 MB, like a file. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/keywords.rs | 7 +++++++ crates/engine/src/tests_keyword_list.rs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/crates/engine/src/cmd/keywords.rs b/crates/engine/src/cmd/keywords.rs index 02c9d0d10..df4750ac4 100644 --- a/crates/engine/src/cmd/keywords.rs +++ b/crates/engine/src/cmd/keywords.rs @@ -110,6 +110,10 @@ fn read_list(path: &str) -> Result { if bytes.len() as u64 > MAX_LIST_BYTES { return Err(bad("keyword.import", format!("{path}: larger than {} MB, not a keyword list", MAX_LIST_BYTES >> 20))); } + // Notepad and some exporters save UTF-16: say how to get the file in, not just that it isn't + if bytes.starts_with(&[0xff, 0xfe]) || bytes.starts_with(&[0xfe, 0xff]) { + return Err(bad("keyword.import", format!("{path}: UTF-16 text — save it as UTF-8 and import it again"))); + } String::from_utf8(bytes).map_err(|_| bad("keyword.import", format!("{path}: not UTF-8 text"))) } @@ -458,6 +462,9 @@ pub fn specs() -> Vec { |s, p| { let text = match (str_param(p, "path").map(str::trim).filter(|x| !x.is_empty()), str_param(p, "text")) { (Some(path), _) => read_list(path)?, + (None, Some(text)) if text.len() as u64 > MAX_LIST_BYTES => { + return Err(bad("keyword.import", format!("larger than {} MB, not a keyword list", MAX_LIST_BYTES >> 20))); + } (None, Some(text)) => text.to_string(), (None, None) => return Err(bad("keyword.import", "give `path` or `text`")), }; diff --git a/crates/engine/src/tests_keyword_list.rs b/crates/engine/src/tests_keyword_list.rs index 2f8b81d3a..d364544dd 100644 --- a/crates/engine/src/tests_keyword_list.rs +++ b/crates/engine/src/tests_keyword_list.rs @@ -296,6 +296,22 @@ fn exporting_keywords_names_what_the_list_cant_hold() { s.execute("keyword.import", &json!({"text": text})).unwrap(); } +/// A list saved as UTF-16 (as Windows' Notepad can) says to save it as UTF-8, and text too big to +/// be a keyword list is refused, as a file that big is. +#[test] +fn importing_keywords_refuses_what_it_cant_read() { + let dir = temp_dir("import-keywords-refused"); + let mut s = Session::with_demo(); + let utf16: Vec = [0xff, 0xfe].into_iter().chain("Travel\r\n".encode_utf16().flat_map(u16::to_le_bytes)).collect(); + std::fs::write(dir.join("travel.txt"), utf16).unwrap(); + let err = s.execute("keyword.import", &json!({"path": dir.join("travel.txt").to_string_lossy()})).unwrap_err().to_string(); + assert!(err.contains("UTF-16") && err.contains("UTF-8"), "{err}"); + let big = "Travel\n".repeat((17 << 20) / 7); + let err = s.execute("keyword.import", &json!({"text": big})).unwrap_err().to_string(); + assert!(err.contains("16 MB"), "{err}"); + let _ = std::fs::remove_dir_all(&dir); +} + /// Import Keywords reads a keyword list file (or text) as one undo step and says what it added; /// a file it can't read says which line, and nothing is taken. #[test]