1use anyhow::{Context, Result, bail};
2use std::fs;
3use std::io::{self, Read};
4
5const MAX_INPUT_SIZE: usize = 1024 * 1024;
7
8pub struct InputReader;
12
13impl InputReader {
14 pub fn read(file_path: Option<&str>) -> Result<String> {
25 file_path.map_or_else(Self::read_stdin, Self::read_file)
26 }
27
28 fn read_file(path: &str) -> Result<String> {
29 let metadata =
30 fs::metadata(path).with_context(|| format!("Failed to access file: {path}"))?;
31
32 let size = metadata.len() as usize;
33 if size > MAX_INPUT_SIZE {
34 bail!(
35 "Input size ({:.1} MB) exceeds maximum allowed size (1 MB).\n\n\
36 Consider splitting the file into smaller parts.",
37 size as f64 / 1024.0 / 1024.0
38 );
39 }
40
41 fs::read_to_string(path).with_context(|| format!("Failed to read file: {path}"))
42 }
43
44 #[allow(clippy::significant_drop_tightening)]
45 fn read_stdin() -> Result<String> {
46 let mut buffer = Vec::new();
47 let mut chunk = [0u8; 8192];
48 let mut stdin = io::stdin().lock();
49
50 loop {
51 let bytes_read = stdin
52 .read(&mut chunk)
53 .context("Failed to read from stdin")?;
54
55 if bytes_read == 0 {
56 break;
57 }
58
59 buffer.extend_from_slice(&chunk[..bytes_read]);
60
61 if buffer.len() > MAX_INPUT_SIZE {
62 bail!(
63 "Input size ({:.1} MB) exceeds maximum allowed size (1 MB).\n\n\
64 Consider splitting the input into smaller parts.",
65 buffer.len() as f64 / 1024.0 / 1024.0
66 );
67 }
68 }
69
70 String::from_utf8(buffer).context("Input is not valid UTF-8")
71 }
72}
73
74#[cfg(test)]
75#[allow(clippy::unwrap_used)]
76mod tests {
77 use super::*;
78 use std::io::Write;
79 use tempfile::{NamedTempFile, TempDir};
80
81 #[test]
82 fn test_read_file() {
83 let mut temp_file = NamedTempFile::new().unwrap();
84 writeln!(temp_file, "Hello, World!").unwrap();
85
86 let content = InputReader::read(Some(temp_file.path().to_str().unwrap())).unwrap();
87 assert_eq!(content.trim(), "Hello, World!");
88 }
89
90 #[test]
91 fn test_read_nonexistent_file() {
92 let result = InputReader::read(Some("/nonexistent/path/to/file.txt"));
93 assert!(result.is_err());
94 }
95
96 #[test]
97 fn test_max_input_size_constant() {
98 assert_eq!(MAX_INPUT_SIZE, 1024 * 1024);
99 }
100
101 #[test]
102 fn test_read_file_unicode() {
103 let mut temp_file = NamedTempFile::new().unwrap();
104 let content = "こんにちは世界!🌍\n日本語テスト";
105 write!(temp_file, "{content}").unwrap();
106
107 let result = InputReader::read(Some(temp_file.path().to_str().unwrap())).unwrap();
108 assert_eq!(result, content);
109 }
110
111 #[test]
112 fn test_read_empty_file() {
113 let temp_file = NamedTempFile::new().unwrap();
114
115 let content = InputReader::read(Some(temp_file.path().to_str().unwrap())).unwrap();
116 assert!(content.is_empty());
117 }
118
119 #[test]
120 fn test_read_file_exceeds_max_size() {
121 let temp_dir = TempDir::new().unwrap();
122 let file_path = temp_dir.path().join("large_file.txt");
123
124 let large_content = "x".repeat(MAX_INPUT_SIZE + 1);
126 fs::write(&file_path, &large_content).unwrap();
127
128 let result = InputReader::read(Some(file_path.to_str().unwrap()));
129 assert!(result.is_err());
130 assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
131 }
132
133 #[test]
134 fn test_read_file_at_max_size() {
135 let temp_dir = TempDir::new().unwrap();
136 let file_path = temp_dir.path().join("max_file.txt");
137
138 let content = "x".repeat(MAX_INPUT_SIZE);
140 fs::write(&file_path, &content).unwrap();
141
142 let result = InputReader::read(Some(file_path.to_str().unwrap()));
143 assert!(result.is_ok());
144 assert_eq!(result.unwrap().len(), MAX_INPUT_SIZE);
145 }
146
147 #[test]
148 fn test_read_file_multiline() {
149 let mut temp_file = NamedTempFile::new().unwrap();
150 let content = "Line 1\nLine 2\nLine 3";
151 write!(temp_file, "{content}").unwrap();
152
153 let result = InputReader::read(Some(temp_file.path().to_str().unwrap())).unwrap();
154 assert_eq!(result, content);
155 }
156}