1#[cfg(unix)]
4use crate::io_buffers::IoBuffer;
5use crate::io_buffers::{IoVector, IoVectorMut};
6#[cfg(unix)]
7use crate::misc_helpers::while_eintr;
8use crate::misc_helpers::ResultErrorContext;
9use crate::storage::drivers::CommonStorageHelper;
10use crate::storage::ext::write_full_zeroes;
11use crate::storage::PreallocateMode;
12use crate::{Storage, StorageCreateOptions, StorageOpenOptions};
13use cfg_if::cfg_if;
14use std::fmt::{self, Display, Formatter};
15use std::io::{self, Write};
16#[cfg(any(target_os = "linux", target_os = "macos"))]
17use std::os::fd::AsRawFd;
18#[cfg(unix)]
19use std::os::unix::fs::FileTypeExt;
20#[cfg(all(unix, not(target_os = "macos")))]
21use std::os::unix::fs::OpenOptionsExt;
22#[cfg(windows)]
23use std::os::windows::fs::{FileExt, OpenOptionsExt};
24#[cfg(windows)]
25use std::os::windows::io::AsRawHandle;
26use std::path::{Path, PathBuf};
27use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
28use std::sync::RwLock;
29use std::{cmp, fs};
30#[cfg(unix)]
31use tracing::{debug, warn};
32#[cfg(windows)]
33use windows_sys::Win32::System::Ioctl::{FILE_ZERO_DATA_INFORMATION, FSCTL_SET_ZERO_DATA};
34#[cfg(windows)]
35use windows_sys::Win32::System::IO::DeviceIoControl;
36
37#[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
42const RWF_DONTCACHE_FLAG: libc::c_int = 0x0000_0080;
43
44#[derive(Debug)]
46pub struct File {
47 file: RwLock<fs::File>,
49
50 filename: Option<PathBuf>,
52
53 req_align: usize,
55
56 mem_align: usize,
58
59 zero_align: usize,
61
62 discard_align: usize,
64
65 size: AtomicU64,
69
70 common_storage_helper: CommonStorageHelper,
72
73 #[cfg(target_os = "macos")]
75 relaxed_sync: bool,
76
77 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
82 write_dontcache: AtomicBool,
83
84 discard_unsupported: AtomicBool,
86}
87
88impl TryFrom<fs::File> for File {
89 type Error = io::Error;
90
91 fn try_from(file: fs::File) -> io::Result<Self> {
98 Self::new(
99 file,
100 None,
101 false,
102 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
103 false,
104 #[cfg(target_os = "macos")]
105 false,
106 )
107 }
108}
109
110impl Storage for File {
111 async fn open(opts: StorageOpenOptions) -> io::Result<Self> {
112 Self::do_open_sync(opts, fs::OpenOptions::new())
113 }
114
115 #[cfg(feature = "sync-wrappers")]
116 fn open_sync(opts: StorageOpenOptions) -> io::Result<Self> {
117 Self::do_open_sync(opts, fs::OpenOptions::new())
118 }
119
120 async fn create_open(opts: StorageCreateOptions) -> io::Result<Self> {
121 let opts = opts.modify_open_opts(|o| o.write(true));
123 let size = opts.size;
124 let prealloc_mode = opts.prealloc_mode;
125
126 let mut file_opts = fs::OpenOptions::new();
127 if opts.overwrite {
128 file_opts.create(true).truncate(true);
129 } else {
130 file_opts.create_new(true);
131 };
132
133 let file = Self::do_open_sync(opts.get_open_options(), file_opts)?;
134 if size > 0 {
135 file.resize(size, prealloc_mode)
136 .await
137 .err_context(|| "Resizing file")?;
138 }
139
140 Ok(file)
141 }
142
143 fn mem_align(&self) -> usize {
144 self.mem_align
145 }
146
147 fn req_align(&self) -> usize {
148 self.req_align
149 }
150
151 fn zero_align(&self) -> usize {
152 self.zero_align
153 }
154
155 fn discard_align(&self) -> usize {
156 self.discard_align
157 }
158
159 fn size(&self) -> io::Result<u64> {
160 Ok(self.size.load(Ordering::Relaxed))
161 }
162
163 fn resolve_relative_path<P: AsRef<Path>>(&self, relative: P) -> io::Result<PathBuf> {
164 let relative = relative.as_ref();
165
166 if relative.is_absolute() {
167 return Ok(relative.to_path_buf());
168 }
169
170 let filename = self
171 .filename
172 .as_ref()
173 .ok_or_else(|| io::Error::other("No filename set for base image"))?;
174
175 let dirname = filename
176 .parent()
177 .ok_or_else(|| io::Error::other("Invalid base image filename set"))?;
178
179 Ok(dirname.join(relative))
180 }
181
182 fn get_filename(&self) -> Option<PathBuf> {
183 self.filename.as_ref().cloned()
184 }
185
186 #[cfg(unix)]
187 async unsafe fn pure_readv(
188 &self,
189 mut bufv: IoVectorMut<'_>,
190 mut offset: u64,
191 ) -> io::Result<()> {
192 while !bufv.is_empty() {
193 let iovec = unsafe { bufv.as_iovec() };
194 let preadv_offset = offset
195 .try_into()
196 .map_err(|_| io::Error::other("Read offset overflow"))?;
197
198 let len = while_eintr(|| unsafe {
199 libc::preadv(
200 self.file.read().unwrap().as_raw_fd(),
201 iovec.as_ptr(),
202 iovec.len() as libc::c_int,
203 preadv_offset,
204 )
205 })? as u64;
206
207 if len == 0 {
208 bufv.fill(0);
210 break;
211 }
212
213 bufv = bufv.split_tail_at(len);
214 offset = offset
215 .checked_add(len)
216 .ok_or_else(|| io::Error::other("Read offset overflow"))?;
217 }
218
219 Ok(())
220 }
221
222 #[cfg(windows)]
223 async unsafe fn pure_readv(&self, bufv: IoVectorMut<'_>, mut offset: u64) -> io::Result<()> {
224 for mut buffer in bufv.into_inner() {
225 let mut buffer: &mut [u8] = &mut buffer;
226 while !buffer.is_empty() {
227 let len = if offset >= self.size.load(Ordering::Relaxed) {
228 buffer.fill(0);
229 buffer.len()
230 } else {
231 self.file.write().unwrap().seek_read(buffer, offset)?
232 };
233 offset = offset
234 .checked_add(len as u64)
235 .ok_or_else(|| io::Error::other("Read offset overflow"))?;
236 buffer = buffer.split_at_mut(len).1;
237 }
238 }
239 Ok(())
240 }
241
242 #[cfg(unix)]
243 async unsafe fn pure_writev(&self, mut bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
244 while !bufv.is_empty() {
245 let iovec = unsafe { bufv.as_iovec() };
246 let pwritev_offset = offset
247 .try_into()
248 .map_err(|_| io::Error::other("Write offset overflow"))?;
249
250 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
251 let len = write_with_optional_dontcache(
252 &self.write_dontcache,
253 || {
254 syscall_result(unsafe {
257 libc::pwritev2(
258 self.file.read().unwrap().as_raw_fd(),
259 iovec.as_ptr(),
260 iovec.len() as libc::c_int,
261 pwritev_offset,
262 RWF_DONTCACHE_FLAG,
263 )
264 })
265 },
266 || {
267 syscall_result(unsafe {
270 libc::pwritev(
271 self.file.read().unwrap().as_raw_fd(),
272 iovec.as_ptr(),
273 iovec.len() as libc::c_int,
274 pwritev_offset,
275 )
276 })
277 },
278 )?;
279
280 #[cfg(all(
281 unix,
282 not(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))
283 ))]
284 let len = while_eintr(|| unsafe {
285 libc::pwritev(
286 self.file.read().unwrap().as_raw_fd(),
287 iovec.as_ptr(),
288 iovec.len() as libc::c_int,
289 pwritev_offset,
290 )
291 })?;
292
293 let len = require_write_progress(len)?;
294
295 bufv = bufv.split_tail_at(len);
296 offset = offset
297 .checked_add(len)
298 .ok_or_else(|| io::Error::other("Write offset overflow"))?;
299 self.size.fetch_max(offset, Ordering::Relaxed);
300 }
301
302 Ok(())
303 }
304
305 #[cfg(windows)]
306 async unsafe fn pure_writev(&self, bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
307 for buffer in bufv.into_inner() {
308 let mut buffer: &[u8] = &buffer;
309 while !buffer.is_empty() {
310 let len = self.file.write().unwrap().seek_write(buffer, offset)?;
311 offset = offset
312 .checked_add(len as u64)
313 .ok_or_else(|| io::Error::other("Write offset overflow"))?;
314 self.size.fetch_max(offset, Ordering::Relaxed);
315 buffer = buffer.split_at(len).1;
316 }
317 }
318 Ok(())
319 }
320
321 async unsafe fn pure_write_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
322 self.discard_to_zero(offset, length).await
323 }
324
325 #[cfg(target_os = "linux")]
326 async unsafe fn pure_write_allocated_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
327 let offset: libc::off_t = offset
328 .try_into()
329 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
330 let length: libc::off_t = length
331 .try_into()
332 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
333
334 let file = self.file.read().unwrap();
335 while_eintr(|| unsafe {
337 libc::fallocate(file.as_raw_fd(), libc::FALLOC_FL_ZERO_RANGE, offset, length)
338 })
339 .map_err(Self::map_os_err)?;
340
341 Ok(())
342 }
343
344 async unsafe fn pure_discard(&self, offset: u64, length: u64) -> io::Result<()> {
345 if let Err(err) = self.discard_to_zero(offset, length).await {
346 if err.kind() == io::ErrorKind::Unsupported {
350 Ok(())
351 } else {
352 Err(err)
353 }
354 } else {
355 Ok(())
356 }
357 }
358
359 async fn flush(&self) -> io::Result<()> {
360 self.file.write().unwrap().flush()
361 }
362
363 async fn sync(&self) -> io::Result<()> {
364 #[cfg(target_os = "macos")]
365 if self.relaxed_sync {
366 while_eintr(|| unsafe { libc::fsync(self.file.write().unwrap().as_raw_fd()) })?;
368 return Ok(());
369 }
370 self.file.write().unwrap().sync_all()
371 }
372
373 async unsafe fn invalidate_cache(&self) -> io::Result<()> {
374 Ok(())
380 }
381
382 fn get_storage_helper(&self) -> &CommonStorageHelper {
383 &self.common_storage_helper
384 }
385
386 async fn resize(&self, new_size: u64, prealloc_mode: PreallocateMode) -> io::Result<()> {
387 let file = self.file.write().unwrap();
388 let current_size = self.size.load(Ordering::Relaxed);
389
390 match new_size.cmp(¤t_size) {
391 std::cmp::Ordering::Equal => return Ok(()),
392 std::cmp::Ordering::Less => {
393 file.set_len(new_size)?;
394 self.size.fetch_min(new_size, Ordering::Relaxed);
395 return Ok(());
396 }
397 std::cmp::Ordering::Greater => (), }
399
400 match prealloc_mode {
401 PreallocateMode::None | PreallocateMode::Zero => file.set_len(new_size)?,
402 PreallocateMode::Allocate => {
403 #[cfg(not(unix))]
404 return Err(io::ErrorKind::Unsupported.into());
405
406 #[cfg(all(unix, not(target_os = "macos")))]
407 {
408 let ofs = current_size.try_into().map_err(io::Error::other)?;
409 let len = (new_size - current_size)
410 .try_into()
411 .map_err(io::Error::other)?;
412 while_eintr(|| unsafe { libc::fallocate(file.as_raw_fd(), 0, ofs, len) })
413 .map_err(Self::map_os_err)?;
414 }
415
416 #[cfg(target_os = "macos")]
417 {
418 let mut params = libc::fstore_t {
423 fst_flags: libc::F_ALLOCATEALL,
424 fst_posmode: libc::F_PEOFPOSMODE,
425 fst_offset: 0,
426 fst_length: (new_size - current_size)
427 .try_into()
428 .map_err(io::Error::other)?,
429 fst_bytesalloc: 0, };
431 while_eintr(|| unsafe {
432 libc::fcntl(file.as_raw_fd(), libc::F_PREALLOCATE, &mut params)
433 })
434 .map_err(Self::map_os_err)?;
435
436 file.set_len(new_size)?;
437 }
438 }
439 PreallocateMode::WriteData => {
440 drop(file);
443 write_full_zeroes(self, current_size, new_size - current_size).await?;
444 }
445 }
446
447 self.size.fetch_max(new_size, Ordering::Relaxed);
448 Ok(())
449 }
450}
451
452impl File {
453 fn new(
458 mut file: fs::File,
459 filename: Option<PathBuf>,
460 direct_io: bool,
461 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
462 write_dontcache: bool,
463 #[cfg(target_os = "macos")] relaxed_sync: bool,
464 ) -> io::Result<Self> {
465 let size = get_file_size(&file).err_context(|| "Failed to determine file size")?;
466
467 #[cfg(all(unix, not(target_os = "macos")))]
468 let direct_io = direct_io || {
469 let res = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
471 res > 0 && (res & libc::O_DIRECT) != 0
472 };
473
474 let (min_req_align, min_mem_align) = if direct_io {
475 #[cfg(unix)]
476 {
477 (
478 Self::get_min_dio_req_align(&file),
479 Self::get_min_dio_mem_align(&file),
480 )
481 }
482
483 #[cfg(not(unix))]
484 {
485 (1, 1)
486 } } else {
488 (1, 1)
489 };
490
491 let (req_align, mem_align, zero_align, discard_align) =
492 Self::probe_alignments(&mut file, min_req_align, min_mem_align);
493 assert!(req_align.is_power_of_two());
494 assert!(mem_align.is_power_of_two());
495
496 Ok(File {
497 file: RwLock::new(file),
498 filename,
499 req_align,
500 mem_align,
501 zero_align,
502 discard_align,
503 size: size.into(),
504 common_storage_helper: Default::default(),
505 #[cfg(target_os = "macos")]
506 relaxed_sync,
507 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
508 write_dontcache: AtomicBool::new(write_dontcache),
509 discard_unsupported: AtomicBool::new(false),
510 })
511 }
512
513 #[cfg(unix)]
517 fn probe_alignments(
518 file: &mut fs::File,
519 min_req_align: usize,
520 min_mem_align: usize,
521 ) -> (usize, usize, usize, usize) {
522 let mut page_size = page_size::get();
523 if !page_size.is_power_of_two() {
524 let assume = page_size.checked_next_power_of_two().unwrap_or(4096);
525 let assume = cmp::max(4096, assume);
526 warn!("Reported page size of {page_size} is not a power of two, assuming {assume}");
527 page_size = assume;
528 }
529
530 #[cfg(not(target_os = "macos"))]
531 let (zero_align, discard_align) = (1, 1);
532 #[cfg(target_os = "macos")]
533 let (zero_align, discard_align) = {
534 let mut statfs: libc::statfs = unsafe { std::mem::zeroed() };
535 match while_eintr(|| unsafe { libc::fstatfs(file.as_raw_fd(), &mut statfs) }) {
537 Ok(_) => (statfs.f_bsize as usize, statfs.f_bsize as usize),
538 Err(_) => (page_size, page_size),
539 }
540 };
541
542 let mut writable = true;
543
544 let max_req_align = 65536;
545 let max_mem_align = cmp::max(page_size, max_req_align);
546
547 let safe_req_align = 4096;
549 let safe_mem_align = cmp::max(page_size, safe_req_align);
550
551 let mut test_buf = match IoBuffer::new(max_mem_align, max_mem_align) {
552 Ok(buf) => buf,
553 Err(err) => {
554 warn!(
555 "Failed to allocate memory to probe request alignment ({err}), \
556 falling back to {safe_req_align}/{safe_mem_align}"
557 );
558 return (safe_req_align, safe_mem_align, zero_align, discard_align);
559 }
560 };
561
562 let mut req_align: usize = min_req_align;
563 let result = loop {
564 assert!(req_align <= max_mem_align);
565 match Self::probe_access(
566 file,
567 test_buf.as_mut_range(0..req_align).into_slice(),
568 req_align.try_into().unwrap(),
569 &mut writable,
570 ) {
571 Ok(true) => break Ok(req_align),
572 Ok(false) => {
573 if req_align >= max_req_align {
574 break Err(io::Error::other(format!(
575 "Maximum I/O alignment ({max_req_align}) exceeded"
576 )));
577 }
578 if req_align == min_req_align {
580 req_align = cmp::max(min_req_align << 1, 512);
581 } else {
582 req_align <<= 1;
583 }
584 }
585 Err(err) => break Err(err),
586 }
587 };
588
589 let req_align = match result {
590 Ok(align) => {
591 debug!("Probed request alignment: {align}");
592 align
593 }
594 Err(err) => {
595 let align = cmp::max(req_align, safe_req_align);
597 warn!(
598 "Failed to probe request alignment ({err}; {}), falling back to {align} bytes",
599 err.kind(),
600 );
601 align
602 }
603 };
604
605 let mut mem_align: usize = min_mem_align;
606 let result = loop {
607 assert!(mem_align <= max_mem_align);
608 let range = (max_mem_align - mem_align)..max_mem_align;
609 match Self::probe_access(
610 file,
611 test_buf.as_mut_range(range).into_slice(),
612 0,
613 &mut writable,
614 ) {
615 Ok(true) => break Ok(mem_align),
616 Ok(false) => {
617 if mem_align >= max_mem_align {
619 break Err(io::Error::other(format!(
620 "Maximum memory alignment ({max_mem_align}) exceeded"
621 )));
622 }
623 if mem_align == min_mem_align {
625 mem_align = cmp::max(min_mem_align << 1, cmp::min(page_size, 4096));
626 } else {
627 mem_align <<= 1;
628 }
629 }
630 Err(err) => break Err(err),
631 }
632 };
633
634 let mem_align = match result {
635 Ok(align) => {
636 debug!("Probed memory alignment: {align}");
637 align
638 }
639 Err(err) => {
640 let align = cmp::max(mem_align, safe_mem_align);
642 warn!(
643 "Failed to probe memory alignment ({err}; {}), falling back to {align} bytes",
644 err.kind(),
645 );
646 align
647 }
648 };
649
650 (req_align, mem_align, zero_align, discard_align)
651 }
652
653 #[cfg(unix)]
662 fn probe_access(
663 file: &mut fs::File,
664 slice: &mut [u8],
665 offset: libc::off_t,
666 may_write: &mut bool,
667 ) -> io::Result<bool> {
668 let ret = while_eintr(|| unsafe {
671 libc::pread(
672 file.as_raw_fd(),
673 slice.as_mut_ptr() as *mut libc::c_void,
674 slice.len(),
675 offset,
676 )
677 });
678
679 if let Err(err) = ret {
680 if err.raw_os_error() == Some(libc::EINVAL) {
681 return Ok(false);
682 } else {
683 return Err(err);
684 }
685 }
686
687 if !*may_write {
688 return Ok(true);
689 }
690
691 let ret = while_eintr(|| unsafe {
693 libc::pwrite(
694 file.as_raw_fd(),
695 slice.as_ptr() as *const libc::c_void,
696 slice.len(),
697 offset,
698 )
699 });
700
701 if let Err(err) = ret {
702 if err.raw_os_error() == Some(libc::EINVAL) {
703 Ok(false)
704 } else if err.raw_os_error() == Some(libc::EBADF) {
705 *may_write = false;
706 Ok(true)
707 } else {
708 Err(err)
709 }
710 } else {
711 Ok(true)
712 }
713 }
714
715 #[cfg(unix)]
717 fn get_min_dio_req_align(file: &fs::File) -> usize {
718 #[cfg(target_os = "linux")]
719 {
720 let mut alignment = 0;
721 let res = unsafe { ioctl::blksszget(file.as_raw_fd(), &mut alignment) };
722 if res.is_ok() && alignment > 0 {
723 let alignment = alignment as usize;
724 if alignment.is_power_of_two() {
725 return alignment;
726 }
727 }
728 }
729
730 #[cfg(target_os = "macos")]
731 {
732 let mut alignment = 0;
733 let res = unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut alignment) };
734 if res.is_ok() && alignment.is_power_of_two() {
735 return alignment as usize;
736 }
737 }
738
739 #[cfg(target_os = "freebsd")]
740 {
741 let mut alignment = 0;
742 let res = unsafe { ioctl::diocgsectorsize(file.as_raw_fd(), &mut alignment) };
743 if res.is_ok() && alignment.is_power_of_two() {
744 return alignment as usize;
745 }
746 }
747
748 1
750 }
751
752 #[cfg(unix)]
754 fn get_min_dio_mem_align(_file: &fs::File) -> usize {
755 1
757 }
758
759 #[cfg(windows)]
763 fn probe_alignments(
764 _file: &mut fs::File,
765 min_req_align: usize,
766 min_mem_align: usize,
767 ) -> (usize, usize, usize, usize) {
768 (
770 cmp::max(min_req_align, 4096),
771 cmp::max(min_mem_align, 4096),
772 1,
773 1,
774 )
775 }
776
777 fn do_open_sync(opts: StorageOpenOptions, base_fs_opts: fs::OpenOptions) -> io::Result<Self> {
779 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
780 if opts.write_dontcache && !opts.writable {
781 return Err(io::Error::new(
782 io::ErrorKind::InvalidInput,
783 "RWF_DONTCACHE requires writable storage",
784 ));
785 }
786
787 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
788 if opts.write_dontcache && opts.direct {
789 return Err(io::Error::new(
790 io::ErrorKind::InvalidInput,
791 "RWF_DONTCACHE is incompatible with direct I/O",
792 ));
793 }
794
795 let Some(filename) = opts.filename else {
796 return Err(io::Error::new(
797 io::ErrorKind::InvalidInput,
798 "Filename required",
799 ));
800 };
801
802 let mut file_opts = base_fs_opts;
803 file_opts.read(true).write(opts.writable);
804 #[cfg(not(target_os = "macos"))]
805 if opts.direct {
806 file_opts.custom_flags(
807 #[cfg(unix)]
808 libc::O_DIRECT,
809 #[cfg(windows)]
810 windows_sys::Win32::Storage::FileSystem::FILE_FLAG_NO_BUFFERING,
811 );
812 }
813
814 let filename_owned = filename.to_owned();
815 let file = file_opts.open(filename)?;
816
817 #[cfg(target_os = "macos")]
818 if opts.direct {
819 while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) })
821 .err_context(|| "Failed to disable host cache")?;
822 }
823
824 Self::new(
825 file,
826 Some(filename_owned),
827 opts.direct,
828 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
829 opts.write_dontcache,
830 #[cfg(target_os = "macos")]
831 opts.relaxed_sync,
832 )
833 }
834
835 #[cfg(unix)]
851 fn map_os_err(err: io::Error) -> io::Error {
852 let Some(raw) = err.raw_os_error() else {
853 return err;
854 };
855
856 let has_kind = err.kind();
857 let want_kind = match raw {
858 #[allow(unreachable_patterns)] libc::ENOTSUP | libc::EOPNOTSUPP | libc::ENODEV | libc::ENXIO | libc::ENOTTY => {
860 io::ErrorKind::Unsupported
861 }
862 _ => has_kind,
863 };
864
865 if has_kind != want_kind {
866 io::Error::new(want_kind, err)
867 } else {
868 err
869 }
870 }
871
872 #[cfg(not(unix))]
876 fn map_os_err(err: io::Error) -> io::Error {
877 err
878 }
879
880 fn try_discard_by_truncate(&self, offset: u64, length: u64) -> io::Result<bool> {
888 #[allow(clippy::readonly_write_lock)]
890 let file = self.file.write().unwrap();
891
892 let size = self.size.load(Ordering::Relaxed);
893 if offset >= size {
894 return Ok(true);
896 }
897
898 let end = offset.checked_add(length).unwrap_or(size);
901 if end < size {
902 return Ok(false);
903 }
904
905 file.set_len(offset)?;
906 Ok(true)
907 }
908
909 async fn discard_to_zero(&self, offset: u64, length: u64) -> io::Result<()> {
911 if self.try_discard_by_truncate(offset, length)? {
912 return Ok(());
913 }
914
915 if self.discard_unsupported.load(Ordering::Relaxed) {
916 Err(io::ErrorKind::Unsupported.into())
917 } else if let Err(err) = self.discard_to_zero_os_specific(offset, length).await {
918 if err.kind() == io::ErrorKind::Unsupported {
919 self.discard_unsupported.store(true, Ordering::Relaxed);
920 }
921 Err(err)
922 } else {
923 Ok(())
924 }
925 }
926
927 #[cfg(target_os = "linux")]
929 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
930 let offset: libc::off_t = offset
931 .try_into()
932 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
933 let length: libc::off_t = length
934 .try_into()
935 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
936
937 let file = self.file.read().unwrap();
938 while_eintr(|| unsafe {
940 libc::fallocate(
941 file.as_raw_fd(),
942 libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
943 offset,
944 length,
945 )
946 })
947 .map_err(Self::map_os_err)?;
948
949 Ok(())
950 }
951
952 #[cfg(windows)]
954 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
955 let offset: i64 = offset
956 .try_into()
957 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
958 let length: i64 = length
959 .try_into()
960 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
961
962 let end = offset.saturating_add(length).saturating_add(1);
963 let params = FILE_ZERO_DATA_INFORMATION {
964 FileOffset: offset,
965 BeyondFinalZero: end,
966 };
967 let mut _returned = 0;
968 let file = self.file.read().unwrap();
969 let ret = unsafe {
973 DeviceIoControl(
974 file.as_raw_handle(),
975 FSCTL_SET_ZERO_DATA,
976 (¶ms as *const FILE_ZERO_DATA_INFORMATION).cast::<std::ffi::c_void>(),
977 size_of_val(¶ms) as u32,
978 std::ptr::null_mut(),
979 0,
980 &mut _returned,
981 std::ptr::null_mut(),
982 )
983 };
984 if ret == 0 {
985 return Err(Self::map_os_err(io::Error::last_os_error()));
986 }
987
988 Ok(())
989 }
990
991 #[cfg(target_os = "macos")]
993 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
994 let offset: libc::off_t = offset
995 .try_into()
996 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
997 let length: libc::off_t = length
998 .try_into()
999 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
1000
1001 let params = libc::fpunchhole_t {
1002 fp_flags: 0,
1003 reserved: 0,
1004 fp_offset: offset,
1005 fp_length: length,
1006 };
1007 let file = self.file.read().unwrap();
1008 while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_PUNCHHOLE, ¶ms) })
1010 .map_err(Self::map_os_err)?;
1011
1012 Ok(())
1013 }
1014
1015 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1017 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1018 Err(io::ErrorKind::Unsupported.into())
1019 }
1020}
1021
1022impl Display for File {
1023 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1024 if let Some(filename) = self.filename.as_ref() {
1025 write!(f, "file:{filename:?}")
1026 } else {
1027 write!(f, "file:<unknown path>")
1028 }
1029 }
1030}
1031
1032#[cfg(unix)]
1034fn require_write_progress(length: libc::ssize_t) -> io::Result<u64> {
1035 if length == 0 {
1036 Err(io::ErrorKind::WriteZero.into())
1037 } else {
1038 debug_assert!(length > 0);
1039 Ok(length as u64)
1040 }
1041}
1042
1043#[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1045fn syscall_result(result: libc::ssize_t) -> io::Result<libc::ssize_t> {
1046 if result == -1 {
1047 Err(io::Error::last_os_error())
1048 } else {
1049 Ok(result)
1050 }
1051}
1052
1053#[cfg(any(
1055 all(target_os = "linux", any(target_env = "gnu", target_env = "musl")),
1056 all(test, unix)
1057))]
1058fn retry_interrupted<F>(mut operation: F) -> io::Result<libc::ssize_t>
1059where
1060 F: FnMut() -> io::Result<libc::ssize_t>,
1061{
1062 loop {
1063 match operation() {
1064 Err(error) if error.raw_os_error() == Some(libc::EINTR) => continue,
1065 result => return result,
1066 }
1067 }
1068}
1069
1070#[cfg(any(
1076 all(target_os = "linux", any(target_env = "gnu", target_env = "musl")),
1077 all(test, unix)
1078))]
1079fn write_with_optional_dontcache<H, P>(
1080 write_dontcache: &AtomicBool,
1081 hinted_write: H,
1082 plain_write: P,
1083) -> io::Result<libc::ssize_t>
1084where
1085 H: FnMut() -> io::Result<libc::ssize_t>,
1086 P: FnMut() -> io::Result<libc::ssize_t>,
1087{
1088 if !write_dontcache.load(Ordering::Relaxed) {
1089 return retry_interrupted(plain_write);
1090 }
1091
1092 match retry_interrupted(hinted_write) {
1093 Err(error)
1094 if matches!(
1095 error.raw_os_error(),
1096 Some(libc::EOPNOTSUPP) | Some(libc::ENOSYS)
1097 ) =>
1098 {
1099 write_dontcache.store(false, Ordering::Relaxed);
1102 retry_interrupted(plain_write)
1103 }
1104 result => result,
1105 }
1106}
1107
1108fn get_file_size(file: &fs::File) -> io::Result<u64> {
1113 #[allow(clippy::bind_instead_of_map)]
1114 file.metadata().and_then(|m| {
1115 #[cfg(unix)]
1116 if m.file_type().is_block_device() || m.file_type().is_char_device() {
1117 return get_device_size(file);
1118 }
1119 Ok(m.len())
1120 })
1121}
1122
1123cfg_if! {
1124 if #[cfg(target_os = "linux")] {
1125 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1127 let mut size = 0;
1128 unsafe { ioctl::blkgetsize64(file.as_raw_fd(), &mut size) }?;
1129 Ok(size)
1130 }
1131 } else if #[cfg(target_os = "macos")] {
1132 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1134 let mut block_size = 0;
1135 unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut block_size) }?;
1136 let mut block_count = 0;
1137 unsafe { ioctl::dkiocgetblockcount(file.as_raw_fd(), &mut block_count) }?;
1138 Ok(u64::from(block_size) * block_count)
1139 }
1140 } else if #[cfg(target_os = "freebsd")] {
1141 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1143 let mut size = 0;
1144 unsafe { ioctl::diocgmediasize(file.as_raw_fd(), &mut size) }?;
1145 Ok(size as u64)
1146 }
1147 } else if #[cfg(unix)] {
1148 fn get_device_size(_file: &fs::File) -> io::Result<u64> {
1150 Err(io::ErrorKind::Unsupported.into())
1151 }
1152 }
1153}
1154
1155mod ioctl {
1157 #[cfg(unix)]
1158 use nix::ioctl_read;
1159 #[cfg(target_os = "linux")]
1160 use nix::ioctl_read_bad;
1161
1162 #[cfg(target_os = "linux")]
1165 ioctl_read!(blkgetsize64, 0x12, 114, u64);
1166
1167 #[cfg(target_os = "linux")]
1168 ioctl_read_bad!(blksszget, libc::BLKSSZGET, libc::c_int);
1169
1170 #[cfg(target_os = "macos")]
1173 ioctl_read!(dkiocgetblocksize, 'd', 24, u32);
1174
1175 #[cfg(target_os = "macos")]
1176 ioctl_read!(dkiocgetblockcount, 'd', 25, u64);
1177
1178 #[cfg(target_os = "freebsd")]
1181 ioctl_read!(diocgsectorsize, 'd', 128, libc::c_uint);
1182
1183 #[cfg(target_os = "freebsd")]
1184 ioctl_read!(diocgmediasize, 'd', 129, libc::off_t);
1185}
1186
1187#[cfg(all(test, unix))]
1188mod tests {
1189 use super::*;
1190
1191 use std::cell::Cell;
1192
1193 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1194 #[test]
1195 fn write_dontcache_options_default_to_disabled() {
1196 let options = StorageOpenOptions::new();
1197 assert!(!options.get_write_dontcache());
1198
1199 let options = options.write_dontcache(true);
1200 assert!(options.get_write_dontcache());
1201 }
1202
1203 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1204 #[test]
1205 fn write_dontcache_rejects_read_only_files() {
1206 let options = StorageOpenOptions::new().write_dontcache(true);
1207 let error = File::do_open_sync(options, fs::OpenOptions::new()).unwrap_err();
1208
1209 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1210 assert!(error.to_string().contains("writable storage"));
1211 }
1212
1213 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1214 #[test]
1215 fn write_dontcache_rejects_direct_io() {
1216 let options = StorageOpenOptions::new()
1217 .write(true)
1218 .direct(true)
1219 .write_dontcache(true);
1220 let error = File::do_open_sync(options, fs::OpenOptions::new()).unwrap_err();
1221
1222 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1223 assert!(error.to_string().contains("direct I/O"));
1224 }
1225
1226 #[test]
1227 fn unsupported_hint_retries_the_same_tail_and_disables_future_hints() {
1228 for unsupported_error in [libc::EOPNOTSUPP, libc::ENOSYS] {
1229 let enabled = AtomicBool::new(true);
1230 let hinted_calls = Cell::new(0);
1231 let plain_calls = Cell::new(0);
1232
1233 let length = write_with_optional_dontcache(
1234 &enabled,
1235 || {
1236 hinted_calls.set(hinted_calls.get() + 1);
1237 if hinted_calls.get() == 1 {
1238 Err(io::Error::from_raw_os_error(libc::EINTR))
1239 } else {
1240 Err(io::Error::from_raw_os_error(unsupported_error))
1241 }
1242 },
1243 || {
1244 plain_calls.set(plain_calls.get() + 1);
1245 Ok(37)
1246 },
1247 )
1248 .unwrap();
1249
1250 assert_eq!(length, 37);
1251 assert_eq!(hinted_calls.get(), 2);
1252 assert_eq!(plain_calls.get(), 1);
1253 assert!(!enabled.load(Ordering::Relaxed));
1254
1255 let length = write_with_optional_dontcache(
1256 &enabled,
1257 || panic!("a disabled hint must not be retried"),
1258 || {
1259 plain_calls.set(plain_calls.get() + 1);
1260 Ok(11)
1261 },
1262 )
1263 .unwrap();
1264
1265 assert_eq!(length, 11);
1266 assert_eq!(plain_calls.get(), 2);
1267 }
1268 }
1269
1270 #[test]
1271 fn invalid_hint_error_does_not_fall_back() {
1272 let enabled = AtomicBool::new(true);
1273 let plain_calls = Cell::new(0);
1274
1275 let error = write_with_optional_dontcache(
1276 &enabled,
1277 || Err(io::Error::from_raw_os_error(libc::EINVAL)),
1278 || {
1279 plain_calls.set(plain_calls.get() + 1);
1280 Ok(1)
1281 },
1282 )
1283 .unwrap_err();
1284
1285 assert_eq!(error.raw_os_error(), Some(libc::EINVAL));
1286 assert_eq!(plain_calls.get(), 0);
1287 assert!(enabled.load(Ordering::Relaxed));
1288 }
1289
1290 #[test]
1291 fn successful_partial_hint_write_advances_without_fallback() {
1292 let enabled = AtomicBool::new(true);
1293 let plain_calls = Cell::new(0);
1294
1295 let length = write_with_optional_dontcache(
1296 &enabled,
1297 || Ok(13),
1298 || {
1299 plain_calls.set(plain_calls.get() + 1);
1300 Ok(99)
1301 },
1302 )
1303 .unwrap();
1304
1305 assert_eq!(length, 13);
1306 assert_eq!(plain_calls.get(), 0);
1307 assert!(enabled.load(Ordering::Relaxed));
1308 }
1309
1310 #[test]
1311 fn zero_length_write_is_write_zero() {
1312 let error = require_write_progress(0).unwrap_err();
1313 assert_eq!(error.kind(), io::ErrorKind::WriteZero);
1314 }
1315}