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 file.set_len(size)?;
908 Ok(true)
909 }
910
911 async fn discard_to_zero(&self, offset: u64, length: u64) -> io::Result<()> {
913 if self.try_discard_by_truncate(offset, length)? {
914 return Ok(());
915 }
916
917 if self.discard_unsupported.load(Ordering::Relaxed) {
918 Err(io::ErrorKind::Unsupported.into())
919 } else if let Err(err) = self.discard_to_zero_os_specific(offset, length).await {
920 if err.kind() == io::ErrorKind::Unsupported {
921 self.discard_unsupported.store(true, Ordering::Relaxed);
922 }
923 Err(err)
924 } else {
925 Ok(())
926 }
927 }
928
929 #[cfg(target_os = "linux")]
931 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
932 let offset: libc::off_t = offset
933 .try_into()
934 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
935 let length: libc::off_t = length
936 .try_into()
937 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
938
939 let file = self.file.read().unwrap();
940 while_eintr(|| unsafe {
942 libc::fallocate(
943 file.as_raw_fd(),
944 libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
945 offset,
946 length,
947 )
948 })
949 .map_err(Self::map_os_err)?;
950
951 Ok(())
952 }
953
954 #[cfg(windows)]
956 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
957 let offset: i64 = offset
958 .try_into()
959 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
960 let length: i64 = length
961 .try_into()
962 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
963
964 let end = offset.saturating_add(length).saturating_add(1);
965 let params = FILE_ZERO_DATA_INFORMATION {
966 FileOffset: offset,
967 BeyondFinalZero: end,
968 };
969 let mut _returned = 0;
970 let file = self.file.read().unwrap();
971 let ret = unsafe {
975 DeviceIoControl(
976 file.as_raw_handle(),
977 FSCTL_SET_ZERO_DATA,
978 (¶ms as *const FILE_ZERO_DATA_INFORMATION).cast::<std::ffi::c_void>(),
979 size_of_val(¶ms) as u32,
980 std::ptr::null_mut(),
981 0,
982 &mut _returned,
983 std::ptr::null_mut(),
984 )
985 };
986 if ret == 0 {
987 return Err(Self::map_os_err(io::Error::last_os_error()));
988 }
989
990 Ok(())
991 }
992
993 #[cfg(target_os = "macos")]
995 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
996 let offset: libc::off_t = offset
997 .try_into()
998 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
999 let length: libc::off_t = length
1000 .try_into()
1001 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
1002
1003 let params = libc::fpunchhole_t {
1004 fp_flags: 0,
1005 reserved: 0,
1006 fp_offset: offset,
1007 fp_length: length,
1008 };
1009 let file = self.file.read().unwrap();
1010 while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_PUNCHHOLE, ¶ms) })
1012 .map_err(Self::map_os_err)?;
1013
1014 Ok(())
1015 }
1016
1017 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1019 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1020 Err(io::ErrorKind::Unsupported.into())
1021 }
1022}
1023
1024impl Display for File {
1025 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1026 if let Some(filename) = self.filename.as_ref() {
1027 write!(f, "file:{filename:?}")
1028 } else {
1029 write!(f, "file:<unknown path>")
1030 }
1031 }
1032}
1033
1034#[cfg(unix)]
1036fn require_write_progress(length: libc::ssize_t) -> io::Result<u64> {
1037 if length == 0 {
1038 Err(io::ErrorKind::WriteZero.into())
1039 } else {
1040 debug_assert!(length > 0);
1041 Ok(length as u64)
1042 }
1043}
1044
1045#[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1047fn syscall_result(result: libc::ssize_t) -> io::Result<libc::ssize_t> {
1048 if result == -1 {
1049 Err(io::Error::last_os_error())
1050 } else {
1051 Ok(result)
1052 }
1053}
1054
1055#[cfg(any(
1057 all(target_os = "linux", any(target_env = "gnu", target_env = "musl")),
1058 all(test, unix)
1059))]
1060fn retry_interrupted<F>(mut operation: F) -> io::Result<libc::ssize_t>
1061where
1062 F: FnMut() -> io::Result<libc::ssize_t>,
1063{
1064 loop {
1065 match operation() {
1066 Err(error) if error.raw_os_error() == Some(libc::EINTR) => continue,
1067 result => return result,
1068 }
1069 }
1070}
1071
1072#[cfg(any(
1078 all(target_os = "linux", any(target_env = "gnu", target_env = "musl")),
1079 all(test, unix)
1080))]
1081fn write_with_optional_dontcache<H, P>(
1082 write_dontcache: &AtomicBool,
1083 hinted_write: H,
1084 plain_write: P,
1085) -> io::Result<libc::ssize_t>
1086where
1087 H: FnMut() -> io::Result<libc::ssize_t>,
1088 P: FnMut() -> io::Result<libc::ssize_t>,
1089{
1090 if !write_dontcache.load(Ordering::Relaxed) {
1091 return retry_interrupted(plain_write);
1092 }
1093
1094 match retry_interrupted(hinted_write) {
1095 Err(error)
1096 if matches!(
1097 error.raw_os_error(),
1098 Some(libc::EOPNOTSUPP) | Some(libc::ENOSYS)
1099 ) =>
1100 {
1101 write_dontcache.store(false, Ordering::Relaxed);
1104 retry_interrupted(plain_write)
1105 }
1106 result => result,
1107 }
1108}
1109
1110fn get_file_size(file: &fs::File) -> io::Result<u64> {
1115 #[allow(clippy::bind_instead_of_map)]
1116 file.metadata().and_then(|m| {
1117 #[cfg(unix)]
1118 if m.file_type().is_block_device() || m.file_type().is_char_device() {
1119 return get_device_size(file);
1120 }
1121 Ok(m.len())
1122 })
1123}
1124
1125cfg_if! {
1126 if #[cfg(target_os = "linux")] {
1127 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1129 let mut size = 0;
1130 unsafe { ioctl::blkgetsize64(file.as_raw_fd(), &mut size) }?;
1131 Ok(size)
1132 }
1133 } else if #[cfg(target_os = "macos")] {
1134 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1136 let mut block_size = 0;
1137 unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut block_size) }?;
1138 let mut block_count = 0;
1139 unsafe { ioctl::dkiocgetblockcount(file.as_raw_fd(), &mut block_count) }?;
1140 Ok(u64::from(block_size) * block_count)
1141 }
1142 } else if #[cfg(target_os = "freebsd")] {
1143 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1145 let mut size = 0;
1146 unsafe { ioctl::diocgmediasize(file.as_raw_fd(), &mut size) }?;
1147 Ok(size as u64)
1148 }
1149 } else if #[cfg(unix)] {
1150 fn get_device_size(_file: &fs::File) -> io::Result<u64> {
1152 Err(io::ErrorKind::Unsupported.into())
1153 }
1154 }
1155}
1156
1157mod ioctl {
1159 #[cfg(unix)]
1160 use nix::ioctl_read;
1161 #[cfg(target_os = "linux")]
1162 use nix::ioctl_read_bad;
1163
1164 #[cfg(target_os = "linux")]
1167 ioctl_read!(blkgetsize64, 0x12, 114, u64);
1168
1169 #[cfg(target_os = "linux")]
1170 ioctl_read_bad!(blksszget, libc::BLKSSZGET, libc::c_int);
1171
1172 #[cfg(target_os = "macos")]
1175 ioctl_read!(dkiocgetblocksize, 'd', 24, u32);
1176
1177 #[cfg(target_os = "macos")]
1178 ioctl_read!(dkiocgetblockcount, 'd', 25, u64);
1179
1180 #[cfg(target_os = "freebsd")]
1183 ioctl_read!(diocgsectorsize, 'd', 128, libc::c_uint);
1184
1185 #[cfg(target_os = "freebsd")]
1186 ioctl_read!(diocgmediasize, 'd', 129, libc::off_t);
1187}
1188
1189#[cfg(all(test, unix))]
1190mod tests {
1191 use super::*;
1192
1193 use std::cell::Cell;
1194
1195 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1196 #[test]
1197 fn write_dontcache_options_default_to_disabled() {
1198 let options = StorageOpenOptions::new();
1199 assert!(!options.get_write_dontcache());
1200
1201 let options = options.write_dontcache(true);
1202 assert!(options.get_write_dontcache());
1203 }
1204
1205 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1206 #[test]
1207 fn write_dontcache_rejects_read_only_files() {
1208 let options = StorageOpenOptions::new().write_dontcache(true);
1209 let error = File::do_open_sync(options, fs::OpenOptions::new()).unwrap_err();
1210
1211 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1212 assert!(error.to_string().contains("writable storage"));
1213 }
1214
1215 #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "musl")))]
1216 #[test]
1217 fn write_dontcache_rejects_direct_io() {
1218 let options = StorageOpenOptions::new()
1219 .write(true)
1220 .direct(true)
1221 .write_dontcache(true);
1222 let error = File::do_open_sync(options, fs::OpenOptions::new()).unwrap_err();
1223
1224 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1225 assert!(error.to_string().contains("direct I/O"));
1226 }
1227
1228 #[test]
1229 fn unsupported_hint_retries_the_same_tail_and_disables_future_hints() {
1230 for unsupported_error in [libc::EOPNOTSUPP, libc::ENOSYS] {
1231 let enabled = AtomicBool::new(true);
1232 let hinted_calls = Cell::new(0);
1233 let plain_calls = Cell::new(0);
1234
1235 let length = write_with_optional_dontcache(
1236 &enabled,
1237 || {
1238 hinted_calls.set(hinted_calls.get() + 1);
1239 if hinted_calls.get() == 1 {
1240 Err(io::Error::from_raw_os_error(libc::EINTR))
1241 } else {
1242 Err(io::Error::from_raw_os_error(unsupported_error))
1243 }
1244 },
1245 || {
1246 plain_calls.set(plain_calls.get() + 1);
1247 Ok(37)
1248 },
1249 )
1250 .unwrap();
1251
1252 assert_eq!(length, 37);
1253 assert_eq!(hinted_calls.get(), 2);
1254 assert_eq!(plain_calls.get(), 1);
1255 assert!(!enabled.load(Ordering::Relaxed));
1256
1257 let length = write_with_optional_dontcache(
1258 &enabled,
1259 || panic!("a disabled hint must not be retried"),
1260 || {
1261 plain_calls.set(plain_calls.get() + 1);
1262 Ok(11)
1263 },
1264 )
1265 .unwrap();
1266
1267 assert_eq!(length, 11);
1268 assert_eq!(plain_calls.get(), 2);
1269 }
1270 }
1271
1272 #[test]
1273 fn invalid_hint_error_does_not_fall_back() {
1274 let enabled = AtomicBool::new(true);
1275 let plain_calls = Cell::new(0);
1276
1277 let error = write_with_optional_dontcache(
1278 &enabled,
1279 || Err(io::Error::from_raw_os_error(libc::EINVAL)),
1280 || {
1281 plain_calls.set(plain_calls.get() + 1);
1282 Ok(1)
1283 },
1284 )
1285 .unwrap_err();
1286
1287 assert_eq!(error.raw_os_error(), Some(libc::EINVAL));
1288 assert_eq!(plain_calls.get(), 0);
1289 assert!(enabled.load(Ordering::Relaxed));
1290 }
1291
1292 #[test]
1293 fn successful_partial_hint_write_advances_without_fallback() {
1294 let enabled = AtomicBool::new(true);
1295 let plain_calls = Cell::new(0);
1296
1297 let length = write_with_optional_dontcache(
1298 &enabled,
1299 || Ok(13),
1300 || {
1301 plain_calls.set(plain_calls.get() + 1);
1302 Ok(99)
1303 },
1304 )
1305 .unwrap();
1306
1307 assert_eq!(length, 13);
1308 assert_eq!(plain_calls.get(), 0);
1309 assert!(enabled.load(Ordering::Relaxed));
1310 }
1311
1312 #[test]
1313 fn zero_length_write_is_write_zero() {
1314 let error = require_write_progress(0).unwrap_err();
1315 assert_eq!(error.kind(), io::ErrorKind::WriteZero);
1316 }
1317}
1318
1319#[cfg(test)]
1320mod tail_discard_tests {
1321 use super::File;
1322 use crate::{Storage, StorageExt, StorageOpenOptions};
1323 use std::{fs, io};
1324
1325 struct TempPath(std::path::PathBuf);
1327
1328 impl Drop for TempPath {
1329 fn drop(&mut self) {
1330 let _ = fs::remove_file(&self.0);
1331 }
1332 }
1333
1334 #[test]
1335 fn tail_discard_keeps_file_length() -> io::Result<()> {
1336 let runtime = tokio::runtime::Builder::new_current_thread().build()?;
1337 runtime.block_on(async {
1338 let unique = std::time::SystemTime::now()
1339 .duration_since(std::time::UNIX_EPOCH)
1340 .unwrap()
1341 .as_nanos();
1342 let path = std::env::temp_dir().join(format!(
1343 "msb-imago-tail-discard-{}-{unique}.raw",
1344 std::process::id()
1345 ));
1346 let _temp_path = TempPath(path.clone());
1347 fs::write(&path, vec![0xabu8; 8192])?;
1348
1349 let file = File::open(StorageOpenOptions::new().write(true).filename(&path)).await?;
1350 file.discard(4096, 4096).await?;
1351 assert_eq!(fs::metadata(&path)?.len(), 8192);
1352 assert_eq!(file.size()?, 8192);
1353
1354 let mut prefix = vec![0u8; 4096];
1355 file.read(&mut prefix, 0).await?;
1356 assert_eq!(prefix, vec![0xabu8; 4096]);
1357
1358 let mut tail = vec![0xffu8; 4096];
1359 file.read(&mut tail, 4096).await?;
1360 assert!(tail.iter().all(|&byte| byte == 0));
1361
1362 drop(file);
1364 let reopened = File::open(StorageOpenOptions::new().filename(&path)).await?;
1365 assert_eq!(reopened.size()?, 8192);
1366 Ok(())
1367 })
1368 }
1369}