From 172696bb267b092dbc30e6e23677745415044d02 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:42:30 +0000 Subject: [PATCH] shot_path and its test move from src/live.rs into src/server.rs beside the /api/shot route, so src/live.rs is ember-tune's file whole at the 0.3.17 merge Co-Authored-By: Claude Fable 5.1 --- app/igneum-app/src/live.rs | 17 ----------------- app/igneum-app/src/server.rs | 25 ++++++++++++++++++++++++- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/igneum-app/src/live.rs b/app/igneum-app/src/live.rs index af0ec378..68f7b9dd 100644 --- a/app/igneum-app/src/live.rs +++ b/app/igneum-app/src/live.rs @@ -67,27 +67,10 @@ pub fn fetch(live_api: &str, window_s: u32, ids: &HashSet) -> Value { reply } -/// Where a screenshot lands: `/shots/igneum--.png`, the name reduced to [a-z0-9-] (at most -/// 24 characters, "shot" when empty) so a caller cannot steer the host outside the folder. -pub fn shot_path(data_root: &std::path::Path, name: &str, unix: u64) -> std::path::PathBuf { - let safe: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-').map(|c| c.to_ascii_lowercase()).take(24).collect(); - let safe = if safe.is_empty() { "shot".to_string() } else { safe }; - data_root.join("shots").join(format!("igneum-{safe}-{unix}.png")) -} - #[cfg(test)] mod tests { use super::*; - #[test] - fn a_shot_lands_under_the_data_root_with_a_safe_name() { - let root = std::path::Path::new("/tmp/igneum-data"); - assert_eq!(shot_path(root, "overview", 1791327000), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-overview-1791327000.png")); - assert_eq!(shot_path(root, "../../etc/passwd", 1), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-etcpasswd-1.png")); - assert_eq!(shot_path(root, "", 2), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-shot-2.png")); - assert_eq!(shot_path(root, "Cards Light!", 3), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-cardslight-3.png")); - assert!(shot_path(root, &"x".repeat(80), 4).file_name().unwrap().to_string_lossy().len() < 48); - } // a fixture in the observer's shape (site/api/live.mjs): three miners, this machine is 8fafda27, one of its blocks on // the selected chain and proven, one excluded; a locked and a pending checkpoint diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index b661f979..2be34c31 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -139,6 +139,15 @@ fn json_resp(stream: &mut TcpStream, status: u16, v: Value) { respond(stream, status, "application/json; charset=utf-8", v.to_string().as_bytes(), false); } +/// Where a screenshot lands: `/shots/igneum--.png`, the name reduced to [a-z0-9-] (at most +/// 24 characters, "shot" when empty) so a caller cannot steer the host outside the folder. +pub fn shot_path(data_root: &std::path::Path, name: &str, unix: u64) -> std::path::PathBuf { + let safe: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-').map(|c| c.to_ascii_lowercase()).take(24).collect(); + let safe = if safe.is_empty() { "shot".to_string() } else { safe }; + data_root.join("shots").join(format!("igneum-{safe}-{unix}.png")) +} + + fn query_param(q: &str, key: &str) -> Option { q.split('&').find_map(|kv| { let (k, v) = kv.split_once('=')?; @@ -374,7 +383,7 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result { let name = body.get("name").and_then(|v| v.as_str()).unwrap_or(""); - let path = crate::live::shot_path(&crate::platform::data_root(), name, crate::platform::unix_now()); + let path = shot_path(&crate::platform::data_root(), name, crate::platform::unix_now()); if let Some(dir) = path.parent() { let _ = std::fs::create_dir_all(dir); } println!("SHOT {}", path.display()); Ok(json!({ "ok": true, "path": path.display().to_string(), "note": "the window host writes the PNG within a few seconds; a bare engine without a host writes nothing" })) @@ -387,3 +396,17 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result Err("unknown api".into()), } } + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn a_shot_lands_under_the_data_root_with_a_safe_name() { + let root = std::path::Path::new("/tmp/igneum-data"); + assert_eq!(shot_path(root, "overview", 1791327000), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-overview-1791327000.png")); + assert_eq!(shot_path(root, "../../etc/passwd", 1), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-etcpasswd-1.png")); + assert_eq!(shot_path(root, "", 2), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-shot-2.png")); + assert_eq!(shot_path(root, "Cards Light!", 3), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-cardslight-3.png")); + assert!(shot_path(root, &"x".repeat(80), 4).file_name().unwrap().to_string_lossy().len() < 48); + } +}