microsandbox_utils/
extent.rs1use std::fs::File;
10use std::io;
11use std::path::Path;
12
13#[cfg(unix)]
14use std::os::unix::io::AsRawFd;
15#[cfg(windows)]
16use std::os::windows::ffi::OsStrExt;
17#[cfg(windows)]
18use std::os::windows::io::AsRawHandle;
19#[cfg(windows)]
20use std::ptr;
21
22#[cfg(windows)]
23use windows_sys::Win32::Foundation::{ERROR_MORE_DATA, GetLastError, HANDLE, NO_ERROR};
24#[cfg(windows)]
25use windows_sys::Win32::Storage::FileSystem::GetCompressedFileSizeW;
26#[cfg(windows)]
27use windows_sys::Win32::System::IO::DeviceIoControl;
28#[cfg(windows)]
29use windows_sys::Win32::System::Ioctl::{
30 FILE_ALLOCATED_RANGE_BUFFER, FSCTL_QUERY_ALLOCATED_RANGES, FSCTL_SET_SPARSE,
31};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ExtentMap {
40 pub len: u64,
42 pub extents: Vec<(u64, u64)>,
44}
45
46impl ExtentMap {
51 pub fn scan(path: &Path) -> io::Result<Option<ExtentMap>> {
58 let file = File::open(path)?;
59 Self::scan_file(&file)
60 }
61
62 pub fn scan_file(file: &File) -> io::Result<Option<ExtentMap>> {
64 let len = file.metadata()?.len();
65 if len == 0 {
66 return Ok(Some(ExtentMap {
67 len,
68 extents: Vec::new(),
69 }));
70 }
71 scan_impl(file, len)
72 }
73
74 pub fn data_bytes(&self) -> u64 {
76 self.extents.iter().map(|(_, len)| len).sum()
77 }
78
79 pub fn has_holes(&self) -> bool {
81 self.data_bytes() < self.len
82 }
83}
84
85pub fn allocated_file_bytes(path: &Path) -> io::Result<u64> {
91 #[cfg(unix)]
92 {
93 use std::os::unix::fs::MetadataExt;
94
95 Ok(std::fs::metadata(path)?.blocks().saturating_mul(512))
96 }
97 #[cfg(windows)]
98 {
99 let mut high = 0u32;
100 let path_wide = path
101 .as_os_str()
102 .encode_wide()
103 .chain(std::iter::once(0))
104 .collect::<Vec<_>>();
105 let low = unsafe { GetCompressedFileSizeW(path_wide.as_ptr(), &mut high) };
106 if low == u32::MAX {
107 let error = unsafe { GetLastError() };
108 if error != NO_ERROR {
109 return Err(io::Error::from_raw_os_error(error as i32));
110 }
111 }
112 Ok((u64::from(high) << 32) | u64::from(low))
113 }
114 #[cfg(not(any(unix, windows)))]
115 {
116 let _ = path;
117 Err(io::Error::new(
118 io::ErrorKind::Unsupported,
119 "allocated file size is unsupported on this platform",
120 ))
121 }
122}
123
124#[cfg(windows)]
127pub fn mark_sparse(file: &File) -> io::Result<()> {
128 let mut bytes_returned = 0;
129 let ok = unsafe {
130 DeviceIoControl(
131 file.as_raw_handle() as HANDLE,
132 FSCTL_SET_SPARSE,
133 ptr::null(),
134 0,
135 ptr::null_mut(),
136 0,
137 &mut bytes_returned,
138 ptr::null_mut(),
139 )
140 };
141 if ok == 0 {
142 return Err(io::Error::last_os_error());
143 }
144 Ok(())
145}
146
147#[cfg(unix)]
149pub fn mark_sparse(_file: &File) -> io::Result<()> {
150 Ok(())
151}
152
153#[cfg(target_os = "macos")]
156pub fn punch_hole_aligned(file: &File, offset: u64, len: u64) -> io::Result<()> {
157 let block = allocation_block_size(file)?;
158 let start = offset.div_ceil(block).saturating_mul(block);
159 let end = (offset.saturating_add(len) / block).saturating_mul(block);
160 if end <= start {
161 return Ok(());
162 }
163 let args = libc::fpunchhole_t {
164 fp_flags: 0,
165 reserved: 0,
166 fp_offset: start as libc::off_t,
167 fp_length: (end - start) as libc::off_t,
168 };
169 let rc = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_PUNCHHOLE, &args) };
170 if rc != 0 {
171 return Err(io::Error::last_os_error());
172 }
173 Ok(())
174}
175
176#[cfg(not(target_os = "macos"))]
178pub fn punch_hole_aligned(_file: &File, _offset: u64, _len: u64) -> io::Result<()> {
179 Ok(())
180}
181
182#[cfg(unix)]
187fn scan_impl(file: &File, len: u64) -> io::Result<Option<ExtentMap>> {
188 let fd = file.as_raw_fd();
189
190 let mut extents: Vec<(u64, u64)> = Vec::new();
191 let mut off: i64 = 0;
192 while (off as u64) < len {
193 let data_start = unsafe { libc::lseek(fd, off, libc::SEEK_DATA) };
194 if data_start < 0 {
195 let err = io::Error::last_os_error();
196 match err.raw_os_error() {
197 Some(libc::ENXIO) => break,
199 Some(libc::EINVAL) | Some(libc::ENOTSUP) => return Ok(None),
201 #[cfg(not(target_os = "linux"))]
202 Some(libc::EOPNOTSUPP) => return Ok(None),
203 _ => return Err(err),
204 }
205 }
206 let data_end = unsafe { libc::lseek(fd, data_start, libc::SEEK_HOLE) };
207 if data_end < 0 {
208 return Err(io::Error::last_os_error());
209 }
210 let data_end = (data_end as u64).min(len);
211 let data_start = data_start as u64;
212 if data_end <= data_start {
213 break;
214 }
215 extents.push((data_start, data_end - data_start));
216 off = data_end as i64;
217 }
218
219 Ok(Some(ExtentMap { len, extents }))
220}
221
222#[cfg(windows)]
223fn scan_impl(file: &File, len: u64) -> io::Result<Option<ExtentMap>> {
224 const BATCH: usize = 64;
226
227 let handle = file.as_raw_handle() as HANDLE;
228 let mut extents: Vec<(u64, u64)> = Vec::new();
229 let mut next_offset: u64 = 0;
230
231 while next_offset < len {
232 let query = FILE_ALLOCATED_RANGE_BUFFER {
233 FileOffset: next_offset as i64,
234 Length: (len - next_offset) as i64,
235 };
236 let mut out = [FILE_ALLOCATED_RANGE_BUFFER {
237 FileOffset: 0,
238 Length: 0,
239 }; BATCH];
240 let mut bytes_returned: u32 = 0;
241 let ok = unsafe {
242 DeviceIoControl(
243 handle,
244 FSCTL_QUERY_ALLOCATED_RANGES,
245 &query as *const _ as *const _,
246 size_of::<FILE_ALLOCATED_RANGE_BUFFER>() as u32,
247 out.as_mut_ptr() as *mut _,
248 (size_of::<FILE_ALLOCATED_RANGE_BUFFER>() * BATCH) as u32,
249 &mut bytes_returned,
250 ptr::null_mut(),
251 )
252 };
253 let more = if ok == 0 {
254 let err = io::Error::last_os_error();
255 if err.raw_os_error() == Some(ERROR_MORE_DATA as i32) {
256 true
257 } else {
258 return Ok(None);
260 }
261 } else {
262 false
263 };
264
265 let count = bytes_returned as usize / size_of::<FILE_ALLOCATED_RANGE_BUFFER>();
266 if count == 0 {
267 break;
268 }
269 for range in &out[..count] {
270 let start = range.FileOffset as u64;
271 let end = (start + range.Length as u64).min(len);
272 if end > start {
273 extents.push((start, end - start));
274 }
275 }
276 let (last_off, last_len) = extents[extents.len() - 1];
277 next_offset = last_off + last_len;
278 if !more {
279 break;
280 }
281 }
282
283 Ok(Some(ExtentMap { len, extents }))
284}
285
286#[cfg(target_os = "macos")]
288fn allocation_block_size(file: &File) -> io::Result<u64> {
289 let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
290 let rc = unsafe { libc::fstatfs(file.as_raw_fd(), &mut stat) };
291 if rc != 0 {
292 return Err(io::Error::last_os_error());
293 }
294 Ok((stat.f_bsize as u64).max(512))
295}
296
297#[cfg(test)]
302mod tests {
303 use std::io::{Seek, SeekFrom, Write};
304
305 use super::*;
306
307 #[test]
308 fn dense_file_scans_as_single_extent_or_unsupported() {
309 let dir = tempfile::tempdir().unwrap();
310 let path = dir.path().join("dense.bin");
311 std::fs::write(&path, vec![0xAB; 8192]).unwrap();
312
313 match ExtentMap::scan(&path).unwrap() {
314 None => {} Some(map) => {
316 assert_eq!(map.len, 8192);
317 assert_eq!(map.data_bytes(), 8192);
318 assert!(!map.has_holes());
319 }
320 }
321 }
322
323 #[test]
324 fn empty_file_scans_as_empty_map() {
325 let dir = tempfile::tempdir().unwrap();
326 let path = dir.path().join("empty.bin");
327 std::fs::write(&path, b"").unwrap();
328
329 let map = ExtentMap::scan(&path).unwrap().unwrap();
330 assert_eq!(map.len, 0);
331 assert!(map.extents.is_empty());
332 assert!(!map.has_holes());
333 }
334
335 #[test]
336 fn sparse_file_scan_covers_all_data() {
337 let dir = tempfile::tempdir().unwrap();
338 let path = dir.path().join("sparse.bin");
339 let len: u64 = 8 * 1024 * 1024;
340 let mut f = std::fs::OpenOptions::new()
341 .read(true)
342 .write(true)
343 .create(true)
344 .truncate(true)
345 .open(&path)
346 .unwrap();
347 mark_sparse(&f).unwrap();
349 f.set_len(len).unwrap();
350 f.seek(SeekFrom::Start(0)).unwrap();
351 f.write_all(&[0x11; 4096]).unwrap();
352 f.seek(SeekFrom::Start(4 * 1024 * 1024)).unwrap();
353 f.write_all(&[0x22; 4096]).unwrap();
354 f.sync_all().unwrap();
355 punch_hole_aligned(&f, 4096, 4 * 1024 * 1024 - 4096).unwrap();
356 punch_hole_aligned(&f, 4 * 1024 * 1024 + 4096, len - (4 * 1024 * 1024 + 4096)).unwrap();
357 drop(f);
358
359 let Some(map) = ExtentMap::scan(&path).unwrap() else {
360 eprintln!("filesystem can't enumerate extents; scan not exercised");
361 return;
362 };
363 assert_eq!(map.len, len);
364 let covers = |target: u64| {
366 map.extents
367 .iter()
368 .any(|(off, l)| *off <= target && target < off + l)
369 };
370 assert!(covers(0), "extent map misses data at 0: {:?}", map.extents);
371 assert!(
372 covers(4 * 1024 * 1024),
373 "extent map misses data at 4 MiB: {:?}",
374 map.extents
375 );
376 }
377}