use std::io;
use std::path::Path;
pub fn parse_bool(v: &str) -> Option<bool> {
match v.to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
}
pub fn write_atomic(path: &Path, contents: &str) -> io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let name = path.file_name().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"write_atomic: path has no file name",
)
})?;
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut tmp_name = name.to_owned();
tmp_name.push(format!(".tmp.{}.{}", std::process::id(), nanos));
let tmp = path.with_file_name(tmp_name);
if let Err(e) = std::fs::write(&tmp, contents) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
if let Err(e) = std::fs::rename(&tmp, path) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{parse_bool, write_atomic};
#[test]
fn write_atomic_creates_parent_and_replaces_existing() {
let dir = std::env::temp_dir().join(format!("tui-common-atomic-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("nested/deep/state.toml");
write_atomic(&path, "first").expect("first write");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "first");
write_atomic(&path, "second").expect("second write");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "second");
let parent = path.parent().unwrap();
for entry in std::fs::read_dir(parent).unwrap() {
let name = entry.unwrap().file_name();
let name = name.to_string_lossy();
assert!(
!name.starts_with("state.toml.tmp."),
"leftover temp file: {name}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn write_atomic_temp_names_are_unique_per_call() {
use std::collections::HashSet;
let dir =
std::env::temp_dir().join(format!("tui-common-atomic-unique-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("state.txt");
let mut seen: HashSet<String> = HashSet::new();
for i in 0..16 {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let synthetic = format!("state.txt.tmp.{}.{}.{}", std::process::id(), nanos, i);
assert!(seen.insert(synthetic), "temp name collided in tight loop");
write_atomic(&path, &i.to_string()).unwrap();
}
assert_eq!(std::fs::read_to_string(&path).unwrap(), "15");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parse_bool_accepts_canonical_forms() {
for s in ["true", "1", "yes", "on", "ON", "Yes", "TRUE"] {
assert_eq!(parse_bool(s), Some(true), "expected true for {s:?}");
}
for s in ["false", "0", "no", "off", "OFF", "No"] {
assert_eq!(parse_bool(s), Some(false), "expected false for {s:?}");
}
for s in ["", "maybe", "2", "trueish"] {
assert_eq!(parse_bool(s), None, "expected None for {s:?}");
}
}
}