fast_yaml_cli/batch/
reader.rs1use std::fs::File;
4use std::path::Path;
5
6use memmap2::Mmap;
7
8use super::error::ProcessingError;
9
10const MMAP_THRESHOLD: u64 = 512 * 1024;
12
13pub enum FileContent {
15 String(String),
17 Mmap(Mmap),
19}
20
21impl FileContent {
22 pub fn as_str(&self) -> Result<&str, ProcessingError> {
27 match self {
28 Self::String(s) => Ok(s),
29 Self::Mmap(mmap) => std::str::from_utf8(mmap).map_err(ProcessingError::Utf8Error),
30 }
31 }
32
33 pub const fn is_mmap(&self) -> bool {
35 matches!(self, Self::Mmap(_))
36 }
37
38 pub fn len(&self) -> usize {
40 match self {
41 Self::String(s) => s.len(),
42 Self::Mmap(mmap) => mmap.len(),
43 }
44 }
45
46 pub fn is_empty(&self) -> bool {
48 self.len() == 0
49 }
50}
51
52pub struct SmartFileReader {
57 mmap_threshold: u64,
58}
59
60impl SmartFileReader {
61 pub const fn new() -> Self {
63 Self::with_threshold(MMAP_THRESHOLD)
64 }
65
66 pub const fn with_threshold(threshold: u64) -> Self {
68 Self {
69 mmap_threshold: threshold,
70 }
71 }
72
73 pub fn read(&self, path: &Path) -> Result<FileContent, ProcessingError> {
81 let metadata = std::fs::metadata(path).map_err(ProcessingError::ReadError)?;
82
83 let size = metadata.len();
84
85 if size >= self.mmap_threshold {
86 Self::read_mmap(path).or_else(|_| {
87 Self::read_string(path)
89 })
90 } else {
91 Self::read_string(path)
92 }
93 }
94
95 fn read_string(path: &Path) -> Result<FileContent, ProcessingError> {
97 let content = std::fs::read_to_string(path).map_err(ProcessingError::ReadError)?;
98 Ok(FileContent::String(content))
99 }
100
101 #[allow(unsafe_code)]
105 fn read_mmap(path: &Path) -> Result<FileContent, ProcessingError> {
106 let file = File::open(path).map_err(ProcessingError::MmapError)?;
107
108 let mmap = unsafe { Mmap::map(&file).map_err(ProcessingError::MmapError)? };
117
118 Ok(FileContent::Mmap(mmap))
119 }
120}
121
122impl Default for SmartFileReader {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use std::io::Write;
132 use tempfile::NamedTempFile;
133
134 #[test]
135 fn test_file_content_as_str_string() {
136 let content = FileContent::String("test content".to_string());
137 assert_eq!(content.as_str().unwrap(), "test content");
138 assert!(!content.is_mmap());
139 assert_eq!(content.len(), 12);
140 assert!(!content.is_empty());
141 }
142
143 #[test]
144 fn test_file_content_is_empty() {
145 let content = FileContent::String(String::new());
146 assert!(content.is_empty());
147 }
148
149 #[test]
150 fn test_reader_small_file_uses_string() {
151 let mut file = NamedTempFile::new().unwrap();
152 write!(file, "small: content").unwrap();
153
154 let reader = SmartFileReader::new();
155 let content = reader.read(file.path()).unwrap();
156
157 assert!(!content.is_mmap());
158 assert_eq!(content.as_str().unwrap(), "small: content");
159 }
160
161 #[test]
162 fn test_reader_large_file_uses_mmap() {
163 let mut file = NamedTempFile::new().unwrap();
164
165 let large_content = "x".repeat(2 * 1024 * 1024);
167 write!(file, "{large_content}").unwrap();
168
169 let reader = SmartFileReader::new();
170 let content = reader.read(file.path()).unwrap();
171
172 assert!(content.is_mmap());
173 assert_eq!(content.len(), large_content.len());
174 }
175
176 #[test]
177 fn test_reader_custom_threshold() {
178 let mut file = NamedTempFile::new().unwrap();
179 write!(file, "test content").unwrap();
180
181 let reader = SmartFileReader::with_threshold(5);
183 let content = reader.read(file.path()).unwrap();
184
185 assert!(content.is_mmap());
187 }
188
189 #[test]
190 fn test_reader_default_equals_new() {
191 let reader1 = SmartFileReader::new();
192 let reader2 = SmartFileReader::default();
193
194 assert_eq!(reader1.mmap_threshold, reader2.mmap_threshold);
195 }
196
197 #[test]
198 fn test_read_nonexistent_file() {
199 let reader = SmartFileReader::new();
200 let result = reader.read(Path::new("/nonexistent/file.yaml"));
201 assert!(result.is_err());
202 }
203
204 #[test]
205 fn test_file_content_len() {
206 let content = FileContent::String("hello".to_string());
207 assert_eq!(content.len(), 5);
208 }
209
210 #[test]
211 fn test_read_utf8_validation_with_mmap() {
212 let mut file = NamedTempFile::new().unwrap();
213
214 let content = "valid: utf8 content\n".repeat(100_000);
216 write!(file, "{content}").unwrap();
217
218 let reader = SmartFileReader::new();
219 let file_content = reader.read(file.path()).unwrap();
220
221 assert!(file_content.is_mmap());
223 assert!(file_content.as_str().is_ok());
224 }
225
226 #[test]
227 #[cfg(unix)]
228 fn test_symlink_handling() {
229 use std::os::unix::fs::symlink;
230
231 let temp_dir = tempfile::tempdir().unwrap();
232 let target = temp_dir.path().join("target.yaml");
233 let link = temp_dir.path().join("link.yaml");
234
235 std::fs::write(&target, "key: value\n").unwrap();
237
238 symlink(&target, &link).unwrap();
240
241 let reader = SmartFileReader::new();
243 let content = reader.read(&link).unwrap();
244
245 assert_eq!(content.as_str().unwrap(), "key: value\n");
246 }
247
248 #[test]
249 #[cfg(unix)]
250 fn test_broken_symlink_error() {
251 use std::os::unix::fs::symlink;
252
253 let temp_dir = tempfile::tempdir().unwrap();
254 let nonexistent = temp_dir.path().join("nonexistent.yaml");
255 let link = temp_dir.path().join("broken_link.yaml");
256
257 symlink(&nonexistent, &link).unwrap();
259
260 let reader = SmartFileReader::new();
262 let result = reader.read(&link);
263
264 assert!(result.is_err());
265 }
266
267 #[test]
268 #[cfg(unix)]
269 fn test_symlink_loop_detection() {
270 use std::os::unix::fs::symlink;
271
272 let temp_dir = tempfile::tempdir().unwrap();
273 let link1 = temp_dir.path().join("link1.yaml");
274 let link2 = temp_dir.path().join("link2.yaml");
275
276 symlink(&link2, &link1).unwrap();
278 symlink(&link1, &link2).unwrap();
279
280 let reader = SmartFileReader::new();
282 let result = reader.read(&link1);
283
284 assert!(result.is_err());
285 }
286
287 #[test]
288 #[cfg(unix)]
289 fn test_symlink_to_directory_error() {
290 use std::os::unix::fs::symlink;
291
292 let temp_dir = tempfile::tempdir().unwrap();
293 let dir = temp_dir.path().join("subdir");
294 let link = temp_dir.path().join("dir_link.yaml");
295
296 std::fs::create_dir(&dir).unwrap();
298 symlink(&dir, &link).unwrap();
299
300 let reader = SmartFileReader::new();
302 let result = reader.read(&link);
303
304 assert!(result.is_err());
305 }
306}