use std::path::PathBuf;
use serde::Deserialize;
const PROFILE_SCAN_CAP: usize = 32;
#[derive(Debug, Deserialize)]
struct RuntimeDescriptor {
port: u16,
token: String,
}
#[derive(Debug, Clone)]
pub(crate) struct Candidate {
pub base_url: String,
pub token: String,
pub source: PathBuf,
}
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
}
fn is_safe_profile(p: &str) -> bool {
!p.is_empty()
&& p != "local"
&& p.len() <= 128
&& p.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
fn candidate_paths() -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = Vec::new();
if let Some(writ_home) = std::env::var_os("WRIT_HOME") {
paths.push(PathBuf::from(writ_home).join("runtime.json"));
}
if let Some(home) = home_dir() {
let base = home.join(".writ");
if let Ok(profile) = std::fs::read_to_string(base.join("active_profile")) {
let p = profile.trim();
if is_safe_profile(p) {
paths.push(base.join("profiles").join(p).join("runtime.json"));
}
}
paths.push(base.join("runtime.json"));
if let Ok(entries) = std::fs::read_dir(base.join("profiles")) {
for entry in entries.flatten().take(PROFILE_SCAN_CAP) {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
paths.push(entry.path().join("runtime.json"));
}
}
}
}
let mut seen = std::collections::HashSet::new();
paths
.into_iter()
.filter(|p| seen.insert(p.clone()))
.collect()
}
pub(crate) fn runtime_candidates() -> Vec<Candidate> {
candidate_paths()
.into_iter()
.filter_map(|path| {
let bytes = std::fs::read(&path).ok()?;
let desc: RuntimeDescriptor = serde_json::from_slice(&bytes).ok()?;
Some(Candidate {
base_url: format!("http://127.0.0.1:{}", desc.port),
token: desc.token,
source: path,
})
})
.collect()
}
pub(crate) fn env_var(name: &str) -> Option<String> {
std::env::var(name)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_id_validation_mirrors_daemon() {
assert!(is_safe_profile("acct_42-A"));
assert!(!is_safe_profile(""));
assert!(!is_safe_profile("local"));
assert!(!is_safe_profile("../evil"));
assert!(!is_safe_profile("a b"));
assert!(!is_safe_profile(&"x".repeat(129)));
assert!(is_safe_profile(&"x".repeat(128)));
}
}