use crate::error::CliError;
use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
#[must_use]
pub fn is_stdin(path: &str) -> bool {
matches!(path, "-" | "/dev/stdin" | "/dev/fd/0" | "/proc/self/fd/0")
}
#[must_use]
pub fn is_stdout(path: &str) -> bool {
matches!(path, "-" | "/dev/stdout" | "/dev/fd/1" | "/proc/self/fd/1")
}
pub struct TempModelFile {
path: PathBuf,
}
impl TempModelFile {
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempModelFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
pub fn read_stdin_to_tempfile() -> Result<TempModelFile, CliError> {
let mut buf = Vec::new();
io::stdin()
.lock()
.read_to_end(&mut buf)
.map_err(|e| CliError::ValidationFailed(format!("Failed to read stdin: {e}")))?;
if buf.is_empty() {
return Err(CliError::ValidationFailed(
"No data received on stdin. Pipe a model file: cat model.gguf | apr validate -"
.to_string(),
));
}
let tmp_dir = std::env::temp_dir();
let tmp_path = tmp_dir.join(format!("apr-stdin-{}.bin", std::process::id()));
fs::write(&tmp_path, &buf)
.map_err(|e| CliError::ValidationFailed(format!("Failed to write temp file: {e}")))?;
Ok(TempModelFile { path: tmp_path })
}
pub fn resolve_input(path_str: &str) -> Result<(PathBuf, Option<TempModelFile>), CliError> {
if is_stdin(path_str) {
let tmp = read_stdin_to_tempfile()?;
let p = tmp.path().to_path_buf();
Ok((p, Some(tmp)))
} else {
Ok((PathBuf::from(path_str), None))
}
}
pub fn write_stdout(data: &[u8]) -> Result<(), CliError> {
io::stdout()
.lock()
.write_all(data)
.map_err(|e| CliError::ValidationFailed(format!("Failed to write to stdout: {e}")))?;
io::stdout()
.lock()
.flush()
.map_err(|e| CliError::ValidationFailed(format!("Failed to flush stdout: {e}")))?;
Ok(())
}
pub fn with_stdin_support<F>(file: &Path, f: F) -> Result<(), CliError>
where
F: FnOnce(&Path) -> Result<(), CliError>,
{
let file_str = file.to_string_lossy();
if is_stdin(&file_str) {
let tmp = read_stdin_to_tempfile()?;
f(tmp.path())
} else {
let resolved = crate::error::resolve_model_path(file)?;
f(&resolved)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_stdin() {
assert!(is_stdin("-"));
assert!(is_stdin("/dev/stdin"));
assert!(is_stdin("/dev/fd/0"));
assert!(is_stdin("/proc/self/fd/0"));
assert!(!is_stdin("model.gguf"));
assert!(!is_stdin(""));
assert!(!is_stdin("--"));
assert!(!is_stdin("/dev/fd/1"));
}
#[test]
fn test_is_stdout() {
assert!(is_stdout("-"));
assert!(is_stdout("/dev/stdout"));
assert!(is_stdout("/dev/fd/1"));
assert!(is_stdout("/proc/self/fd/1"));
assert!(!is_stdout("output.apr"));
assert!(!is_stdout("/dev/stdin"));
assert!(!is_stdout("/dev/fd/0"));
}
#[test]
fn test_resolve_input_file_path() {
let (path, tmp) = resolve_input("/tmp/nonexistent.gguf").expect("should resolve");
assert_eq!(path, PathBuf::from("/tmp/nonexistent.gguf"));
assert!(tmp.is_none());
}
#[test]
fn test_temp_model_file_cleanup() {
let tmp_path = std::env::temp_dir().join("apr-test-cleanup.bin");
fs::write(&tmp_path, b"test data").expect("write");
assert!(tmp_path.exists());
{
let _tmp = TempModelFile {
path: tmp_path.clone(),
};
assert!(tmp_path.exists());
}
assert!(!tmp_path.exists());
}
}