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 maybe_async::maybe_async;
15use parking_lot::RwLock;
16use std::fmt::{self, Display, Formatter};
17use std::io::{self, Write};
18#[cfg(any(target_os = "linux", target_os = "macos"))]
19use std::os::fd::AsRawFd;
20#[cfg(unix)]
21use std::os::unix::fs::FileTypeExt;
22#[cfg(all(unix, not(target_os = "macos")))]
23use std::os::unix::fs::OpenOptionsExt;
24#[cfg(windows)]
25use std::os::windows::fs::{FileExt, OpenOptionsExt};
26#[cfg(windows)]
27use std::os::windows::io::AsRawHandle;
28use std::path::{Path, PathBuf};
29use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
30use std::{cmp, fs};
31#[cfg(unix)]
32use tracing::{debug, warn};
33#[cfg(windows)]
34use windows_sys::Win32;
35#[cfg(windows)]
36use windows_sys::Win32::System::Ioctl::{FILE_ZERO_DATA_INFORMATION, FSCTL_SET_ZERO_DATA};
37#[cfg(windows)]
38use windows_sys::Win32::System::IO::DeviceIoControl;
39
40#[derive(Debug)]
42pub struct File {
43 file: RwLock<fs::File>,
45
46 filename: Option<PathBuf>,
48
49 req_align: usize,
51
52 mem_align: usize,
54
55 zero_align: usize,
57
58 discard_align: usize,
60
61 size: AtomicU64,
65
66 common_storage_helper: CommonStorageHelper,
68
69 #[cfg(target_os = "macos")]
71 relaxed_sync: bool,
72
73 discard_unsupported: AtomicBool,
75}
76
77impl TryFrom<fs::File> for File {
78 type Error = io::Error;
79
80 fn try_from(file: fs::File) -> io::Result<Self> {
87 let open_mode = Self::detect_open_mode(&file);
88 Self::new(
89 file,
90 None,
91 open_mode.0,
92 #[cfg(unix)]
93 open_mode.1,
94 #[cfg(target_os = "macos")]
95 false,
96 )
97 }
98}
99
100#[maybe_async(AFIT)]
101impl Storage for File {
102 async fn open(opts: StorageOpenOptions) -> io::Result<Self> {
103 Self::do_open_sync(opts, fs::OpenOptions::new())
104 }
105
106 #[cfg(feature = "sync-wrappers")]
107 fn open_sync(opts: StorageOpenOptions) -> io::Result<Self> {
108 Self::do_open_sync(opts, fs::OpenOptions::new())
109 }
110
111 async fn create_open(opts: StorageCreateOptions) -> io::Result<Self> {
112 let opts = opts.modify_open_opts(|o| o.write(true));
114 let size = opts.size;
115 let prealloc_mode = opts.prealloc_mode;
116
117 let mut file_opts = fs::OpenOptions::new();
118 if opts.overwrite {
119 file_opts.create(true).truncate(true);
120 } else {
121 file_opts.create_new(true);
122 };
123
124 let file = Self::do_open_sync(opts.get_open_options(), file_opts)?;
125 if size > 0 {
126 file.resize(size, prealloc_mode)
127 .await
128 .err_context(|| "Resizing file")?;
129 }
130
131 Ok(file)
132 }
133
134 fn mem_align(&self) -> usize {
135 self.mem_align
136 }
137
138 fn req_align(&self) -> usize {
139 self.req_align
140 }
141
142 fn zero_align(&self) -> usize {
143 cmp::max(self.zero_align, self.req_align)
144 }
145
146 fn discard_align(&self) -> usize {
147 cmp::max(self.discard_align, self.req_align)
148 }
149
150 fn size(&self) -> io::Result<u64> {
151 Ok(self.size.load(Ordering::Relaxed))
152 }
153
154 fn resolve_relative_path<P: AsRef<Path>>(&self, relative: P) -> io::Result<PathBuf> {
155 let relative = relative.as_ref();
156
157 if relative.is_absolute() {
158 return Ok(relative.to_path_buf());
159 }
160
161 let filename = self
162 .filename
163 .as_ref()
164 .ok_or_else(|| io::Error::other("No filename set for base image"))?;
165
166 let dirname = filename
167 .parent()
168 .ok_or_else(|| io::Error::other("Invalid base image filename set"))?;
169
170 Ok(dirname.join(relative))
171 }
172
173 fn get_filename(&self) -> Option<PathBuf> {
174 self.filename.as_ref().cloned()
175 }
176
177 #[cfg(unix)]
178 async unsafe fn pure_readv(
179 &self,
180 mut bufv: IoVectorMut<'_>,
181 mut offset: u64,
182 ) -> io::Result<()> {
183 while !bufv.is_empty() {
184 let iovec = unsafe { bufv.as_iovec() };
185 let preadv_offset = offset
186 .try_into()
187 .map_err(|_| io::Error::other("Read offset overflow"))?;
188
189 let len = while_eintr(|| unsafe {
190 libc::preadv(
191 self.file.read().as_raw_fd(),
192 iovec.as_ptr(),
193 iovec.len() as libc::c_int,
194 preadv_offset,
195 )
196 })? as u64;
197
198 if len == 0 {
199 bufv.fill(0);
201 break;
202 }
203
204 bufv = bufv.split_tail_at(len);
205 offset = offset
206 .checked_add(len)
207 .ok_or_else(|| io::Error::other("Read offset overflow"))?;
208 }
209
210 Ok(())
211 }
212
213 #[cfg(windows)]
214 async unsafe fn pure_readv(&self, bufv: IoVectorMut<'_>, mut offset: u64) -> io::Result<()> {
215 for mut buffer in bufv.into_inner() {
216 let mut buffer: &mut [u8] = &mut buffer;
217 while !buffer.is_empty() {
218 let len = if offset >= self.size.load(Ordering::Relaxed) {
219 buffer.fill(0);
220 buffer.len()
221 } else {
222 match self.file.write().seek_read(buffer, offset)? {
223 0 => {
224 buffer.fill(0);
226 buffer.len()
227 }
228 len => len,
229 }
230 };
231 offset = offset
232 .checked_add(len as u64)
233 .ok_or_else(|| io::Error::other("Read offset overflow"))?;
234 buffer = buffer.split_at_mut(len).1;
235 }
236 }
237 Ok(())
238 }
239
240 #[cfg(unix)]
241 async unsafe fn pure_writev(&self, mut bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
242 while !bufv.is_empty() {
243 let iovec = unsafe { bufv.as_iovec() };
244 let pwritev_offset = offset
245 .try_into()
246 .map_err(|_| io::Error::other("Write offset overflow"))?;
247
248 let file = self.file.read();
251 let len = while_eintr(|| unsafe {
252 libc::pwritev(
253 file.as_raw_fd(),
254 iovec.as_ptr(),
255 iovec.len() as libc::c_int,
256 pwritev_offset,
257 )
258 })? as u64;
259
260 if len == 0 {
261 return Err(io::ErrorKind::WriteZero.into());
263 }
264
265 bufv = bufv.split_tail_at(len);
266 offset = offset
267 .checked_add(len)
268 .ok_or_else(|| io::Error::other("Write offset overflow"))?;
269 self.size.fetch_max(offset, Ordering::Relaxed);
270 }
271
272 Ok(())
273 }
274
275 #[cfg(windows)]
276 async unsafe fn pure_writev(&self, bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
277 for buffer in bufv.into_inner() {
278 let mut buffer: &[u8] = &buffer;
279 while !buffer.is_empty() {
280 #[allow(clippy::readonly_write_lock)]
283 let file = self.file.write();
284 let len = file.seek_write(buffer, offset)?;
285 if len == 0 {
286 return Err(io::ErrorKind::WriteZero.into());
288 }
289 offset = offset
290 .checked_add(len as u64)
291 .ok_or_else(|| io::Error::other("Write offset overflow"))?;
292 self.size.fetch_max(offset, Ordering::Relaxed);
293 buffer = buffer.split_at(len).1;
294 }
295 }
296 Ok(())
297 }
298
299 async unsafe fn pure_write_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
300 let end = offset
301 .checked_add(length)
302 .ok_or_else(|| io::Error::other("Write-zeroes wrap-around"))?;
303
304 self.discard_to_zero(offset, length).await?;
306
307 #[allow(clippy::readonly_write_lock)]
312 let file = self.file.write();
313 if end > self.size.load(Ordering::Relaxed) {
314 file.set_len(end)?;
315 self.size.fetch_max(end, Ordering::Relaxed);
316 }
317
318 Ok(())
319 }
320
321 #[cfg(target_os = "linux")]
322 async unsafe fn pure_write_allocated_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
323 let offset: libc::off_t = offset
324 .try_into()
325 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
326 let length: libc::off_t = length
327 .try_into()
328 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
329
330 let file = self.file.read();
331 while_eintr(|| unsafe {
333 libc::fallocate(file.as_raw_fd(), libc::FALLOC_FL_ZERO_RANGE, offset, length)
334 })
335 .map_err(Self::map_os_err)?;
336
337 self.size
339 .fetch_max(offset as u64 + length as u64, Ordering::Relaxed);
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().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().as_raw_fd()) })?;
368 return Ok(());
369 }
370 self.file.write().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();
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
452#[maybe_async]
453impl File {
454 pub fn from_open_file(file: fs::File, opts: StorageOpenOptions) -> io::Result<Self> {
459 Self::new(
460 file,
461 opts.filename,
462 opts.direct,
463 #[cfg(unix)]
464 opts.writable,
465 #[cfg(target_os = "macos")]
466 opts.relaxed_sync,
467 )
468 }
469
470 fn new(
472 mut file: fs::File,
473 filename: Option<PathBuf>,
474 direct_io: bool,
475 #[cfg(unix)] writable: bool,
476 #[cfg(target_os = "macos")] relaxed_sync: bool,
477 ) -> io::Result<Self> {
478 let size = get_file_size(&file).err_context(|| "Failed to determine file size")?;
479
480 let (req_align, mem_align, zero_align, discard_align) = if direct_io {
481 #[cfg(unix)]
482 let (min_req_align, min_mem_align) = (
483 Self::get_min_dio_req_align(&file),
484 Self::get_min_dio_mem_align(&file),
485 );
486 #[cfg(not(unix))]
487 let (min_req_align, min_mem_align) = (1, 1);
488
489 Self::probe_alignments(
490 &mut file,
491 min_req_align,
492 min_mem_align,
493 #[cfg(unix)]
494 writable,
495 )
496 } else {
497 #[cfg(unix)]
501 let (zero_align, discard_align) = if writable {
502 Self::get_fs_zero_discard_align(&file)
503 } else {
504 (1, 1)
505 };
506 #[cfg(not(unix))]
507 let (zero_align, discard_align) = (1, 1);
508 (1, 1, zero_align, discard_align)
509 };
510 assert!(req_align.is_power_of_two());
511 assert!(mem_align.is_power_of_two());
512
513 Ok(File {
514 file: RwLock::new(file),
515 filename,
516 req_align,
517 mem_align,
518 zero_align,
519 discard_align,
520 size: size.into(),
521 common_storage_helper: Default::default(),
522 #[cfg(target_os = "macos")]
523 relaxed_sync,
524 discard_unsupported: AtomicBool::new(false),
525 })
526 }
527
528 #[cfg(unix)]
533 fn detect_open_mode(file: &fs::File) -> (bool, bool) {
534 let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
536 let writable = flags < 0 || (flags & libc::O_ACCMODE) != libc::O_RDONLY;
537 #[cfg(not(target_os = "macos"))]
538 let direct_io = flags >= 0 && (flags & libc::O_DIRECT) != 0;
539 #[cfg(target_os = "macos")]
540 let direct_io = false;
541 (direct_io, writable)
542 }
543
544 #[cfg(not(unix))]
549 fn detect_open_mode(_file: &fs::File) -> (bool, bool) {
550 (false, true)
551 }
552
553 #[cfg(unix)]
555 fn host_page_size() -> usize {
556 let page_size = page_size::get();
557 if page_size.is_power_of_two() {
558 return page_size;
559 }
560 let assume = page_size.checked_next_power_of_two().unwrap_or(4096);
561 let assume = cmp::max(4096, assume);
562 warn!("Reported page size of {page_size} is not a power of two, assuming {assume}");
563 assume
564 }
565
566 #[cfg(unix)]
568 fn get_fs_zero_discard_align(file: &fs::File) -> (usize, usize) {
569 let page_size = Self::host_page_size();
570
571 #[cfg(any(target_os = "linux", target_os = "macos"))]
572 let (mut zero_align, mut discard_align) = {
573 let mut statfs: libc::statfs = unsafe { std::mem::zeroed() };
574 match while_eintr(|| unsafe { libc::fstatfs(file.as_raw_fd(), &mut statfs) }) {
576 #[cfg(target_os = "linux")]
579 Ok(_) => (statfs.f_frsize as usize, statfs.f_frsize as usize),
580 #[cfg(target_os = "macos")]
581 Ok(_) => (statfs.f_bsize as usize, statfs.f_bsize as usize),
582
583 Err(_) => (page_size, page_size),
584 }
585 };
586
587 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
588 let (mut zero_align, mut discard_align) = {
589 let _ = file;
590 (page_size, page_size)
591 };
592
593 if !zero_align.is_power_of_two() {
595 zero_align = page_size;
596 }
597 if !discard_align.is_power_of_two() {
598 discard_align = page_size;
599 }
600
601 (zero_align, discard_align)
602 }
603
604 #[cfg(unix)]
608 fn probe_alignments(
609 file: &mut fs::File,
610 min_req_align: usize,
611 min_mem_align: usize,
612 mut writable: bool,
613 ) -> (usize, usize, usize, usize) {
614 let page_size = Self::host_page_size();
615 let (zero_align, discard_align) = Self::get_fs_zero_discard_align(file);
616
617 let max_req_align = 65536;
618 let max_mem_align = cmp::max(page_size, max_req_align);
619
620 let safe_req_align = 4096;
622 let safe_mem_align = cmp::max(page_size, safe_req_align);
623
624 let mut test_buf = match IoBuffer::new(max_mem_align, max_mem_align) {
625 Ok(buf) => buf,
626 Err(err) => {
627 warn!(
628 "Failed to allocate memory to probe request alignment ({err}), \
629 falling back to {safe_req_align}/{safe_mem_align}"
630 );
631 return (safe_req_align, safe_mem_align, zero_align, discard_align);
632 }
633 };
634
635 let mut req_align: usize = min_req_align;
636 let result = loop {
637 assert!(req_align <= max_mem_align);
638 match Self::probe_access(
639 file,
640 test_buf.as_mut_range(0..req_align).into_slice(),
641 req_align.try_into().unwrap(),
642 &mut writable,
643 ) {
644 Ok(true) => break Ok(req_align),
645 Ok(false) => {
646 if req_align >= max_req_align {
647 break Err(io::Error::other(format!(
648 "Maximum I/O alignment ({max_req_align}) exceeded"
649 )));
650 }
651 if req_align == min_req_align {
653 req_align = cmp::max(min_req_align << 1, 512);
654 } else {
655 req_align <<= 1;
656 }
657 }
658 Err(err) => break Err(err),
659 }
660 };
661
662 let req_align = match result {
663 Ok(align) => {
664 debug!("Probed request alignment: {align}");
665 align
666 }
667 Err(err) => {
668 let align = cmp::max(req_align, safe_req_align);
670 warn!(
671 "Failed to probe request alignment ({err}; {}), falling back to {align} bytes",
672 err.kind(),
673 );
674 align
675 }
676 };
677
678 let mut mem_align: usize = min_mem_align;
679 let result = loop {
680 assert!(mem_align <= max_mem_align);
681 let range = (max_mem_align - mem_align)..max_mem_align;
682 match Self::probe_access(
683 file,
684 test_buf.as_mut_range(range).into_slice(),
685 0,
686 &mut writable,
687 ) {
688 Ok(true) => break Ok(mem_align),
689 Ok(false) => {
690 if mem_align >= max_mem_align {
692 break Err(io::Error::other(format!(
693 "Maximum memory alignment ({max_mem_align}) exceeded"
694 )));
695 }
696 if mem_align == min_mem_align {
698 mem_align = cmp::max(min_mem_align << 1, cmp::min(page_size, 4096));
699 } else {
700 mem_align <<= 1;
701 }
702 }
703 Err(err) => break Err(err),
704 }
705 };
706
707 let mem_align = match result {
708 Ok(align) => {
709 debug!("Probed memory alignment: {align}");
710 align
711 }
712 Err(err) => {
713 let align = cmp::max(mem_align, safe_mem_align);
715 warn!(
716 "Failed to probe memory alignment ({err}; {}), falling back to {align} bytes",
717 err.kind(),
718 );
719 align
720 }
721 };
722
723 (req_align, mem_align, zero_align, discard_align)
724 }
725
726 #[cfg(unix)]
735 fn probe_access(
736 file: &mut fs::File,
737 slice: &mut [u8],
738 offset: libc::off_t,
739 may_write: &mut bool,
740 ) -> io::Result<bool> {
741 let ret = while_eintr(|| unsafe {
744 libc::pread(
745 file.as_raw_fd(),
746 slice.as_mut_ptr() as *mut libc::c_void,
747 slice.len(),
748 offset,
749 )
750 });
751
752 if let Err(err) = ret {
753 if err.raw_os_error() == Some(libc::EINVAL) {
754 return Ok(false);
755 } else {
756 return Err(err);
757 }
758 }
759
760 if !*may_write {
761 return Ok(true);
762 }
763
764 let ret = while_eintr(|| unsafe {
766 libc::pwrite(
767 file.as_raw_fd(),
768 slice.as_ptr() as *const libc::c_void,
769 slice.len(),
770 offset,
771 )
772 });
773
774 if let Err(err) = ret {
775 if err.raw_os_error() == Some(libc::EINVAL) {
776 Ok(false)
777 } else if err.raw_os_error() == Some(libc::EBADF) {
778 *may_write = false;
779 Ok(true)
780 } else {
781 Err(err)
782 }
783 } else {
784 Ok(true)
785 }
786 }
787
788 #[cfg(unix)]
790 fn get_min_dio_req_align(file: &fs::File) -> usize {
791 #[cfg(target_os = "linux")]
792 {
793 let mut alignment = 0;
794 let res = unsafe { ioctl::blksszget(file.as_raw_fd(), &mut alignment) };
795 if res.is_ok() && alignment > 0 {
796 let alignment = alignment as usize;
797 if alignment.is_power_of_two() {
798 return alignment;
799 }
800 }
801 }
802
803 #[cfg(target_os = "macos")]
804 {
805 let mut alignment = 0;
806 let res = unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut alignment) };
807 if res.is_ok() && alignment.is_power_of_two() {
808 return alignment as usize;
809 }
810 }
811
812 #[cfg(target_os = "freebsd")]
813 {
814 let mut alignment = 0;
815 let res = unsafe { ioctl::diocgsectorsize(file.as_raw_fd(), &mut alignment) };
816 if res.is_ok() && alignment.is_power_of_two() {
817 return alignment as usize;
818 }
819 }
820
821 1
823 }
824
825 #[cfg(unix)]
827 fn get_min_dio_mem_align(_file: &fs::File) -> usize {
828 1
830 }
831
832 #[cfg(windows)]
836 fn probe_alignments(
837 _file: &mut fs::File,
838 min_req_align: usize,
839 min_mem_align: usize,
840 ) -> (usize, usize, usize, usize) {
841 (
843 cmp::max(min_req_align, 4096),
844 cmp::max(min_mem_align, 4096),
845 1,
846 1,
847 )
848 }
849
850 fn do_open_sync(opts: StorageOpenOptions, base_fs_opts: fs::OpenOptions) -> io::Result<Self> {
852 let Some(filename) = opts.filename else {
853 return Err(io::Error::new(
854 io::ErrorKind::InvalidInput,
855 "Filename required",
856 ));
857 };
858
859 let mut file_opts = base_fs_opts;
860 file_opts.read(true).write(opts.writable);
861 #[cfg(not(target_os = "macos"))]
862 if opts.direct {
863 file_opts.custom_flags(
864 #[cfg(unix)]
865 libc::O_DIRECT,
866 #[cfg(windows)]
867 windows_sys::Win32::Storage::FileSystem::FILE_FLAG_NO_BUFFERING,
868 );
869 }
870
871 let filename_owned = filename.to_owned();
872 let file = file_opts.open(filename)?;
873
874 #[cfg(target_os = "macos")]
875 if opts.direct {
876 while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) })
878 .err_context(|| "Failed to disable host cache")?;
879 }
880
881 Self::new(
882 file,
883 Some(filename_owned),
884 opts.direct,
885 #[cfg(unix)]
886 opts.writable,
887 #[cfg(target_os = "macos")]
888 opts.relaxed_sync,
889 )
890 }
891
892 #[cfg(unix)]
908 fn map_os_err(err: io::Error) -> io::Error {
909 let Some(raw) = err.raw_os_error() else {
910 return err;
911 };
912
913 let has_kind = err.kind();
914 let want_kind = match raw {
915 #[allow(unreachable_patterns)] libc::ENOTSUP | libc::EOPNOTSUPP | libc::ENODEV | libc::ENXIO | libc::ENOTTY => {
917 io::ErrorKind::Unsupported
918 }
919 _ => has_kind,
920 };
921
922 if has_kind != want_kind {
923 io::Error::new(want_kind, err)
924 } else {
925 err
926 }
927 }
928
929 #[cfg(windows)]
937 fn map_os_err(err: io::Error) -> io::Error {
938 let Some(raw) = err.raw_os_error() else {
939 return err;
940 };
941
942 let has_kind = err.kind();
943 let want_kind = match raw as u32 {
944 Win32::Foundation::ERROR_NOT_SUPPORTED | Win32::Foundation::ERROR_INVALID_FUNCTION => {
945 io::ErrorKind::Unsupported
946 }
947 _ => has_kind,
948 };
949
950 if has_kind != want_kind {
951 io::Error::new(want_kind, err)
952 } else {
953 err
954 }
955 }
956
957 #[cfg(not(any(unix, windows)))]
961 fn map_os_err(err: io::Error) -> io::Error {
962 err
963 }
964
965 async fn discard_to_zero(&self, offset: u64, length: u64) -> io::Result<()> {
967 if offset >= self.size.load(Ordering::Relaxed) {
968 return Ok(());
970 }
971
972 if self.discard_unsupported.load(Ordering::Relaxed) {
973 Err(io::ErrorKind::Unsupported.into())
974 } else if let Err(err) = self.discard_to_zero_os_specific(offset, length).await {
975 if err.kind() == io::ErrorKind::Unsupported {
976 self.discard_unsupported.store(true, Ordering::Relaxed);
977 }
978 Err(err)
979 } else {
980 Ok(())
981 }
982 }
983
984 #[cfg(target_os = "linux")]
986 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
987 let offset: libc::off_t = offset
988 .try_into()
989 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
990 let length: libc::off_t = length
991 .try_into()
992 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
993
994 let file = self.file.read();
995 while_eintr(|| unsafe {
997 libc::fallocate(
998 file.as_raw_fd(),
999 libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
1000 offset,
1001 length,
1002 )
1003 })
1004 .map_err(Self::map_os_err)?;
1005
1006 Ok(())
1007 }
1008
1009 #[cfg(windows)]
1011 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1012 let offset: i64 = offset
1013 .try_into()
1014 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
1015 let length: i64 = length
1016 .try_into()
1017 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
1018
1019 let end = offset.saturating_add(length);
1020 let params = FILE_ZERO_DATA_INFORMATION {
1021 FileOffset: offset,
1022 BeyondFinalZero: end,
1023 };
1024 let mut _returned = 0;
1025 let file = self.file.read();
1026 let ret = unsafe {
1030 DeviceIoControl(
1031 file.as_raw_handle(),
1032 FSCTL_SET_ZERO_DATA,
1033 (¶ms as *const FILE_ZERO_DATA_INFORMATION).cast::<std::ffi::c_void>(),
1034 size_of_val(¶ms) as u32,
1035 std::ptr::null_mut(),
1036 0,
1037 &mut _returned,
1038 std::ptr::null_mut(),
1039 )
1040 };
1041 if ret == 0 {
1042 return Err(Self::map_os_err(io::Error::last_os_error()));
1043 }
1044
1045 Ok(())
1046 }
1047
1048 #[cfg(target_os = "macos")]
1050 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1051 let offset: libc::off_t = offset
1052 .try_into()
1053 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
1054 let length: libc::off_t = length
1055 .try_into()
1056 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
1057
1058 let params = libc::fpunchhole_t {
1059 fp_flags: 0,
1060 reserved: 0,
1061 fp_offset: offset,
1062 fp_length: length,
1063 };
1064 let file = self.file.read();
1065 while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_PUNCHHOLE, ¶ms) })
1067 .map_err(Self::map_os_err)?;
1068
1069 Ok(())
1070 }
1071
1072 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1074 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1075 Err(io::ErrorKind::Unsupported.into())
1076 }
1077}
1078
1079impl Display for File {
1080 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1081 if let Some(filename) = self.filename.as_ref() {
1082 write!(f, "file:{filename:?}")
1083 } else {
1084 write!(f, "file:<unknown path>")
1085 }
1086 }
1087}
1088
1089fn get_file_size(file: &fs::File) -> io::Result<u64> {
1094 #[allow(clippy::bind_instead_of_map)]
1095 file.metadata().and_then(|m| {
1096 #[cfg(unix)]
1097 if m.file_type().is_block_device() || m.file_type().is_char_device() {
1098 return get_device_size(file);
1099 }
1100 Ok(m.len())
1101 })
1102}
1103
1104cfg_if! {
1105 if #[cfg(target_os = "linux")] {
1106 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1108 let mut size = 0;
1109 unsafe { ioctl::blkgetsize64(file.as_raw_fd(), &mut size) }?;
1110 Ok(size)
1111 }
1112 } else if #[cfg(target_os = "macos")] {
1113 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1115 let mut block_size = 0;
1116 unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut block_size) }?;
1117 let mut block_count = 0;
1118 unsafe { ioctl::dkiocgetblockcount(file.as_raw_fd(), &mut block_count) }?;
1119 Ok(u64::from(block_size) * block_count)
1120 }
1121 } else if #[cfg(target_os = "freebsd")] {
1122 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1124 let mut size = 0;
1125 unsafe { ioctl::diocgmediasize(file.as_raw_fd(), &mut size) }?;
1126 Ok(size as u64)
1127 }
1128 } else if #[cfg(unix)] {
1129 fn get_device_size(_file: &fs::File) -> io::Result<u64> {
1131 Err(io::ErrorKind::Unsupported.into())
1132 }
1133 }
1134}
1135
1136mod ioctl {
1138 #[cfg(unix)]
1139 use nix::ioctl_read;
1140 #[cfg(target_os = "linux")]
1141 use nix::ioctl_read_bad;
1142
1143 #[cfg(target_os = "linux")]
1146 ioctl_read!(blkgetsize64, 0x12, 114, u64);
1147
1148 #[cfg(target_os = "linux")]
1149 ioctl_read_bad!(blksszget, libc::BLKSSZGET, libc::c_int);
1150
1151 #[cfg(target_os = "macos")]
1154 ioctl_read!(dkiocgetblocksize, 'd', 24, u32);
1155
1156 #[cfg(target_os = "macos")]
1157 ioctl_read!(dkiocgetblockcount, 'd', 25, u64);
1158
1159 #[cfg(target_os = "freebsd")]
1162 ioctl_read!(diocgsectorsize, 'd', 128, libc::c_uint);
1163
1164 #[cfg(target_os = "freebsd")]
1165 ioctl_read!(diocgmediasize, 'd', 129, libc::off_t);
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use super::File;
1171 use crate::{Storage, StorageCreateOptions, StorageExt};
1172 use std::path::PathBuf;
1173 #[cfg(unix)]
1174 use std::time::{SystemTime, UNIX_EPOCH};
1175 use std::{cmp, env, fs, io};
1176
1177 #[cfg(windows)]
1178 #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1179 async fn zero_data_preserves_the_byte_at_the_exclusive_end() {
1180 let path = test_dir().join(format!(
1181 "imago-zero-data-boundary-{}-{}",
1182 std::process::id(),
1183 std::time::SystemTime::now()
1184 .duration_since(std::time::UNIX_EPOCH)
1185 .unwrap()
1186 .as_nanos()
1187 ));
1188 fs::write(&path, vec![0x5a; 12288]).unwrap();
1189 let host = fs::OpenOptions::new()
1190 .read(true)
1191 .write(true)
1192 .open(&path)
1193 .unwrap();
1194 let storage = File::try_from(host).unwrap();
1195 let zero_result = storage.discard_to_zero_os_specific(4096, 4096).await;
1196 drop(storage);
1197
1198 let contents = fs::read(&path);
1199 let _ = fs::remove_file(&path);
1200 if zero_result
1201 .as_ref()
1202 .is_err_and(|err| err.kind() == io::ErrorKind::Unsupported)
1203 {
1204 return;
1206 }
1207 zero_result.unwrap();
1208 let contents = contents.unwrap();
1209 assert_eq!(contents[4095], 0x5a);
1210 assert!(contents[4096..8192].iter().all(|byte| *byte == 0));
1211 assert_eq!(contents[8192], 0x5a);
1212 }
1213
1214 #[cfg(windows)]
1215 #[test]
1216 fn map_os_err_unsupported() {
1217 use windows_sys::Win32::Foundation::{
1218 ERROR_ACCESS_DENIED, ERROR_INVALID_FUNCTION, ERROR_NOT_SUPPORTED,
1219 };
1220
1221 for code in [ERROR_NOT_SUPPORTED, ERROR_INVALID_FUNCTION] {
1222 let err = File::map_os_err(io::Error::from_raw_os_error(code as i32));
1223 assert_eq!(err.kind(), io::ErrorKind::Unsupported);
1224 }
1225
1226 let err = File::map_os_err(io::Error::from_raw_os_error(ERROR_ACCESS_DENIED as i32));
1228 assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
1229 }
1230
1231 #[cfg(unix)]
1232 #[test]
1233 fn buffered_writable_file_is_not_modified_during_construction() {
1234 let unique = SystemTime::now()
1235 .duration_since(UNIX_EPOCH)
1236 .unwrap()
1237 .as_nanos();
1238 let path = test_dir().join(format!(
1239 "imago-buffered-probe-{}-{unique}",
1240 std::process::id()
1241 ));
1242 let file = fs::OpenOptions::new()
1243 .read(true)
1244 .write(true)
1245 .create_new(true)
1246 .open(&path)
1247 .unwrap();
1248
1249 let storage = File::try_from(file).unwrap();
1250 let alignments = (
1251 storage.req_align,
1252 storage.mem_align,
1253 storage.zero_align,
1254 storage.discard_align,
1255 );
1256 let len = fs::metadata(&path).unwrap().len();
1257 let expected_fs_align = {
1258 let probe = fs::File::open(&path).unwrap();
1259 super::File::get_fs_zero_discard_align(&probe)
1260 };
1261 drop(storage);
1262 fs::remove_file(path).unwrap();
1263
1264 assert_eq!(alignments, (1, 1, expected_fs_align.0, expected_fs_align.1));
1265 assert_eq!(len, 0);
1266 }
1267
1268 fn test_dir() -> PathBuf {
1274 env::var_os("TEST_DIR")
1275 .map(PathBuf::from)
1276 .unwrap_or_else(env::temp_dir)
1277 }
1278
1279 struct TempPath(PathBuf);
1280
1281 impl Drop for TempPath {
1282 fn drop(&mut self) {
1283 let _ = std::fs::remove_file(&self.0);
1284 }
1285 }
1286
1287 #[cfg(any(target_os = "linux", target_os = "macos", windows))]
1288 #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1289 async fn tail_discard_keeps_file_length() -> io::Result<()> {
1290 let path = test_dir().join(format!("imago-tail-discard-{}.raw", std::process::id()));
1291 let _temp_path = TempPath(path.clone());
1292
1293 let file = File::create_open(StorageCreateOptions::new().filename(&path)).await?;
1294 let granularity = cmp::max(file.discard_align(), 4096) as u64;
1297 file.write(&vec![0xabu8; 2 * granularity as usize][..], 0)
1298 .await?;
1299 assert_eq!(std::fs::metadata(&path)?.len(), 2 * granularity);
1300
1301 file.discard(granularity, granularity).await?;
1302 assert_eq!(std::fs::metadata(&path)?.len(), 2 * granularity);
1303
1304 let mut prefix = vec![0u8; granularity as usize];
1305 file.read(&mut prefix, 0).await?;
1306 assert_eq!(prefix, vec![0xabu8; granularity as usize]);
1307
1308 if !file
1310 .discard_unsupported
1311 .load(std::sync::atomic::Ordering::Relaxed)
1312 {
1313 let mut tail = vec![0xffu8; granularity as usize];
1314 file.read(&mut tail, granularity).await?;
1315 assert!(tail.iter().all(|&byte| byte == 0));
1316 }
1317
1318 io::Result::Ok(())
1319 }
1320
1321 #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1322 async fn write_allocated_zeroes_past_eof_updates_size() -> io::Result<()> {
1323 let path = test_dir().join(format!(
1324 "imago-allocated-zeroes-past-eof-{}.raw",
1325 std::process::id()
1326 ));
1327 let _temp_path = TempPath(path.clone());
1328
1329 let file = File::create_open(StorageCreateOptions::new().filename(&path)).await?;
1330 let granularity = cmp::max(file.zero_align(), 4096) as u64;
1333
1334 file.write(&vec![0xabu8; granularity as usize][..], 0)
1335 .await?;
1336 assert_eq!(fs::metadata(&path)?.len(), granularity);
1337 assert_eq!(file.size()?, granularity);
1338
1339 file.write_allocated_zeroes(granularity, granularity)
1340 .await?;
1341 assert_eq!(fs::metadata(&path)?.len(), 2 * granularity);
1342 assert_eq!(file.size()?, 2 * granularity);
1343
1344 let mut tail = vec![0xffu8; granularity as usize];
1345 file.read(&mut tail, granularity).await?;
1346 assert!(tail.iter().all(|&byte| byte == 0));
1347
1348 io::Result::Ok(())
1349 }
1350
1351 #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1352 async fn write_zeroes_past_eof_grows_file() -> io::Result<()> {
1353 let path = test_dir().join(format!(
1354 "imago-write-zeroes-past-eof-{}.raw",
1355 std::process::id()
1356 ));
1357 let _temp_path = TempPath(path.clone());
1358
1359 let file = File::create_open(StorageCreateOptions::new().filename(&path)).await?;
1360 let granularity = cmp::max(file.zero_align(), 4096) as u64;
1363 file.write(&vec![0xabu8; granularity as usize][..], 0)
1364 .await?;
1365 assert_eq!(fs::metadata(&path)?.len(), granularity);
1366 assert_eq!(file.size()?, granularity);
1367
1368 file.write_zeroes(granularity, granularity).await?;
1370 assert_eq!(fs::metadata(&path)?.len(), 2 * granularity);
1371 assert_eq!(file.size()?, 2 * granularity);
1372
1373 let mut buf = vec![0u8; 2 * granularity as usize];
1374 file.read(&mut buf, 0).await?;
1375 assert!(buf[..granularity as usize].iter().all(|&byte| byte == 0xab));
1376 assert!(buf[granularity as usize..].iter().all(|&byte| byte == 0));
1377
1378 file.write(&vec![0xcdu8; granularity as usize][..], granularity)
1380 .await?;
1381 file.write_zeroes(granularity, 2 * granularity).await?;
1382 assert_eq!(fs::metadata(&path)?.len(), 3 * granularity);
1383 assert_eq!(file.size()?, 3 * granularity);
1384
1385 let mut buf = vec![0xffu8; 3 * granularity as usize];
1386 file.read(&mut buf, 0).await?;
1387 assert!(buf[..granularity as usize].iter().all(|&byte| byte == 0xab));
1388 assert!(buf[granularity as usize..].iter().all(|&byte| byte == 0));
1389
1390 io::Result::Ok(())
1391 }
1392
1393 #[cfg(windows)]
1395 #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1396 async fn read_past_external_truncation() -> io::Result<()> {
1397 use crate::StorageOpenOptions;
1398 use std::sync::atomic::{AtomicBool, Ordering};
1399 use std::sync::Arc;
1400 use std::time::Duration;
1401
1402 let path = test_dir().join(format!(
1403 "imago-read-past-external-truncation-{}.raw",
1404 std::process::id()
1405 ));
1406 let _temp_path = TempPath(path.clone());
1407 std::fs::write(&path, vec![0xabu8; 8192])?;
1408
1409 let file = File::open(StorageOpenOptions::new().filename(&path)).await?;
1410 fs::OpenOptions::new()
1412 .write(true)
1413 .open(&path)?
1414 .set_len(4096)?;
1415
1416 let done = Arc::new(AtomicBool::new(false));
1418 let watchdog_done = Arc::clone(&done);
1419 std::thread::spawn(move || {
1420 std::thread::sleep(Duration::from_secs(10));
1421 if !watchdog_done.load(Ordering::Relaxed) {
1422 eprintln!("read_past_external_truncation: Read hangs, aborting");
1423 std::process::abort();
1424 }
1425 });
1426
1427 let mut buf = vec![0xffu8; 8192];
1428 file.read(&mut buf[..], 0).await?;
1429 done.store(true, Ordering::Relaxed);
1430
1431 assert!(buf[..4096].iter().all(|&byte| byte == 0xab));
1432 assert!(buf[4096..].iter().all(|&byte| byte == 0));
1433
1434 io::Result::Ok(())
1435 }
1436}