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 contract_pre_pipe_stdin_support!();
115 let file_str = file.to_string_lossy();
116 if is_stdin(&file_str) {
117 let tmp = read_stdin_to_tempfile()?;
118 f(tmp.path())
119 } else {
120 let resolved = crate::error::resolve_model_path(file)?;
121 f(&resolved)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_is_stdin() {
131 assert!(is_stdin("-"));
132 assert!(is_stdin("/dev/stdin"));
133 assert!(is_stdin("/dev/fd/0"));
134 assert!(is_stdin("/proc/self/fd/0"));
135 assert!(!is_stdin("model.gguf"));
136 assert!(!is_stdin(""));
137 assert!(!is_stdin("--"));
138 assert!(!is_stdin("/dev/fd/1"));
139 }
140
141 #[test]
142 fn test_is_stdout() {
143 assert!(is_stdout("-"));
144 assert!(is_stdout("/dev/stdout"));
145 assert!(is_stdout("/dev/fd/1"));
146 assert!(is_stdout("/proc/self/fd/1"));
147 assert!(!is_stdout("output.apr"));
148 assert!(!is_stdout("/dev/stdin"));
149 assert!(!is_stdout("/dev/fd/0"));
150 }
151
152 #[test]
153 fn test_resolve_input_file_path() {
154 let (path, tmp) = resolve_input("/tmp/nonexistent.gguf").expect("should resolve");
155 assert_eq!(path, PathBuf::from("/tmp/nonexistent.gguf"));
156 assert!(tmp.is_none());
157 }
158
159 #[test]
160 fn test_temp_model_file_cleanup() {
161 let tmp_path = std::env::temp_dir().join("apr-test-cleanup.bin");
162 fs::write(&tmp_path, b"test data").expect("write");
163 assert!(tmp_path.exists());
164
165 {
166 let _tmp = TempModelFile {
167 path: tmp_path.clone(),
168 };
169 assert!(tmp_path.exists());
170 }
171 assert!(!tmp_path.exists());
173 }
174}