use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use tokio::fs;
pub struct TerminalStateGuard {
fds: Vec<(i32, libc::termios)>,
}
impl TerminalStateGuard {
pub fn capture() -> Self {
let mut fds = Vec::new();
for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] {
let is_tty = unsafe { libc::isatty(fd) } == 1;
if !is_tty {
continue;
}
let mut termios = unsafe { std::mem::zeroed::<libc::termios>() };
let ok = unsafe { libc::tcgetattr(fd, &mut termios as *mut libc::termios) } == 0;
if ok {
fds.push((fd, termios));
}
}
Self { fds }
}
}
impl Drop for TerminalStateGuard {
fn drop(&mut self) {
for (fd, termios) in &self.fds {
let _ = unsafe { libc::tcsetattr(*fd, libc::TCSANOW, termios as *const libc::termios) };
}
}
}
#[derive(Debug, Clone)]
pub struct AuditLogger {
pub path: PathBuf,
}
impl AuditLogger {
pub fn new(path: PathBuf) -> Self {
Self { path }
}
pub fn write_event(&self, event: serde_json::Value) -> Result<(), color_eyre::Report> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
let serialized = serde_json::to_string(&event)?;
writeln!(file, "{}", serialized)?;
Ok(())
}
}
pub async fn resolve_audit_logger(
audit_log: Option<String>,
id: &str,
) -> Result<Option<AuditLogger>, color_eyre::Report> {
let Some(path_str) = audit_log else {
return Ok(None);
};
let path = if path_str.trim().is_empty() {
let home = std::env::var("HOME")?;
let audit_dir = PathBuf::from(home).join(".cache/tnk/audit");
fs::create_dir_all(&audit_dir).await?;
let ts = crate::sandbox::shared::now_unix_seconds();
audit_dir.join(format!("{}-{}.ndjson", ts, id))
} else {
PathBuf::from(path_str)
};
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent).await?;
}
Ok(Some(AuditLogger::new(path)))
}
pub fn shell_escape(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
pub fn validate_script_name(name: &str) -> Result<(), color_eyre::Report> {
if name.is_empty() {
return Err(color_eyre::eyre::eyre!("invalid profile name: empty"));
}
if name
.chars()
.any(|c| !(c.is_ascii_alphanumeric() || c == '-' || c == '_'))
{
return Err(color_eyre::eyre::eyre!(
"invalid profile name: unsupported characters"
));
}
Ok(())
}
pub fn validate_engine_runtime(runtime: &str) -> Result<(), color_eyre::Report> {
if runtime.is_empty() {
return Err(color_eyre::eyre::eyre!("invalid runtime: empty"));
}
if runtime
.chars()
.any(|c| !(c.is_ascii_alphanumeric() || c == '-' || c == '_'))
{
return Err(color_eyre::eyre::eyre!(
"invalid runtime: unsupported characters"
));
}
Ok(())
}
pub fn validate_env_value(value: &str, field: &str) -> Result<(), color_eyre::Report> {
if value.contains('\0') || value.contains('\n') || value.contains('\r') {
return Err(color_eyre::eyre::eyre!(
"invalid value for {}: contains control characters",
field
));
}
Ok(())
}
pub fn validate_model_name(name: &str) -> Result<(), color_eyre::Report> {
if name.is_empty() {
return Err(color_eyre::eyre::eyre!("invalid model name: empty"));
}
if name
.chars()
.any(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | ':' | '/')))
{
return Err(color_eyre::eyre::eyre!(
"invalid model name: unsupported characters"
));
}
Ok(())
}