consistent size cap for OS-opened sheet files

read_text_file (drag&drop) capped sheets at 1MB with an extension
whitelist; get_launch_file and the single-instance file-open handler
only checked the extension. Route both through read_sheet_file so a
file association or second launch gets the same cap.
This commit is contained in:
Horizontal42 2026-08-12 19:59:12 +03:00
parent dea7314842
commit 3bc946bd44

View file

@ -15,6 +15,7 @@ use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt; use tauri_plugin_opener::OpenerExt;
const RATES_TTL_SECS: u64 = 3600; const RATES_TTL_SECS: u64 = 3600;
const MAX_SHEET_BYTES: u64 = 1_000_000;
const SAMPLE_JS: &str = include_str!("../extension-sample.js"); const SAMPLE_JS: &str = include_str!("../extension-sample.js");
fn data_dir(app: &AppHandle) -> PathBuf { fn data_dir(app: &AppHandle) -> PathBuf {
@ -750,7 +751,7 @@ fn read_text_file(app: AppHandle, path: String) -> Result<String, String> {
return Err("unsupported file type".into()); return Err("unsupported file type".into());
} }
let meta = fs::metadata(&p).map_err(|e| e.to_string())?; let meta = fs::metadata(&p).map_err(|e| e.to_string())?;
if meta.len() > 1_000_000 { if meta.len() > MAX_SHEET_BYTES {
return Err("file too large".into()); return Err("file too large".into());
} }
fs::read_to_string(p).map_err(|e| e.to_string()) fs::read_to_string(p).map_err(|e| e.to_string())
@ -799,15 +800,22 @@ fn is_sheet_path(path: &str) -> bool {
lower.ends_with(".numi") || lower.ends_with(".sum") lower.ends_with(".numi") || lower.ends_with(".sum")
} }
/// A sheet handed to us by the OS (file association, second launch): same
/// extension whitelist and size cap `read_text_file` applies to drag&drop.
fn read_sheet_file(path: &Path) -> Option<String> {
if !is_sheet_path(&path.to_string_lossy()) {
return None;
}
if fs::metadata(path).ok()?.len() > MAX_SHEET_BYTES {
return None;
}
fs::read_to_string(path).ok()
}
/// Contents of a .numi/.sum file passed on the command line (file association). /// Contents of a .numi/.sum file passed on the command line (file association).
#[tauri::command] #[tauri::command]
fn get_launch_file() -> Option<String> { fn get_launch_file() -> Option<String> {
let arg = std::env::args().nth(1)?; read_sheet_file(Path::new(&std::env::args().nth(1)?))
if is_sheet_path(&arg) {
fs::read_to_string(arg).ok()
} else {
None
}
} }
#[tauri::command] #[tauri::command]
@ -861,7 +869,7 @@ fn main() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
if let Some(path) = args.iter().skip(1).find(|a| is_sheet_path(a)) { if let Some(path) = args.iter().skip(1).find(|a| is_sheet_path(a)) {
if let Ok(content) = fs::read_to_string(path) { if let Some(content) = read_sheet_file(Path::new(path)) {
app.emit("open-file", content).ok(); app.emit("open-file", content).ok();
} }
} }
@ -1030,4 +1038,38 @@ mod tests {
assert!(!map.contains_key("CCC")); assert!(!map.contains_key("CCC"));
assert!(!map.contains_key("DDD")); assert!(!map.contains_key("DDD"));
} }
// read_sheet_file
fn temp_sheet(name: &str, contents: &[u8]) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("summarum-test-sheet-{}", nanos));
fs::create_dir_all(&dir).unwrap();
let path = dir.join(name);
fs::write(&path, contents).unwrap();
path
}
#[test]
fn read_sheet_file_reads_a_small_sheet() {
let path = temp_sheet("budget.numi", b"1 + 1");
assert_eq!(read_sheet_file(&path), Some("1 + 1".to_string()));
fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn read_sheet_file_rejects_a_foreign_extension() {
let path = temp_sheet("notes.txt", b"1 + 1");
assert_eq!(read_sheet_file(&path), None);
fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn read_sheet_file_rejects_an_oversized_sheet() {
let path = temp_sheet("big.numi", &vec![b'1'; (MAX_SHEET_BYTES + 1) as usize]);
assert_eq!(read_sheet_file(&path), None);
fs::remove_dir_all(path.parent().unwrap()).ok();
}
} }