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 std::fmt::{self, Display, Formatter};
16use std::io::{self, Write};
17#[cfg(any(target_os = "linux", target_os = "macos"))]
18use std::os::fd::AsRawFd;
19#[cfg(unix)]
20use std::os::unix::fs::FileTypeExt;
21#[cfg(all(unix, not(target_os = "macos")))]
22use std::os::unix::fs::OpenOptionsExt;
23#[cfg(windows)]
24use std::os::windows::fs::{FileExt, OpenOptionsExt};
25#[cfg(windows)]
26use std::os::windows::io::AsRawHandle;
27use std::path::{Path, PathBuf};
28use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
29use std::sync::RwLock;
30use std::{cmp, fs};
31#[cfg(unix)]
32use tracing::{debug, warn};
33#[cfg(windows)]
34use windows_sys::Win32::System::Ioctl::{FILE_ZERO_DATA_INFORMATION, FSCTL_SET_ZERO_DATA};
35#[cfg(windows)]
36use windows_sys::Win32::System::IO::DeviceIoControl;
37
38#[derive(Debug)]
40pub struct File {
41 file: RwLock<fs::File>,
43
44 filename: Option<PathBuf>,
46
47 req_align: usize,
49
50 mem_align: usize,
52
53 zero_align: usize,
55
56 discard_align: usize,
58
59 size: AtomicU64,
63
64 common_storage_helper: CommonStorageHelper,
66
67 #[cfg(target_os = "macos")]
69 relaxed_sync: bool,
70
71 discard_unsupported: AtomicBool,
73}
74
75impl TryFrom<fs::File> for File {
76 type Error = io::Error;
77
78 fn try_from(file: fs::File) -> io::Result<Self> {
85 let open_mode = Self::detect_open_mode(&file);
86 Self::new(
87 file,
88 None,
89 open_mode.0,
90 #[cfg(unix)]
91 open_mode.1,
92 #[cfg(target_os = "macos")]
93 false,
94 )
95 }
96}
97
98#[maybe_async(AFIT)]
99impl Storage for File {
100 async fn open(opts: StorageOpenOptions) -> io::Result<Self> {
101 Self::do_open_sync(opts, fs::OpenOptions::new())
102 }
103
104 #[cfg(feature = "sync-wrappers")]
105 fn open_sync(opts: StorageOpenOptions) -> io::Result<Self> {
106 Self::do_open_sync(opts, fs::OpenOptions::new())
107 }
108
109 async fn create_open(opts: StorageCreateOptions) -> io::Result<Self> {
110 let opts = opts.modify_open_opts(|o| o.write(true));
112 let size = opts.size;
113 let prealloc_mode = opts.prealloc_mode;
114
115 let mut file_opts = fs::OpenOptions::new();
116 if opts.overwrite {
117 file_opts.create(true).truncate(true);
118 } else {
119 file_opts.create_new(true);
120 };
121
122 let file = Self::do_open_sync(opts.get_open_options(), file_opts)?;
123 if size > 0 {
124 file.resize(size, prealloc_mode)
125 .await
126 .err_context(|| "Resizing file")?;
127 }
128
129 Ok(file)
130 }
131
132 fn mem_align(&self) -> usize {
133 self.mem_align
134 }
135
136 fn req_align(&self) -> usize {
137 self.req_align
138 }
139
140 fn zero_align(&self) -> usize {
141 cmp::max(self.zero_align, self.req_align)
142 }
143
144 fn discard_align(&self) -> usize {
145 cmp::max(self.discard_align, self.req_align)
146 }
147
148 fn size(&self) -> io::Result<u64> {
149 Ok(self.size.load(Ordering::Relaxed))
150 }
151
152 fn resolve_relative_path<P: AsRef<Path>>(&self, relative: P) -> io::Result<PathBuf> {
153 let relative = relative.as_ref();
154
155 if relative.is_absolute() {
156 return Ok(relative.to_path_buf());
157 }
158
159 let filename = self
160 .filename
161 .as_ref()
162 .ok_or_else(|| io::Error::other("No filename set for base image"))?;
163
164 let dirname = filename
165 .parent()
166 .ok_or_else(|| io::Error::other("Invalid base image filename set"))?;
167
168 Ok(dirname.join(relative))
169 }
170
171 fn get_filename(&self) -> Option<PathBuf> {
172 self.filename.as_ref().cloned()
173 }
174
175 #[cfg(unix)]
176 async unsafe fn pure_readv(
177 &self,
178 mut bufv: IoVectorMut<'_>,
179 mut offset: u64,
180 ) -> io::Result<()> {
181 while !bufv.is_empty() {
182 let iovec = unsafe { bufv.as_iovec() };
183 let preadv_offset = offset
184 .try_into()
185 .map_err(|_| io::Error::other("Read offset overflow"))?;
186
187 let len = while_eintr(|| unsafe {
188 libc::preadv(
189 self.file.read().unwrap().as_raw_fd(),
190 iovec.as_ptr(),
191 iovec.len() as libc::c_int,
192 preadv_offset,
193 )
194 })? as u64;
195
196 if len == 0 {
197 bufv.fill(0);
199 break;
200 }
201
202 bufv = bufv.split_tail_at(len);
203 offset = offset
204 .checked_add(len)
205 .ok_or_else(|| io::Error::other("Read offset overflow"))?;
206 }
207
208 Ok(())
209 }
210
211 #[cfg(windows)]
212 async unsafe fn pure_readv(&self, bufv: IoVectorMut<'_>, mut offset: u64) -> io::Result<()> {
213 for mut buffer in bufv.into_inner() {
214 let mut buffer: &mut [u8] = &mut buffer;
215 while !buffer.is_empty() {
216 let len = if offset >= self.size.load(Ordering::Relaxed) {
217 buffer.fill(0);
218 buffer.len()
219 } else {
220 self.file.write().unwrap().seek_read(buffer, offset)?
221 };
222 offset = offset
223 .checked_add(len as u64)
224 .ok_or_else(|| io::Error::other("Read offset overflow"))?;
225 buffer = buffer.split_at_mut(len).1;
226 }
227 }
228 Ok(())
229 }
230
231 #[cfg(unix)]
232 async unsafe fn pure_writev(&self, mut bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
233 while !bufv.is_empty() {
234 let iovec = unsafe { bufv.as_iovec() };
235 let pwritev_offset = offset
236 .try_into()
237 .map_err(|_| io::Error::other("Write offset overflow"))?;
238
239 let len = while_eintr(|| unsafe {
240 libc::pwritev(
241 self.file.read().unwrap().as_raw_fd(),
242 iovec.as_ptr(),
243 iovec.len() as libc::c_int,
244 pwritev_offset,
245 )
246 })? as u64;
247
248 if len == 0 {
249 return Err(io::ErrorKind::WriteZero.into());
251 }
252
253 bufv = bufv.split_tail_at(len);
254 offset = offset
255 .checked_add(len)
256 .ok_or_else(|| io::Error::other("Write offset overflow"))?;
257 self.size.fetch_max(offset, Ordering::Relaxed);
258 }
259
260 Ok(())
261 }
262
263 #[cfg(windows)]
264 async unsafe fn pure_writev(&self, bufv: IoVector<'_>, mut offset: u64) -> io::Result<()> {
265 for buffer in bufv.into_inner() {
266 let mut buffer: &[u8] = &buffer;
267 while !buffer.is_empty() {
268 let len = self.file.write().unwrap().seek_write(buffer, offset)?;
269 offset = offset
270 .checked_add(len as u64)
271 .ok_or_else(|| io::Error::other("Write offset overflow"))?;
272 self.size.fetch_max(offset, Ordering::Relaxed);
273 buffer = buffer.split_at(len).1;
274 }
275 }
276 Ok(())
277 }
278
279 async unsafe fn pure_write_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
280 self.discard_to_zero(offset, length).await
281 }
282
283 #[cfg(target_os = "linux")]
284 async unsafe fn pure_write_allocated_zeroes(&self, offset: u64, length: u64) -> io::Result<()> {
285 let offset: libc::off_t = offset
286 .try_into()
287 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
288 let length: libc::off_t = length
289 .try_into()
290 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
291
292 let file = self.file.read().unwrap();
293 while_eintr(|| unsafe {
295 libc::fallocate(file.as_raw_fd(), libc::FALLOC_FL_ZERO_RANGE, offset, length)
296 })
297 .map_err(Self::map_os_err)?;
298
299 Ok(())
300 }
301
302 async unsafe fn pure_discard(&self, offset: u64, length: u64) -> io::Result<()> {
303 if let Err(err) = self.discard_to_zero(offset, length).await {
304 if err.kind() == io::ErrorKind::Unsupported {
308 Ok(())
309 } else {
310 Err(err)
311 }
312 } else {
313 Ok(())
314 }
315 }
316
317 async fn flush(&self) -> io::Result<()> {
318 self.file.write().unwrap().flush()
319 }
320
321 async fn sync(&self) -> io::Result<()> {
322 #[cfg(target_os = "macos")]
323 if self.relaxed_sync {
324 while_eintr(|| unsafe { libc::fsync(self.file.write().unwrap().as_raw_fd()) })?;
326 return Ok(());
327 }
328 self.file.write().unwrap().sync_all()
329 }
330
331 async unsafe fn invalidate_cache(&self) -> io::Result<()> {
332 Ok(())
338 }
339
340 fn get_storage_helper(&self) -> &CommonStorageHelper {
341 &self.common_storage_helper
342 }
343
344 async fn resize(&self, new_size: u64, prealloc_mode: PreallocateMode) -> io::Result<()> {
345 let file = self.file.write().unwrap();
346 let current_size = self.size.load(Ordering::Relaxed);
347
348 match new_size.cmp(¤t_size) {
349 std::cmp::Ordering::Equal => return Ok(()),
350 std::cmp::Ordering::Less => {
351 file.set_len(new_size)?;
352 self.size.fetch_min(new_size, Ordering::Relaxed);
353 return Ok(());
354 }
355 std::cmp::Ordering::Greater => (), }
357
358 match prealloc_mode {
359 PreallocateMode::None | PreallocateMode::Zero => file.set_len(new_size)?,
360 PreallocateMode::Allocate => {
361 #[cfg(not(unix))]
362 return Err(io::ErrorKind::Unsupported.into());
363
364 #[cfg(all(unix, not(target_os = "macos")))]
365 {
366 let ofs = current_size.try_into().map_err(io::Error::other)?;
367 let len = (new_size - current_size)
368 .try_into()
369 .map_err(io::Error::other)?;
370 while_eintr(|| unsafe { libc::fallocate(file.as_raw_fd(), 0, ofs, len) })
371 .map_err(Self::map_os_err)?;
372 }
373
374 #[cfg(target_os = "macos")]
375 {
376 let mut params = libc::fstore_t {
381 fst_flags: libc::F_ALLOCATEALL,
382 fst_posmode: libc::F_PEOFPOSMODE,
383 fst_offset: 0,
384 fst_length: (new_size - current_size)
385 .try_into()
386 .map_err(io::Error::other)?,
387 fst_bytesalloc: 0, };
389 while_eintr(|| unsafe {
390 libc::fcntl(file.as_raw_fd(), libc::F_PREALLOCATE, &mut params)
391 })
392 .map_err(Self::map_os_err)?;
393
394 file.set_len(new_size)?;
395 }
396 }
397 PreallocateMode::WriteData => {
398 drop(file);
401 write_full_zeroes(self, current_size, new_size - current_size).await?;
402 }
403 }
404
405 self.size.fetch_max(new_size, Ordering::Relaxed);
406 Ok(())
407 }
408}
409
410#[maybe_async]
411impl File {
412 pub fn from_open_file(file: fs::File, opts: StorageOpenOptions) -> io::Result<Self> {
417 Self::new(
418 file,
419 opts.filename,
420 opts.direct,
421 #[cfg(unix)]
422 opts.writable,
423 #[cfg(target_os = "macos")]
424 opts.relaxed_sync,
425 )
426 }
427
428 fn new(
430 mut file: fs::File,
431 filename: Option<PathBuf>,
432 direct_io: bool,
433 #[cfg(unix)] writable: bool,
434 #[cfg(target_os = "macos")] relaxed_sync: bool,
435 ) -> io::Result<Self> {
436 let size = get_file_size(&file).err_context(|| "Failed to determine file size")?;
437
438 let (req_align, mem_align, zero_align, discard_align) = if direct_io {
439 #[cfg(unix)]
440 let (min_req_align, min_mem_align) = (
441 Self::get_min_dio_req_align(&file),
442 Self::get_min_dio_mem_align(&file),
443 );
444 #[cfg(not(unix))]
445 let (min_req_align, min_mem_align) = (1, 1);
446
447 Self::probe_alignments(
448 &mut file,
449 min_req_align,
450 min_mem_align,
451 #[cfg(unix)]
452 writable,
453 )
454 } else {
455 #[cfg(unix)]
459 let (zero_align, discard_align) = if writable {
460 Self::get_fs_zero_discard_align(&file)
461 } else {
462 (1, 1)
463 };
464 #[cfg(not(unix))]
465 let (zero_align, discard_align) = (1, 1);
466 (1, 1, zero_align, discard_align)
467 };
468 assert!(req_align.is_power_of_two());
469 assert!(mem_align.is_power_of_two());
470
471 Ok(File {
472 file: RwLock::new(file),
473 filename,
474 req_align,
475 mem_align,
476 zero_align,
477 discard_align,
478 size: size.into(),
479 common_storage_helper: Default::default(),
480 #[cfg(target_os = "macos")]
481 relaxed_sync,
482 discard_unsupported: AtomicBool::new(false),
483 })
484 }
485
486 #[cfg(unix)]
491 fn detect_open_mode(file: &fs::File) -> (bool, bool) {
492 let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
494 let writable = flags < 0 || (flags & libc::O_ACCMODE) != libc::O_RDONLY;
495 #[cfg(not(target_os = "macos"))]
496 let direct_io = flags >= 0 && (flags & libc::O_DIRECT) != 0;
497 #[cfg(target_os = "macos")]
498 let direct_io = false;
499 (direct_io, writable)
500 }
501
502 #[cfg(not(unix))]
507 fn detect_open_mode(_file: &fs::File) -> (bool, bool) {
508 (false, true)
509 }
510
511 #[cfg(unix)]
513 fn host_page_size() -> usize {
514 let page_size = page_size::get();
515 if page_size.is_power_of_two() {
516 return page_size;
517 }
518 let assume = page_size.checked_next_power_of_two().unwrap_or(4096);
519 let assume = cmp::max(4096, assume);
520 warn!("Reported page size of {page_size} is not a power of two, assuming {assume}");
521 assume
522 }
523
524 #[cfg(unix)]
526 fn get_fs_zero_discard_align(file: &fs::File) -> (usize, usize) {
527 let page_size = Self::host_page_size();
528
529 #[cfg(any(target_os = "linux", target_os = "macos"))]
530 let (mut zero_align, mut discard_align) = {
531 let mut statfs: libc::statfs = unsafe { std::mem::zeroed() };
532 match while_eintr(|| unsafe { libc::fstatfs(file.as_raw_fd(), &mut statfs) }) {
534 #[cfg(target_os = "linux")]
537 Ok(_) => (statfs.f_frsize as usize, statfs.f_frsize as usize),
538 #[cfg(target_os = "macos")]
539 Ok(_) => (statfs.f_bsize as usize, statfs.f_bsize as usize),
540
541 Err(_) => (page_size, page_size),
542 }
543 };
544
545 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
546 let (mut zero_align, mut discard_align) = {
547 let _ = file;
548 (page_size, page_size)
549 };
550
551 if !zero_align.is_power_of_two() {
553 zero_align = page_size;
554 }
555 if !discard_align.is_power_of_two() {
556 discard_align = page_size;
557 }
558
559 (zero_align, discard_align)
560 }
561
562 #[cfg(unix)]
566 fn probe_alignments(
567 file: &mut fs::File,
568 min_req_align: usize,
569 min_mem_align: usize,
570 mut writable: bool,
571 ) -> (usize, usize, usize, usize) {
572 let page_size = Self::host_page_size();
573 let (zero_align, discard_align) = Self::get_fs_zero_discard_align(file);
574
575 let max_req_align = 65536;
576 let max_mem_align = cmp::max(page_size, max_req_align);
577
578 let safe_req_align = 4096;
580 let safe_mem_align = cmp::max(page_size, safe_req_align);
581
582 let mut test_buf = match IoBuffer::new(max_mem_align, max_mem_align) {
583 Ok(buf) => buf,
584 Err(err) => {
585 warn!(
586 "Failed to allocate memory to probe request alignment ({err}), \
587 falling back to {safe_req_align}/{safe_mem_align}"
588 );
589 return (safe_req_align, safe_mem_align, zero_align, discard_align);
590 }
591 };
592
593 let mut req_align: usize = min_req_align;
594 let result = loop {
595 assert!(req_align <= max_mem_align);
596 match Self::probe_access(
597 file,
598 test_buf.as_mut_range(0..req_align).into_slice(),
599 req_align.try_into().unwrap(),
600 &mut writable,
601 ) {
602 Ok(true) => break Ok(req_align),
603 Ok(false) => {
604 if req_align >= max_req_align {
605 break Err(io::Error::other(format!(
606 "Maximum I/O alignment ({max_req_align}) exceeded"
607 )));
608 }
609 if req_align == min_req_align {
611 req_align = cmp::max(min_req_align << 1, 512);
612 } else {
613 req_align <<= 1;
614 }
615 }
616 Err(err) => break Err(err),
617 }
618 };
619
620 let req_align = match result {
621 Ok(align) => {
622 debug!("Probed request alignment: {align}");
623 align
624 }
625 Err(err) => {
626 let align = cmp::max(req_align, safe_req_align);
628 warn!(
629 "Failed to probe request alignment ({err}; {}), falling back to {align} bytes",
630 err.kind(),
631 );
632 align
633 }
634 };
635
636 let mut mem_align: usize = min_mem_align;
637 let result = loop {
638 assert!(mem_align <= max_mem_align);
639 let range = (max_mem_align - mem_align)..max_mem_align;
640 match Self::probe_access(
641 file,
642 test_buf.as_mut_range(range).into_slice(),
643 0,
644 &mut writable,
645 ) {
646 Ok(true) => break Ok(mem_align),
647 Ok(false) => {
648 if mem_align >= max_mem_align {
650 break Err(io::Error::other(format!(
651 "Maximum memory alignment ({max_mem_align}) exceeded"
652 )));
653 }
654 if mem_align == min_mem_align {
656 mem_align = cmp::max(min_mem_align << 1, cmp::min(page_size, 4096));
657 } else {
658 mem_align <<= 1;
659 }
660 }
661 Err(err) => break Err(err),
662 }
663 };
664
665 let mem_align = match result {
666 Ok(align) => {
667 debug!("Probed memory alignment: {align}");
668 align
669 }
670 Err(err) => {
671 let align = cmp::max(mem_align, safe_mem_align);
673 warn!(
674 "Failed to probe memory alignment ({err}; {}), falling back to {align} bytes",
675 err.kind(),
676 );
677 align
678 }
679 };
680
681 (req_align, mem_align, zero_align, discard_align)
682 }
683
684 #[cfg(unix)]
693 fn probe_access(
694 file: &mut fs::File,
695 slice: &mut [u8],
696 offset: libc::off_t,
697 may_write: &mut bool,
698 ) -> io::Result<bool> {
699 let ret = while_eintr(|| unsafe {
702 libc::pread(
703 file.as_raw_fd(),
704 slice.as_mut_ptr() as *mut libc::c_void,
705 slice.len(),
706 offset,
707 )
708 });
709
710 if let Err(err) = ret {
711 if err.raw_os_error() == Some(libc::EINVAL) {
712 return Ok(false);
713 } else {
714 return Err(err);
715 }
716 }
717
718 if !*may_write {
719 return Ok(true);
720 }
721
722 let ret = while_eintr(|| unsafe {
724 libc::pwrite(
725 file.as_raw_fd(),
726 slice.as_ptr() as *const libc::c_void,
727 slice.len(),
728 offset,
729 )
730 });
731
732 if let Err(err) = ret {
733 if err.raw_os_error() == Some(libc::EINVAL) {
734 Ok(false)
735 } else if err.raw_os_error() == Some(libc::EBADF) {
736 *may_write = false;
737 Ok(true)
738 } else {
739 Err(err)
740 }
741 } else {
742 Ok(true)
743 }
744 }
745
746 #[cfg(unix)]
748 fn get_min_dio_req_align(file: &fs::File) -> usize {
749 #[cfg(target_os = "linux")]
750 {
751 let mut alignment = 0;
752 let res = unsafe { ioctl::blksszget(file.as_raw_fd(), &mut alignment) };
753 if res.is_ok() && alignment > 0 {
754 let alignment = alignment as usize;
755 if alignment.is_power_of_two() {
756 return alignment;
757 }
758 }
759 }
760
761 #[cfg(target_os = "macos")]
762 {
763 let mut alignment = 0;
764 let res = unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut alignment) };
765 if res.is_ok() && alignment.is_power_of_two() {
766 return alignment as usize;
767 }
768 }
769
770 #[cfg(target_os = "freebsd")]
771 {
772 let mut alignment = 0;
773 let res = unsafe { ioctl::diocgsectorsize(file.as_raw_fd(), &mut alignment) };
774 if res.is_ok() && alignment.is_power_of_two() {
775 return alignment as usize;
776 }
777 }
778
779 1
781 }
782
783 #[cfg(unix)]
785 fn get_min_dio_mem_align(_file: &fs::File) -> usize {
786 1
788 }
789
790 #[cfg(windows)]
794 fn probe_alignments(
795 _file: &mut fs::File,
796 min_req_align: usize,
797 min_mem_align: usize,
798 ) -> (usize, usize, usize, usize) {
799 (
801 cmp::max(min_req_align, 4096),
802 cmp::max(min_mem_align, 4096),
803 1,
804 1,
805 )
806 }
807
808 fn do_open_sync(opts: StorageOpenOptions, base_fs_opts: fs::OpenOptions) -> io::Result<Self> {
810 let Some(filename) = opts.filename else {
811 return Err(io::Error::new(
812 io::ErrorKind::InvalidInput,
813 "Filename required",
814 ));
815 };
816
817 let mut file_opts = base_fs_opts;
818 file_opts.read(true).write(opts.writable);
819 #[cfg(not(target_os = "macos"))]
820 if opts.direct {
821 file_opts.custom_flags(
822 #[cfg(unix)]
823 libc::O_DIRECT,
824 #[cfg(windows)]
825 windows_sys::Win32::Storage::FileSystem::FILE_FLAG_NO_BUFFERING,
826 );
827 }
828
829 let filename_owned = filename.to_owned();
830 let file = file_opts.open(filename)?;
831
832 #[cfg(target_os = "macos")]
833 if opts.direct {
834 while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) })
836 .err_context(|| "Failed to disable host cache")?;
837 }
838
839 Self::new(
840 file,
841 Some(filename_owned),
842 opts.direct,
843 #[cfg(unix)]
844 opts.writable,
845 #[cfg(target_os = "macos")]
846 opts.relaxed_sync,
847 )
848 }
849
850 #[cfg(unix)]
866 fn map_os_err(err: io::Error) -> io::Error {
867 let Some(raw) = err.raw_os_error() else {
868 return err;
869 };
870
871 let has_kind = err.kind();
872 let want_kind = match raw {
873 #[allow(unreachable_patterns)] libc::ENOTSUP | libc::EOPNOTSUPP | libc::ENODEV | libc::ENXIO | libc::ENOTTY => {
875 io::ErrorKind::Unsupported
876 }
877 _ => has_kind,
878 };
879
880 if has_kind != want_kind {
881 io::Error::new(want_kind, err)
882 } else {
883 err
884 }
885 }
886
887 #[cfg(not(unix))]
891 fn map_os_err(err: io::Error) -> io::Error {
892 err
893 }
894
895 fn try_discard_by_truncate(&self, offset: u64, length: u64) -> io::Result<bool> {
903 #[allow(clippy::readonly_write_lock)]
905 let file = self.file.write().unwrap();
906
907 let size = self.size.load(Ordering::Relaxed);
908 if offset >= size {
909 return Ok(true);
911 }
912
913 let end = offset.checked_add(length).unwrap_or(size);
916 if end < size {
917 return Ok(false);
918 }
919
920 file.set_len(offset)?;
921 file.set_len(size)?;
922 Ok(true)
923 }
924
925 async fn discard_to_zero(&self, offset: u64, length: u64) -> io::Result<()> {
927 if self.try_discard_by_truncate(offset, length)? {
928 return Ok(());
929 }
930
931 if self.discard_unsupported.load(Ordering::Relaxed) {
932 Err(io::ErrorKind::Unsupported.into())
933 } else if let Err(err) = self.discard_to_zero_os_specific(offset, length).await {
934 if err.kind() == io::ErrorKind::Unsupported {
935 self.discard_unsupported.store(true, Ordering::Relaxed);
936 }
937 Err(err)
938 } else {
939 Ok(())
940 }
941 }
942
943 #[cfg(target_os = "linux")]
945 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
946 let offset: libc::off_t = offset
947 .try_into()
948 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
949 let length: libc::off_t = length
950 .try_into()
951 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
952
953 let file = self.file.read().unwrap();
954 while_eintr(|| unsafe {
956 libc::fallocate(
957 file.as_raw_fd(),
958 libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
959 offset,
960 length,
961 )
962 })
963 .map_err(Self::map_os_err)?;
964
965 Ok(())
966 }
967
968 #[cfg(windows)]
970 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
971 let offset: i64 = offset
972 .try_into()
973 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
974 let length: i64 = length
975 .try_into()
976 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
977
978 let end = offset.saturating_add(length);
979 let params = FILE_ZERO_DATA_INFORMATION {
980 FileOffset: offset,
981 BeyondFinalZero: end,
982 };
983 let mut _returned = 0;
984 let file = self.file.read().unwrap();
985 let ret = unsafe {
989 DeviceIoControl(
990 file.as_raw_handle(),
991 FSCTL_SET_ZERO_DATA,
992 (¶ms as *const FILE_ZERO_DATA_INFORMATION).cast::<std::ffi::c_void>(),
993 size_of_val(¶ms) as u32,
994 std::ptr::null_mut(),
995 0,
996 &mut _returned,
997 std::ptr::null_mut(),
998 )
999 };
1000 if ret == 0 {
1001 return Err(Self::map_os_err(io::Error::last_os_error()));
1002 }
1003
1004 Ok(())
1005 }
1006
1007 #[cfg(target_os = "macos")]
1009 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1010 let offset: libc::off_t = offset
1011 .try_into()
1012 .map_err(|e| io::Error::other(format!("Discard/write-zeroes offset error: {e}")))?;
1013 let length: libc::off_t = length
1014 .try_into()
1015 .map_err(|e| io::Error::other(format!("Discard/write-zeroes length error: {e}")))?;
1016
1017 let params = libc::fpunchhole_t {
1018 fp_flags: 0,
1019 reserved: 0,
1020 fp_offset: offset,
1021 fp_length: length,
1022 };
1023 let file = self.file.read().unwrap();
1024 while_eintr(|| unsafe { libc::fcntl(file.as_raw_fd(), libc::F_PUNCHHOLE, ¶ms) })
1026 .map_err(Self::map_os_err)?;
1027
1028 Ok(())
1029 }
1030
1031 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1033 async fn discard_to_zero_os_specific(&self, offset: u64, length: u64) -> io::Result<()> {
1034 Err(io::ErrorKind::Unsupported.into())
1035 }
1036}
1037
1038impl Display for File {
1039 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1040 if let Some(filename) = self.filename.as_ref() {
1041 write!(f, "file:{filename:?}")
1042 } else {
1043 write!(f, "file:<unknown path>")
1044 }
1045 }
1046}
1047
1048fn get_file_size(file: &fs::File) -> io::Result<u64> {
1053 #[allow(clippy::bind_instead_of_map)]
1054 file.metadata().and_then(|m| {
1055 #[cfg(unix)]
1056 if m.file_type().is_block_device() || m.file_type().is_char_device() {
1057 return get_device_size(file);
1058 }
1059 Ok(m.len())
1060 })
1061}
1062
1063cfg_if! {
1064 if #[cfg(target_os = "linux")] {
1065 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1067 let mut size = 0;
1068 unsafe { ioctl::blkgetsize64(file.as_raw_fd(), &mut size) }?;
1069 Ok(size)
1070 }
1071 } else if #[cfg(target_os = "macos")] {
1072 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1074 let mut block_size = 0;
1075 unsafe { ioctl::dkiocgetblocksize(file.as_raw_fd(), &mut block_size) }?;
1076 let mut block_count = 0;
1077 unsafe { ioctl::dkiocgetblockcount(file.as_raw_fd(), &mut block_count) }?;
1078 Ok(u64::from(block_size) * block_count)
1079 }
1080 } else if #[cfg(target_os = "freebsd")] {
1081 fn get_device_size(file: &fs::File) -> io::Result<u64> {
1083 let mut size = 0;
1084 unsafe { ioctl::diocgmediasize(file.as_raw_fd(), &mut size) }?;
1085 Ok(size as u64)
1086 }
1087 } else if #[cfg(unix)] {
1088 fn get_device_size(_file: &fs::File) -> io::Result<u64> {
1090 Err(io::ErrorKind::Unsupported.into())
1091 }
1092 }
1093}
1094
1095mod ioctl {
1097 #[cfg(unix)]
1098 use nix::ioctl_read;
1099 #[cfg(target_os = "linux")]
1100 use nix::ioctl_read_bad;
1101
1102 #[cfg(target_os = "linux")]
1105 ioctl_read!(blkgetsize64, 0x12, 114, u64);
1106
1107 #[cfg(target_os = "linux")]
1108 ioctl_read_bad!(blksszget, libc::BLKSSZGET, libc::c_int);
1109
1110 #[cfg(target_os = "macos")]
1113 ioctl_read!(dkiocgetblocksize, 'd', 24, u32);
1114
1115 #[cfg(target_os = "macos")]
1116 ioctl_read!(dkiocgetblockcount, 'd', 25, u64);
1117
1118 #[cfg(target_os = "freebsd")]
1121 ioctl_read!(diocgsectorsize, 'd', 128, libc::c_uint);
1122
1123 #[cfg(target_os = "freebsd")]
1124 ioctl_read!(diocgmediasize, 'd', 129, libc::off_t);
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 use super::File;
1130 use crate::{Storage, StorageExt, StorageOpenOptions};
1131 #[cfg(unix)]
1132 use std::time::{SystemTime, UNIX_EPOCH};
1133 use std::{fs, io};
1134
1135 #[cfg(windows)]
1136 #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1137 async fn zero_data_preserves_the_byte_at_the_exclusive_end() {
1138 let path = std::env::temp_dir().join(format!(
1139 "imago-zero-data-boundary-{}-{}",
1140 std::process::id(),
1141 std::time::SystemTime::now()
1142 .duration_since(std::time::UNIX_EPOCH)
1143 .unwrap()
1144 .as_nanos()
1145 ));
1146 fs::write(&path, vec![0x5a; 12288]).unwrap();
1147 let host = fs::OpenOptions::new()
1148 .read(true)
1149 .write(true)
1150 .open(&path)
1151 .unwrap();
1152 let storage = File::try_from(host).unwrap();
1153 let zero_result = storage.discard_to_zero_os_specific(4096, 4096).await;
1154 drop(storage);
1155
1156 let contents = fs::read(&path);
1157 let _ = fs::remove_file(&path);
1158 zero_result.unwrap();
1159 let contents = contents.unwrap();
1160 assert_eq!(contents[4095], 0x5a);
1161 assert!(contents[4096..8192].iter().all(|byte| *byte == 0));
1162 assert_eq!(contents[8192], 0x5a);
1163 }
1164
1165 #[cfg(unix)]
1166 #[test]
1167 fn buffered_writable_file_is_not_modified_during_construction() {
1168 let unique = SystemTime::now()
1169 .duration_since(UNIX_EPOCH)
1170 .unwrap()
1171 .as_nanos();
1172 let path = std::env::temp_dir().join(format!(
1173 "imago-buffered-probe-{}-{unique}",
1174 std::process::id()
1175 ));
1176 let file = fs::OpenOptions::new()
1177 .read(true)
1178 .write(true)
1179 .create_new(true)
1180 .open(&path)
1181 .unwrap();
1182
1183 let storage = File::try_from(file).unwrap();
1184 let alignments = (
1185 storage.req_align,
1186 storage.mem_align,
1187 storage.zero_align,
1188 storage.discard_align,
1189 );
1190 let len = fs::metadata(&path).unwrap().len();
1191 let expected_fs_align = {
1192 let probe = fs::File::open(&path).unwrap();
1193 super::File::get_fs_zero_discard_align(&probe)
1194 };
1195 drop(storage);
1196 fs::remove_file(path).unwrap();
1197
1198 assert_eq!(alignments, (1, 1, expected_fs_align.0, expected_fs_align.1));
1199 assert_eq!(len, 0);
1200 }
1201
1202 struct TempPath(std::path::PathBuf);
1203
1204 impl Drop for TempPath {
1205 fn drop(&mut self) {
1206 let _ = std::fs::remove_file(&self.0);
1207 }
1208 }
1209
1210 #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
1211 async fn tail_discard_keeps_file_length() -> io::Result<()> {
1212 let path =
1213 std::env::temp_dir().join(format!("imago-tail-discard-{}.raw", std::process::id()));
1214 let _temp_path = TempPath(path.clone());
1215 std::fs::write(&path, vec![0xabu8; 8192])?;
1216
1217 let file = File::open(StorageOpenOptions::new().write(true).filename(&path)).await?;
1218 file.discard(4096, 4096).await?;
1219 assert_eq!(std::fs::metadata(&path)?.len(), 8192);
1220
1221 let mut prefix = vec![0u8; 4096];
1222 file.read(&mut prefix, 0).await?;
1223 assert_eq!(prefix, vec![0xabu8; 4096]);
1224
1225 let mut tail = vec![0xffu8; 4096];
1226 file.read(&mut tail, 4096).await?;
1227 assert!(tail.iter().all(|&byte| byte == 0));
1228
1229 io::Result::Ok(())
1230 }
1231}