1use std::fs::{File, OpenOptions};
2use std::io::{self, IsTerminal, Read, Write};
3
4pub enum InputSource {
5 Stdin,
6 File(File),
7}
8
9impl InputSource {
10 pub fn new(input: Option<String>) -> io::Result<Self> {
16 if let Some(filename) = input {
17 if filename != "-" {
19 return Ok(Self::File(File::open(filename)?));
20 }
21 }
22
23 Ok(Self::Stdin)
24 }
25
26 #[must_use]
27 pub fn is_terminal(&self) -> bool {
28 matches!(self, Self::Stdin) && io::stdin().is_terminal()
29 }
30}
31
32impl Read for InputSource {
33 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
34 match self {
35 Self::Stdin => io::stdin().read(buf),
36 Self::File(file) => file.read(buf),
37 }
38 }
39}
40
41pub enum OutputDestination {
43 Stdout,
44 File(File),
45}
46
47impl OutputDestination {
48 #[allow(clippy::suspicious_open_options)]
54 pub fn new(output: Option<String>) -> io::Result<Self> {
55 if let Some(filename) = output {
56 if filename != "-" {
58 let mut options = OpenOptions::new();
59 options.write(true).create(true);
60 #[cfg(unix)]
65 {
66 use std::os::unix::fs::OpenOptionsExt;
67 options.mode(0o600);
68 }
69 return Ok(Self::File(options.open(filename)?));
70 }
71 }
72
73 Ok(Self::Stdout)
74 }
75
76 pub fn truncate(&self) -> io::Result<()> {
82 match self {
83 Self::File(file) => file.set_len(0),
84 Self::Stdout => Ok(()), }
86 }
87
88 pub fn is_empty(&self) -> io::Result<bool> {
95 match self {
96 Self::File(file) => Ok(file.metadata().is_ok_and(|metadata| metadata.len() == 0)),
97 Self::Stdout => Ok(true), }
99 }
100}
101
102impl Write for OutputDestination {
103 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
104 match self {
105 Self::Stdout => io::stdout().write(buf),
106 Self::File(file) => file.write(buf),
107 }
108 }
109
110 fn flush(&mut self) -> io::Result<()> {
111 match self {
112 Self::Stdout => io::stdout().flush(),
113 Self::File(file) => file.flush(),
114 }
115 }
116}
117
118pub fn setup_io(
124 input: Option<String>,
125 output: Option<String>,
126) -> io::Result<(InputSource, OutputDestination)> {
127 let input = InputSource::new(input)?;
128 let output = OutputDestination::new(output)?;
129
130 Ok((input, output))
131}
132
133#[cfg(test)]
134#[allow(clippy::unwrap_used)]
135mod tests {
136 use super::*;
137 use tempfile::NamedTempFile;
138
139 #[test]
140 fn test_setup_io() {
141 let stdin_is_terminal = io::stdin().is_terminal();
142
143 let (input, output) = setup_io(None, None).unwrap();
144 assert!(matches!(input, InputSource::Stdin));
145 assert_eq!(input.is_terminal(), stdin_is_terminal);
146 assert!(matches!(output, OutputDestination::Stdout));
147
148 let (input, output) = setup_io(Some("-".to_string()), None).unwrap();
149 assert!(matches!(input, InputSource::Stdin));
150 assert_eq!(input.is_terminal(), stdin_is_terminal);
151 assert!(matches!(output, OutputDestination::Stdout));
152
153 let rs = setup_io(Some("noneexistent".to_string()), None);
154 assert!(rs.is_err());
155 }
156
157 #[test]
158 fn test_setup_io_file() {
159 let output_file = NamedTempFile::new().unwrap();
160
161 let (input, output) = setup_io(Some("Cargo.toml".to_string()), None).unwrap();
162 assert!(!input.is_terminal());
163 assert!(matches!(output, OutputDestination::Stdout));
164
165 let (input, output) =
166 setup_io(Some("Cargo.toml".to_string()), Some("-".to_string())).unwrap();
167 assert!(!input.is_terminal());
168 assert!(matches!(output, OutputDestination::Stdout));
169
170 let (input, output) = setup_io(
171 Some("Cargo.toml".to_string()),
172 Some(output_file.path().to_str().unwrap().to_string()),
173 )
174 .unwrap();
175 assert!(!input.is_terminal());
176 assert!(matches!(output, OutputDestination::File(_)));
177
178 let rs = setup_io(Some("Cargo.toml".to_string()), Some("/".to_string()));
180 assert!(rs.is_err());
181 }
182
183 #[test]
184 fn test_input_source() {
185 let mut input = InputSource::new(Some("Cargo.toml".to_string())).unwrap();
186 let mut buf = [0; 1024];
187 let n = input.read(&mut buf).unwrap();
188 assert!(n > 0);
189
190 let rs = InputSource::new(Some("noneexistent".to_string()));
191 assert!(rs.is_err());
192 }
193
194 #[test]
195 fn test_output_destination() {
196 let mut output = OutputDestination::new(Some("-".to_string())).unwrap();
197 let n = output.write(b"test").unwrap();
198 assert_eq!(n, 4);
199
200 let mut output = OutputDestination::new(None).unwrap();
201 let n = output.write(b"test").unwrap();
202 assert_eq!(n, 4);
203
204 let output_file = NamedTempFile::new().unwrap();
205 let mut output =
206 OutputDestination::new(Some(output_file.path().to_str().unwrap().to_string())).unwrap();
207 let n = output.write(b"test").unwrap();
208 assert_eq!(n, 4);
209 }
210
211 #[test]
212 fn test_output_destination_truncate() {
213 let mut output_file = NamedTempFile::new().unwrap();
214 let mut output =
215 OutputDestination::new(Some(output_file.path().to_str().unwrap().to_string())).unwrap();
216 let n = output.write(b"test").unwrap();
217 assert_eq!(n, 4);
218
219 output.truncate().unwrap();
220 let mut buf = [0; 1024];
221 let n = output_file.read(&mut buf).unwrap();
222 assert_eq!(n, 0);
223 }
224
225 #[cfg(unix)]
226 #[test]
227 fn test_output_destination_new_file_mode() {
228 use std::os::unix::fs::PermissionsExt;
229
230 let dir = tempfile::tempdir().unwrap();
231 let path = dir.path().join("secret.txt");
232
233 let _output = OutputDestination::new(Some(path.to_str().unwrap().to_string())).unwrap();
234
235 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
237 assert_eq!(mode, 0o600);
238 }
239
240 #[test]
241 fn test_output_destination_is_empty() {
242 let output_file = NamedTempFile::new().unwrap();
243 let mut output =
244 OutputDestination::new(Some(output_file.path().to_str().unwrap().to_string())).unwrap();
245 let n = output.write(b"test").unwrap();
246 assert_eq!(n, 4);
247
248 let is_empty = output.is_empty().unwrap();
249 assert!(!is_empty);
250
251 output.truncate().unwrap();
252 let is_empty = output.is_empty().unwrap();
253 assert!(is_empty);
254 }
255}