1#![cfg_attr(
12 any(unix, windows),
13 expect(
14 clippy::items_after_statements,
15 reason = "https://github.com/rust-lang/rust-clippy/issues/17498"
16 )
17)]
18
19#[cfg(all(test, not(all(miri, windows))))]
21mod tests;
22
23use parking_lot::Mutex;
24use std::fs::{File, OpenOptions};
25use std::mem::MaybeUninit;
26use std::path::Path;
27use std::{io, mem, slice};
28
29pub const DISK_PAGE_SIZE: usize = 4096;
31const MAX_READ_SIZE: usize = 1024 * 1024;
33
34const _: () = {
35 assert!(MAX_READ_SIZE.is_multiple_of(AlignedPage::SIZE));
36};
37
38#[derive(Debug, Copy, Clone)]
41#[repr(C, align(4096))]
42pub struct AlignedPage([u8; AlignedPage::SIZE]);
43
44const _: () = {
45 assert!(align_of::<AlignedPage>() == AlignedPage::SIZE);
46};
47
48impl Default for AlignedPage {
49 #[inline(always)]
50 fn default() -> Self {
51 Self([0; AlignedPage::SIZE])
52 }
53}
54
55impl AlignedPage {
56 pub const SIZE: usize = 4096;
59
60 pub fn as_uninit_slice_mut(value: &mut [Self]) -> &mut [MaybeUninit<Self>] {
62 unsafe { mem::transmute(value) }
64 }
65
66 #[inline(always)]
68 pub fn slice_to_repr(value: &[Self]) -> &[[u8; AlignedPage::SIZE]] {
69 unsafe { mem::transmute(value) }
71 }
72
73 #[inline(always)]
75 pub fn uninit_slice_to_repr(
76 value: &[MaybeUninit<Self>],
77 ) -> &[MaybeUninit<[u8; AlignedPage::SIZE]>] {
78 unsafe { mem::transmute(value) }
80 }
81
82 #[inline]
86 pub fn try_slice_from_repr(value: &[[u8; AlignedPage::SIZE]]) -> Option<&[Self]> {
87 let (before, slice, after) = unsafe { value.align_to::<Self>() };
89
90 if before.is_empty() && after.is_empty() {
91 Some(slice)
92 } else {
93 None
94 }
95 }
96
97 #[inline]
101 pub fn try_uninit_slice_from_repr(
102 value: &[MaybeUninit<[u8; AlignedPage::SIZE]>],
103 ) -> Option<&[MaybeUninit<Self>]> {
104 let (before, slice, after) = unsafe { value.align_to::<MaybeUninit<Self>>() };
106
107 if before.is_empty() && after.is_empty() {
108 Some(slice)
109 } else {
110 None
111 }
112 }
113
114 #[inline(always)]
117 pub fn slice_mut_to_repr(slice: &mut [Self]) -> &mut [[u8; AlignedPage::SIZE]] {
118 unsafe { mem::transmute(slice) }
120 }
121
122 #[inline(always)]
125 pub fn uninit_slice_mut_to_repr(
126 slice: &mut [MaybeUninit<Self>],
127 ) -> &mut [MaybeUninit<[u8; AlignedPage::SIZE]>] {
128 unsafe { mem::transmute(slice) }
130 }
131
132 #[inline]
136 pub fn try_slice_mut_from_repr(value: &mut [[u8; AlignedPage::SIZE]]) -> Option<&mut [Self]> {
137 let (before, slice, after) = unsafe { value.align_to_mut::<Self>() };
139
140 if before.is_empty() && after.is_empty() {
141 Some(slice)
142 } else {
143 None
144 }
145 }
146
147 #[inline]
151 pub fn try_uninit_slice_mut_from_repr(
152 value: &mut [MaybeUninit<[u8; AlignedPage::SIZE]>],
153 ) -> Option<&mut [MaybeUninit<Self>]> {
154 let (before, slice, after) = unsafe { value.align_to_mut::<MaybeUninit<Self>>() };
156
157 if before.is_empty() && after.is_empty() {
158 Some(slice)
159 } else {
160 None
161 }
162 }
163}
164
165#[derive(Debug)]
174pub struct DirectIoFile {
175 file: File,
176 scratch_buffer: Mutex<Vec<AlignedPage>>,
178}
179
180impl DirectIoFile {
181 #[inline]
189 pub fn open<P>(
190 #[cfg(any(target_os = "linux", windows))] mut options: OpenOptions,
191 #[cfg(not(any(target_os = "linux", windows)))] options: OpenOptions,
192 path: P,
193 ) -> io::Result<Self>
194 where
195 P: AsRef<Path>,
196 {
197 #[cfg(target_os = "linux")]
199 if !cfg!(miri) {
201 use std::os::unix::fs::OpenOptionsExt;
202
203 options.custom_flags(libc::O_DIRECT);
204 }
205 #[cfg(windows)]
207 if !cfg!(miri) {
209 use std::os::windows::fs::OpenOptionsExt;
210
211 options.custom_flags(
212 windows::Win32::Storage::FileSystem::FILE_FLAG_WRITE_THROUGH.0
213 | windows::Win32::Storage::FileSystem::FILE_FLAG_NO_BUFFERING.0,
214 );
215 }
216 let file = options.open(path)?;
217
218 #[cfg(target_os = "macos")]
220 if !cfg!(miri) {
222 use std::os::unix::io::AsRawFd;
223
224 if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) } != 0 {
226 return Err(io::Error::last_os_error());
227 }
228 }
229
230 Ok(Self {
231 file,
232 scratch_buffer: Mutex::new(vec![
234 AlignedPage::default();
235 MAX_READ_SIZE / AlignedPage::SIZE
236 ]),
237 })
238 }
239
240 #[inline]
242 pub fn len(&self) -> io::Result<u64> {
243 Ok(self.file.metadata()?.len())
244 }
245
246 #[inline]
248 pub fn is_empty(&self) -> io::Result<bool> {
249 Ok(self.len()? == 0)
250 }
251
252 #[inline(always)]
256 pub fn allocate(&self, len: u64) -> io::Result<()> {
257 fs4::FileExt::allocate(&self.file, len)
258 }
259
260 #[inline(always)]
267 pub fn set_len(&self, len: u64) -> io::Result<()> {
268 self.file.set_len(len)
269 }
270
271 pub fn read_exact_at(&self, buf: &mut [u8], mut offset: u64) -> io::Result<()> {
276 if buf.is_empty() {
277 return Ok(());
278 }
279
280 let mut scratch_buffer = self.scratch_buffer.lock();
281
282 debug_assert!(
284 AlignedPage::slice_to_repr(&scratch_buffer)
285 .as_flattened()
286 .len()
287 <= MAX_READ_SIZE
288 );
289
290 let padding = (offset % AlignedPage::SIZE as u64) as usize;
292 let first_unaligned_chunk_size = (MAX_READ_SIZE - padding).min(buf.len());
293 let (unaligned_start, buf) = buf.split_at_mut(first_unaligned_chunk_size);
294 {
295 let bytes_to_read = unaligned_start.len();
296 unaligned_start.copy_from_slice(self.read_exact_at_internal(
297 &mut scratch_buffer,
298 bytes_to_read,
299 offset,
300 )?);
301 offset += unaligned_start.len() as u64;
302 }
303
304 if buf.is_empty() {
305 return Ok(());
306 }
307
308 for buf in buf.chunks_mut(MAX_READ_SIZE) {
310 let bytes_to_read = buf.len();
311 buf.copy_from_slice(self.read_exact_at_internal(
312 &mut scratch_buffer,
313 bytes_to_read,
314 offset,
315 )?);
316 offset += buf.len() as u64;
317 }
318
319 Ok(())
320 }
321
322 pub fn write_all_at(&self, buf: &[u8], mut offset: u64) -> io::Result<()> {
327 if buf.is_empty() {
328 return Ok(());
329 }
330
331 let mut scratch_buffer = self.scratch_buffer.lock();
332
333 debug_assert!(
335 AlignedPage::slice_to_repr(&scratch_buffer)
336 .as_flattened()
337 .len()
338 <= MAX_READ_SIZE
339 );
340
341 let padding = (offset % AlignedPage::SIZE as u64) as usize;
343 let first_unaligned_chunk_size = (MAX_READ_SIZE - padding).min(buf.len());
344 let (unaligned_start, buf) = buf.split_at(first_unaligned_chunk_size);
345 {
346 self.write_all_at_internal(&mut scratch_buffer, unaligned_start, offset)?;
347 offset += unaligned_start.len() as u64;
348 }
349
350 if buf.is_empty() {
351 return Ok(());
352 }
353
354 for buf in buf.chunks(MAX_READ_SIZE) {
356 self.write_all_at_internal(&mut scratch_buffer, buf, offset)?;
357 offset += buf.len() as u64;
358 }
359
360 Ok(())
361 }
362
363 #[inline]
370 pub fn read_exact_at_raw(
371 &self,
372 buf: &mut [MaybeUninit<AlignedPage>],
373 offset: u64,
374 ) -> io::Result<()> {
375 let buf = AlignedPage::uninit_slice_mut_to_repr(buf);
376
377 let buf = unsafe {
381 slice::from_raw_parts_mut(
382 buf.as_mut_ptr().cast::<[u8; AlignedPage::SIZE]>(),
383 buf.len(),
384 )
385 };
386
387 let buf = buf.as_flattened_mut();
388
389 cfg_select! {
390 unix => {
391 use std::os::unix::fs::FileExt;
392
393 self.file.read_exact_at(buf, offset)
394 }
395 windows => {
396 use std::os::windows::fs::FileExt;
397
398 let mut buf = buf;
399 let mut offset = offset;
400 while !buf.is_empty() {
401 match self.file.seek_read(buf, offset) {
402 Ok(0) => {
403 break;
404 }
405 Ok(n) => {
406 buf = &mut buf[n..];
407 offset += n as u64;
408 }
409 Err(e) if e.kind() == io::ErrorKind::Interrupted => {
410 }
412 Err(e) => {
413 return Err(e);
414 }
415 }
416 }
417
418 if buf.is_empty() {
419 Ok(())
420 } else {
421 Err(io::Error::new(
422 io::ErrorKind::UnexpectedEof,
423 "failed to fill the whole buffer",
424 ))
425 }
426 }
427 _ => {
428 compile_error!("Unsupported platform (consider contributing)");
429 }
430 }
431 }
432
433 #[inline]
438 pub fn write_all_at_raw(&self, buf: &[AlignedPage], offset: u64) -> io::Result<()> {
439 let buf = AlignedPage::slice_to_repr(buf).as_flattened();
440
441 cfg_select! {
442 unix => {
443 use std::os::unix::fs::FileExt;
444
445 self.file.write_all_at(buf, offset)
446 }
447 windows => {
448 use std::os::windows::fs::FileExt;
449
450 let mut buf = buf;
451 let mut offset = offset;
452 while !buf.is_empty() {
453 match self.file.seek_write(buf, offset) {
454 Ok(0) => {
455 return Err(io::Error::new(
456 io::ErrorKind::WriteZero,
457 "failed to write the whole buffer",
458 ));
459 }
460 Ok(n) => {
461 buf = &buf[n..];
462 offset += n as u64;
463 }
464 Err(e) if e.kind() == io::ErrorKind::Interrupted => {
465 }
467 Err(e) => {
468 return Err(e);
469 }
470 }
471 }
472
473 Ok(())
474 }
475 _ => {
476 compile_error!("Unsupported platform (consider contributing)");
477 }
478 }
479 }
480
481 #[inline(always)]
483 pub fn file(&self) -> &File {
484 &self.file
485 }
486
487 fn read_exact_at_internal<'a>(
488 &self,
489 scratch_buffer: &'a mut [AlignedPage],
490 bytes_to_read: usize,
491 offset: u64,
492 ) -> io::Result<&'a [u8]> {
493 let page_aligned_offset = offset / AlignedPage::SIZE as u64 * AlignedPage::SIZE as u64;
494 let padding = (offset - page_aligned_offset) as usize;
495
496 let pages_to_read = (padding + bytes_to_read).div_ceil(AlignedPage::SIZE);
499 let scratch_buffer = &mut scratch_buffer[..pages_to_read];
500
501 self.read_exact_at_raw(
502 AlignedPage::as_uninit_slice_mut(scratch_buffer),
503 page_aligned_offset,
504 )?;
505
506 Ok(&AlignedPage::slice_to_repr(scratch_buffer).as_flattened()[padding..][..bytes_to_read])
507 }
508
509 fn write_all_at_internal(
511 &self,
512 scratch_buffer: &mut [AlignedPage],
513 bytes_to_write: &[u8],
514 offset: u64,
515 ) -> io::Result<()> {
516 let page_aligned_offset = offset / AlignedPage::SIZE as u64 * AlignedPage::SIZE as u64;
517 let padding = (offset - page_aligned_offset) as usize;
518
519 let pages_to_read = (padding + bytes_to_write.len()).div_ceil(AlignedPage::SIZE);
521
522 if padding == 0 && pages_to_read == bytes_to_write.len() {
523 let scratch_buffer = &mut scratch_buffer[..pages_to_read];
524 AlignedPage::slice_mut_to_repr(scratch_buffer)
525 .as_flattened_mut()
526 .copy_from_slice(bytes_to_write);
527 self.write_all_at_raw(scratch_buffer, offset)?;
528 } else {
529 let scratch_buffer = &mut scratch_buffer[..pages_to_read];
530 self.read_exact_at_raw(
532 AlignedPage::as_uninit_slice_mut(scratch_buffer),
533 page_aligned_offset,
534 )?;
535 AlignedPage::slice_mut_to_repr(scratch_buffer).as_flattened_mut()[padding..]
537 [..bytes_to_write.len()]
538 .copy_from_slice(bytes_to_write);
539 self.write_all_at_raw(scratch_buffer, page_aligned_offset)?;
540 }
541
542 Ok(())
543 }
544}