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 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-09 16:51:35 -04:00
co-authored by Claude Opus 5.5
parent 91e8a0ac37
commit 68c044990e
2 changed files with 23 additions and 0 deletions
+7
View File
@@ -110,6 +110,10 @@ fn read_list(path: &str) -> Result<String> {
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<CommandSpec> {
|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`")),
};
+16
View File
@@ -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<u8> = [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]