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