1use crate::error::CliError;
8use std::fs;
9use std::io::{self, Read, Write};
10use std::path::{Path, PathBuf};
11
12#[must_use]
17pub fn is_stdin(path: &str) -> bool {
18 matches!(path, "-" | "/dev/stdin" | "/dev/fd/0" | "/proc/self/fd/0")
19}
20
21#[must_use]
26pub fn is_stdout(path: &str) -> bool {
27 matches!(path, "-" | "/dev/stdout" | "/dev/fd/1" | "/proc/self/fd/1")
28}
29
30pub struct TempModelFile {
33 path: PathBuf,
34}
35
36impl TempModelFile {
37 #[must_use]
39 pub fn path(&self) -> &Path {
40 &self.path
41 }
42}
43
44impl Drop for TempModelFile {
45 fn drop(&mut self) {
46 let _ = fs::remove_file(&self.path);
47 }
48}
49
50pub fn read_stdin_to_tempfile() -> Result<TempModelFile, CliError> {
55 let mut buf = Vec::new();
56 io::stdin()
57 .lock()
58 .read_to_end(&mut buf)
59 .map_err(|e| CliError::ValidationFailed(format!("Failed to read stdin: {e}")))?;
60
61 if buf.is_empty() {
62 return Err(CliError::ValidationFailed(
63 "No data received on stdin. Pipe a model file: cat model.gguf | apr validate -"
64 .to_string(),
65 ));
66 }
67
68 let tmp_dir = std::env::temp_dir();
69 let tmp_path = tmp_dir.join(format!("apr-stdin-{}.bin", std::process::id()));
70
71 fs::write(&tmp_path, &buf)
72 .map_err(|e| CliError::ValidationFailed(format!("Failed to write temp file: {e}")))?;
73
74 Ok(TempModelFile { path: tmp_path })
75}
76
77pub fn resolve_input(path_str: &str) -> Result<(PathBuf, Option<TempModelFile>), CliError> {
82 if is_stdin(path_str) {
83 let tmp = read_stdin_to_tempfile()?;
84 let p = tmp.path().to_path_buf();
85 Ok((p, Some(tmp)))
86 } else {
87 Ok((PathBuf::from(path_str), None))
88 }
89}
90
91pub fn write_stdout(data: &[u8]) -> Result<(), CliError> {
93 io::stdout()
94 .lock()
95 .write_all(data)
96 .map_err(|e| CliError::ValidationFailed(format!("Failed to write to stdout: {e}")))?;
97 io::stdout()
98 .lock()
99 .flush()
100 .map_err(|e| CliError::ValidationFailed(format!("Failed to flush stdout: {e}")))?;
101 Ok(())
102}
103
104pub fn with_stdin_support<F>(file: &Path, f: F) -> Result<(), CliError>
111where
112 F: FnOnce(&Path) -> Result<(), CliError>,
113{
114 let file_str = file.to_string_lossy();
115 if is_stdin(&file_str) {
116 let tmp = read_stdin_to_tempfile()?;
117 f(tmp.path())
118 } else {
119 let resolved = crate::error::resolve_model_path(file)?;
120 f(&resolved)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_is_stdin() {
130 assert!(is_stdin("-"));
131 assert!(is_stdin("/dev/stdin"));
132 assert!(is_stdin("/dev/fd/0"));
133 assert!(is_stdin("/proc/self/fd/0"));
134 assert!(!is_stdin("model.gguf"));
135 assert!(!is_stdin(""));
136 assert!(!is_stdin("--"));
137 assert!(!is_stdin("/dev/fd/1"));
138 }
139
140 #[test]
141 fn test_is_stdout() {
142 assert!(is_stdout("-"));
143 assert!(is_stdout("/dev/stdout"));
144 assert!(is_stdout("/dev/fd/1"));
145 assert!(is_stdout("/proc/self/fd/1"));
146 assert!(!is_stdout("output.apr"));
147 assert!(!is_stdout("/dev/stdin"));
148 assert!(!is_stdout("/dev/fd/0"));
149 }
150
151 #[test]
152 fn test_resolve_input_file_path() {
153 let (path, tmp) = resolve_input("/tmp/nonexistent.gguf").expect("should resolve");
154 assert_eq!(path, PathBuf::from("/tmp/nonexistent.gguf"));
155 assert!(tmp.is_none());
156 }
157
158 #[test]
159 fn test_temp_model_file_cleanup() {
160 let tmp_path = std::env::temp_dir().join("apr-test-cleanup.bin");
161 fs::write(&tmp_path, b"test data").expect("write");
162 assert!(tmp_path.exists());
163
164 {
165 let _tmp = TempModelFile {
166 path: tmp_path.clone(),
167 };
168 assert!(tmp_path.exists());
169 }
170 assert!(!tmp_path.exists());
172 }
173}