1use std::fs::File;
2use std::path::Path;
3use std::sync::Arc;
4
5use memmap2::MmapOptions;
6
7use super::{FileInfo, FileRange};
8use crate::Error;
9use crate::mincore::PageMap;
10
11pub trait FileProcessed {
12 type Output;
13 fn into_output(self) -> Self::Output;
14 fn output_ref(&self) -> &Self::Output;
15 fn total_pages(&self) -> usize;
16 fn pages_in_core_before(&self) -> Option<usize> {
17 None
18 }
19 fn pages_in_core_after(&self) -> usize;
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct FullResult<O, PM> {
24 pub output: O,
25 pub total_pages: usize,
26 pub pages_in_core_before: usize,
27 pub pages_in_core_after: usize,
28 pub residency_before: Option<PM>,
29 pub residency_after: Option<PM>,
30}
31
32impl<O, PM> FileProcessed for FullResult<O, PM> {
33 type Output = O;
34 fn into_output(self) -> O {
35 self.output
36 }
37 fn output_ref(&self) -> &O {
38 &self.output
39 }
40 fn total_pages(&self) -> usize {
41 self.total_pages
42 }
43 fn pages_in_core_before(&self) -> Option<usize> {
44 Some(self.pages_in_core_before)
45 }
46 fn pages_in_core_after(&self) -> usize {
47 self.pages_in_core_after
48 }
49}
50
51#[derive(Debug, Clone, PartialEq)]
52pub struct CountsResult<O> {
53 pub output: O,
54 pub total_pages: usize,
55 pub pages_in_core_after: usize,
56}
57
58impl<O> FileProcessed for CountsResult<O> {
59 type Output = O;
60 fn into_output(self) -> O {
61 self.output
62 }
63 fn output_ref(&self) -> &O {
64 &self.output
65 }
66 fn total_pages(&self) -> usize {
67 self.total_pages
68 }
69 fn pages_in_core_after(&self) -> usize {
70 self.pages_in_core_after
71 }
72}
73
74#[derive(Debug, Clone, PartialEq)]
75pub struct SkipResult<O> {
76 pub output: O,
77 pub total_pages: usize,
78}
79
80impl<O> FileProcessed for SkipResult<O> {
81 type Output = O;
82 fn into_output(self) -> O {
83 self.output
84 }
85 fn output_ref(&self) -> &O {
86 &self.output
87 }
88 fn total_pages(&self) -> usize {
89 self.total_pages
90 }
91 fn pages_in_core_after(&self) -> usize {
92 0
93 }
94}
95
96pub(crate) struct PreparedFile {
97 pub file: File,
98 pub offset: u64,
99 pub len: usize,
100 pub total_pages: usize,
101 pub mmap: Arc<memmap2::Mmap>,
102}
103
104pub(crate) fn prepare_file(path: &Path, range: &FileRange) -> crate::Result<Option<PreparedFile>> {
105 let io_err = |e| Error::io(path.display().to_string(), e);
106
107 let file = File::open(path).map_err(io_err)?;
108 let file_len = file.metadata().map_err(io_err)?.len();
109
110 if file_len == 0 {
111 return Ok(None);
112 }
113
114 let (offset, len) =
115 effective_range(file_len, range).ok_or_else(|| Error::OffsetBeyondFile {
116 path: path.to_path_buf(),
117 offset: range.offset,
118 file_len,
119 })?;
120
121 let total_pages = len.div_ceil(*crate::pagesize::PAGE_SIZE);
122
123 let mmap = Arc::new(unsafe {
124 MmapOptions::new()
125 .offset(offset)
126 .len(len)
127 .map(&file)
128 .map_err(io_err)?
129 });
130
131 Ok(Some(PreparedFile {
132 file,
133 offset,
134 len,
135 total_pages,
136 mmap,
137 }))
138}
139
140fn effective_range(file_len: u64, range: &FileRange) -> Option<(u64, usize)> {
141 if file_len == 0 {
142 return None;
143 }
144 let offset = range.offset;
145 if offset >= file_len {
146 return None;
147 }
148 let len = match range.max_len {
149 Some(max) if (offset + max) < file_len => max as usize,
150 _ => (file_len - offset) as usize,
151 };
152 Some((offset, len))
153}
154
155pub fn file_info<PM: PageMap>(
156 path: &Path,
157 range: &FileRange,
158) -> crate::Result<Option<FileInfo<PM>>> {
159 let io_err = |e| Error::io(path.display().to_string(), e);
160
161 let file = File::open(path).map_err(io_err)?;
162 let file_len = file.metadata().map_err(io_err)?.len();
163
164 let Some((offset, len)) = effective_range(file_len, range) else {
165 return Ok(None);
166 };
167
168 let total_pages = len.div_ceil(*crate::pagesize::PAGE_SIZE);
169 let mmap = unsafe {
170 MmapOptions::new()
171 .offset(offset)
172 .len(len)
173 .map(&file)
174 .map_err(io_err)?
175 };
176 let residency: PM = crate::mincore::residency(&mmap, len)?;
177 Ok(Some(FileInfo {
178 total_pages,
179 residency,
180 }))
181}
182
183#[cfg(test)]
184mod tests {
185 use crate::mincore::PageMapSlice as _;
186 use crate::mode;
187
188 use super::*;
189 use std::io::Write;
190 use std::sync::atomic::Ordering;
191
192 use super::super::*;
193
194 fn create_temp_file(pages: usize) -> (tempfile::NamedTempFile, usize) {
195 let page_size = *crate::pagesize::PAGE_SIZE;
196 let size = page_size * pages;
197 let mut f = tempfile::NamedTempFile::new().unwrap();
198 f.write_all(&vec![0xABu8; size]).unwrap();
199 f.flush().unwrap();
200 (f, size)
201 }
202
203 macro_rules! process_tests {
204 ($t:ty, $mod:ident) => {
205 mod $mod {
206 use super::*;
207
208 #[test]
209 fn query_counts_pages() {
210 let (f, _) = create_temp_file(4);
211 let range = FileRange {
212 offset: 0,
213 max_len: None,
214 };
215 let result = mode::counts_process_file::<Query, $t>(&Query, f.path(), &range)
216 .unwrap()
217 .unwrap();
218 assert_eq!(result.total_pages, 4);
219 }
220
221 #[test]
222 fn empty_file_returns_none() {
223 let f = tempfile::NamedTempFile::new().unwrap();
224 let range = FileRange {
225 offset: 0,
226 max_len: None,
227 };
228 let result: Option<CountsResult<()>> =
229 mode::counts_process_file::<Query, $t>(&Query, f.path(), &range).unwrap();
230 assert!(result.is_none());
231 }
232
233 #[test]
234 fn offset_beyond_file() {
235 let (f, _) = create_temp_file(1);
236 let range = FileRange {
237 offset: 1_000_000,
238 max_len: None,
239 };
240 let result: crate::Result<Option<CountsResult<()>>> =
241 mode::counts_process_file::<Query, $t>(&Query, f.path(), &range);
242 assert!(result.is_err());
243 let err = result.unwrap_err();
244 assert!(
245 matches!(err, Error::OffsetBeyondFile { .. }),
246 "expected OffsetBeyondFile, got: {err}"
247 );
248 }
249
250 #[test]
251 fn with_max_len() {
252 let (f, _) = create_temp_file(8);
253 let page_size = *crate::pagesize::PAGE_SIZE;
254 let range = FileRange {
255 offset: 0,
256 max_len: Some((page_size * 2) as u64),
257 };
258 let result = mode::counts_process_file::<Query, $t>(&Query, f.path(), &range)
259 .unwrap()
260 .unwrap();
261 assert_eq!(result.total_pages, 2);
262 }
263
264 #[test]
265 fn touch_makes_resident() {
266 let (f, size) = create_temp_file(4);
267 let range = FileRange {
268 offset: 0,
269 max_len: None,
270 };
271
272 mode::counts_process_file::<Evict, $t>(&Evict, f.path(), &range).unwrap();
273 mode::counts_process_file::<Touch, $t>(&Touch, f.path(), &range).unwrap();
274
275 let file = std::fs::File::open(f.path()).unwrap();
276 let mmap_check =
277 unsafe { memmap2::MmapOptions::new().len(size).map(&file).unwrap() };
278 let residency: $t = crate::mincore::residency(&mmap_check, size).unwrap();
279 assert!(
280 (0..residency.len()).all(|i| residency[i..i + 1].count_filled() == 1),
281 "expected all pages resident after touch"
282 );
283 }
284
285 #[test]
286 fn evict_succeeds() {
287 let (f, _) = create_temp_file(4);
288 let range = FileRange {
289 offset: 0,
290 max_len: None,
291 };
292 let result: crate::Result<Option<CountsResult<()>>> =
293 mode::counts_process_file::<Evict, $t>(&Evict, f.path(), &range);
294 assert!(result.is_ok());
295 }
296
297 #[test]
298 fn full_residency() {
299 let (f, _) = create_temp_file(4);
300 let range = FileRange {
301 offset: 0,
302 max_len: None,
303 };
304 let result: FullResult<(), $t> =
305 mode::full_process_file::<Query, $t>(&Query, f.path(), &range, None)
306 .unwrap()
307 .unwrap();
308 assert!(result.residency_after.is_some());
309 assert_eq!(result.residency_after.unwrap().len(), 4);
310 }
311
312 #[test]
313 fn query_full_reuses_before() {
314 let (f, _) = create_temp_file(4);
315 let range = FileRange {
316 offset: 0,
317 max_len: None,
318 };
319 let result: FullResult<(), $t> =
320 mode::full_process_file::<Query, $t>(&Query, f.path(), &range, None)
321 .unwrap()
322 .unwrap();
323 assert_eq!(result.pages_in_core_before, result.pages_in_core_after);
324 assert!(result.residency_before.is_none());
325 assert!(result.residency_after.is_some());
326 }
327
328 #[test]
329 fn counts_without_bitmap() {
330 let (f, _) = create_temp_file(4);
331 let range = FileRange {
332 offset: 0,
333 max_len: None,
334 };
335 let result = mode::counts_process_file::<Query, $t>(&Query, f.path(), &range)
336 .unwrap()
337 .unwrap();
338 assert_eq!(result.total_pages, 4);
339 }
340
341 #[test]
342 fn nonexistent_returns_error() {
343 let range = FileRange {
344 offset: 0,
345 max_len: None,
346 };
347 let result: crate::Result<Option<CountsResult<()>>> =
348 mode::counts_process_file::<Query, $t>(
349 &Query,
350 std::path::Path::new("/nonexistent/file.dat"),
351 &range,
352 );
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn file_info_pages() {
358 let (f, _) = create_temp_file(4);
359 let range = FileRange {
360 offset: 0,
361 max_len: None,
362 };
363 let info: FileInfo<$t> = file_info(f.path(), &range).unwrap().unwrap();
364 assert_eq!(info.total_pages, 4);
365 assert_eq!(info.residency.len(), 4);
366 }
367 }
368 };
369 }
370
371 process_tests!(Vec<bool>, vec_bool_impl);
372
373 #[cfg(feature = "bitvec")]
374 process_tests!(::bitvec::vec::BitVec, bitvec_impl);
375
376 #[test]
377 fn test_stats_default() {
378 let stats = Stats::default();
379 assert_eq!(stats.total_pages.load(Ordering::Relaxed), 0);
380 assert_eq!(stats.initial_pages_in_core.load(Ordering::Relaxed), 0);
381 assert_eq!(stats.action_pages.load(Ordering::Relaxed), 0);
382 assert_eq!(stats.total_files.load(Ordering::Relaxed), 0);
383 assert_eq!(stats.total_dirs.load(Ordering::Relaxed), 0);
384 }
385}