1use crate::{NativeStorageCapabilityError, probe_native_storage_capabilities};
4use std::path::Path;
5use thiserror::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum NativeSparseAccelerationEvidence {
10 Available {
12 logical_bytes: u64,
14 allocated_bytes: u64,
16 },
17 Unavailable {
19 platform_error_code: i32,
21 },
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum NativeBlockCloneAccelerationEvidence {
27 Available {
29 cloned_bytes: u64,
31 copy_on_write_isolation: bool,
33 },
34 Unavailable {
36 platform_error_code: i32,
38 },
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub struct NativeStorageAccelerationEvidence {
44 pub sparse: NativeSparseAccelerationEvidence,
46 pub block_clone: NativeBlockCloneAccelerationEvidence,
48}
49
50#[derive(Debug, Error)]
52pub enum NativeStorageAccelerationError {
53 #[error(transparent)]
55 Capability(#[from] NativeStorageCapabilityError),
56 #[error("native storage acceleration probing is unavailable on this target")]
58 UnsupportedTarget,
59 #[error("native storage acceleration I/O failed: {0}")]
61 Io(#[from] std::io::Error),
62 #[error("native storage acceleration probe failed: {0}")]
64 Platform(String),
65 #[error("native storage acceleration evidence is invalid: {0}")]
67 Invalid(&'static str),
68}
69
70pub fn probe_native_storage_accelerations(
81 root: &Path,
82) -> Result<NativeStorageAccelerationEvidence, NativeStorageAccelerationError> {
83 let capabilities = probe_native_storage_capabilities(root)?;
84 probe_platform(root, capabilities.allocation_unit_bytes)
85}
86
87#[cfg(windows)]
88struct ProbeDirectory(std::path::PathBuf);
89
90#[cfg(windows)]
91impl ProbeDirectory {
92 fn create(root: &Path) -> Result<Self, std::io::Error> {
93 use std::time::{SystemTime, UNIX_EPOCH};
94
95 let nonce = SystemTime::now()
96 .duration_since(UNIX_EPOCH)
97 .map_err(std::io::Error::other)?
98 .as_nanos();
99 let path = root.join(format!(
100 ".acyclic-fs-storage-acceleration-{}-{nonce}",
101 std::process::id()
102 ));
103 std::fs::create_dir(&path)?;
104 Ok(Self(path))
105 }
106
107 fn path(&self) -> &Path {
108 &self.0
109 }
110
111 fn remove(self) -> Result<(), std::io::Error> {
112 std::fs::remove_dir_all(&self.0)?;
113 std::mem::forget(self);
114 Ok(())
115 }
116}
117
118#[cfg(windows)]
119impl Drop for ProbeDirectory {
120 fn drop(&mut self) {
121 let _ = std::fs::remove_dir_all(&self.0);
122 }
123}
124
125#[cfg(windows)]
126#[allow(unsafe_code)]
127fn file_allocation_bytes(file: &std::fs::File) -> Result<u64, NativeStorageAccelerationError> {
128 use std::mem::size_of;
129 use std::os::windows::io::AsRawHandle;
130 use windows::Win32::Foundation::HANDLE;
131 use windows::Win32::Storage::FileSystem::{
132 FILE_STANDARD_INFO, FileStandardInfo, GetFileInformationByHandleEx,
133 };
134
135 let mut information = FILE_STANDARD_INFO::default();
136 unsafe {
139 GetFileInformationByHandleEx(
140 HANDLE(file.as_raw_handle()),
141 FileStandardInfo,
142 (&raw mut information).cast(),
143 u32::try_from(size_of::<FILE_STANDARD_INFO>())
144 .map_err(|_| NativeStorageAccelerationError::Invalid("file info size overflow"))?,
145 )
146 .map_err(|error| NativeStorageAccelerationError::Platform(error.to_string()))?;
147 }
148 u64::try_from(information.AllocationSize)
149 .map_err(|_| NativeStorageAccelerationError::Invalid("negative allocation size"))
150}
151
152#[cfg(windows)]
153#[allow(unsafe_code)]
154fn mark_sparse(file: &std::fs::File) -> windows::core::Result<()> {
155 use std::os::windows::io::AsRawHandle;
156 use windows::Win32::Foundation::HANDLE;
157 use windows::Win32::System::IO::DeviceIoControl;
158 use windows::Win32::System::Ioctl::FSCTL_SET_SPARSE;
159
160 unsafe {
163 DeviceIoControl(
164 HANDLE(file.as_raw_handle()),
165 FSCTL_SET_SPARSE,
166 None,
167 0,
168 None,
169 0,
170 None,
171 None,
172 )
173 }
174}
175
176#[cfg(windows)]
177fn unavailable_platform_code(error: &windows::core::Error) -> Option<i32> {
178 use windows::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_NOT_SUPPORTED};
179 use windows::core::HRESULT;
180
181 let code = error.code();
182 if code == HRESULT::from_win32(ERROR_INVALID_FUNCTION.0)
183 || code == HRESULT::from_win32(ERROR_NOT_SUPPORTED.0)
184 {
185 Some(code.0)
186 } else {
187 None
188 }
189}
190
191#[cfg(windows)]
192fn unexpected_platform_error(
193 operation: &str,
194 error: &windows::core::Error,
195) -> NativeStorageAccelerationError {
196 NativeStorageAccelerationError::Platform(format!(
197 "{operation} returned HRESULT {}: {error}",
198 error.code().0
199 ))
200}
201
202#[cfg(windows)]
203fn sparse_probe(
204 directory: &Path,
205 allocation_unit_bytes: u64,
206) -> Result<NativeSparseAccelerationEvidence, NativeStorageAccelerationError> {
207 use std::io::{Read, Seek, SeekFrom, Write};
208
209 let logical_bytes = allocation_unit_bytes
210 .checked_mul(256)
211 .ok_or(NativeStorageAccelerationError::Invalid(
212 "sparse logical length overflow",
213 ))?
214 .max(1024 * 1024);
215 let path = directory.join("sparse.bin");
216 let mut file = std::fs::OpenOptions::new()
217 .create_new(true)
218 .read(true)
219 .write(true)
220 .open(path)?;
221 if let Err(error) = mark_sparse(&file) {
222 return unavailable_platform_code(&error).map_or_else(
223 || Err(unexpected_platform_error("sparse allocation", &error)),
224 |platform_error_code| {
225 Ok(NativeSparseAccelerationEvidence::Unavailable {
226 platform_error_code,
227 })
228 },
229 );
230 }
231 file.set_len(logical_bytes)?;
232 let edge = [0xA5_u8; 4_096];
233 file.write_all(&edge)?;
234 file.seek(SeekFrom::Start(logical_bytes - 4_096))?;
235 file.write_all(&edge)?;
236 file.sync_all()?;
237 let allocated_bytes = file_allocation_bytes(&file)?;
238 if allocated_bytes == 0 || allocated_bytes >= logical_bytes {
239 return Err(NativeStorageAccelerationError::Invalid(
240 "sparse file did not retain a physical hole",
241 ));
242 }
243 file.seek(SeekFrom::Start(logical_bytes / 2))?;
244 let mut hole = [1_u8; 4_096];
245 file.read_exact(&mut hole)?;
246 if hole.iter().any(|byte| *byte != 0) {
247 return Err(NativeStorageAccelerationError::Invalid(
248 "sparse hole did not read as logical zero bytes",
249 ));
250 }
251 Ok(NativeSparseAccelerationEvidence::Available {
252 logical_bytes,
253 allocated_bytes,
254 })
255}
256
257#[cfg(windows)]
258#[allow(unsafe_code)]
259fn block_clone_probe(
260 directory: &Path,
261 allocation_unit_bytes: u64,
262) -> Result<NativeBlockCloneAccelerationEvidence, NativeStorageAccelerationError> {
263 use std::io::{Read, Seek, SeekFrom, Write};
264 use std::mem::size_of;
265 use std::os::windows::io::AsRawHandle;
266 use windows::Win32::Foundation::HANDLE;
267 use windows::Win32::System::IO::DeviceIoControl;
268 use windows::Win32::System::Ioctl::{DUPLICATE_EXTENTS_DATA, FSCTL_DUPLICATE_EXTENTS_TO_FILE};
269
270 let clone_bytes = allocation_unit_bytes
271 .checked_mul(256)
272 .ok_or(NativeStorageAccelerationError::Invalid(
273 "block clone byte count overflow",
274 ))?
275 .max(1024 * 1024);
276 let length = usize::try_from(clone_bytes).map_err(|_| {
277 NativeStorageAccelerationError::Invalid("block clone byte count is not addressable")
278 })?;
279 let mut expected = Vec::new();
280 expected.try_reserve_exact(length).map_err(|_| {
281 NativeStorageAccelerationError::Invalid("block clone source allocation failed")
282 })?;
283 expected.extend((0..clone_bytes).map(|offset| (offset.wrapping_mul(131) % 251) as u8));
284 let source_path = directory.join("clone-source.bin");
285 let target_path = directory.join("clone-target.bin");
286 let mut source = std::fs::OpenOptions::new()
287 .create_new(true)
288 .read(true)
289 .write(true)
290 .open(source_path)?;
291 source.write_all(&expected)?;
292 source.sync_all()?;
293 let mut target = std::fs::OpenOptions::new()
294 .create_new(true)
295 .read(true)
296 .write(true)
297 .open(target_path)?;
298 target.set_len(clone_bytes)?;
299 target.sync_all()?;
300 let duplicate = DUPLICATE_EXTENTS_DATA {
301 FileHandle: HANDLE(source.as_raw_handle()),
302 SourceFileOffset: 0,
303 TargetFileOffset: 0,
304 ByteCount: i64::try_from(clone_bytes).map_err(|_| {
305 NativeStorageAccelerationError::Invalid("block clone byte count exceeds i64")
306 })?,
307 };
308 let clone_result = unsafe {
311 DeviceIoControl(
312 HANDLE(target.as_raw_handle()),
313 FSCTL_DUPLICATE_EXTENTS_TO_FILE,
314 Some((&raw const duplicate).cast()),
315 u32::try_from(size_of::<DUPLICATE_EXTENTS_DATA>()).map_err(|_| {
316 NativeStorageAccelerationError::Invalid("block clone control size overflow")
317 })?,
318 None,
319 0,
320 None,
321 None,
322 )
323 };
324 if let Err(error) = clone_result {
325 return unavailable_platform_code(&error).map_or_else(
326 || Err(unexpected_platform_error("block cloning", &error)),
327 |platform_error_code| {
328 Ok(NativeBlockCloneAccelerationEvidence::Unavailable {
329 platform_error_code,
330 })
331 },
332 );
333 }
334 target.sync_all()?;
335 let mut observed = Vec::new();
336 observed.try_reserve_exact(length).map_err(|_| {
337 NativeStorageAccelerationError::Invalid("block clone result allocation failed")
338 })?;
339 target.seek(SeekFrom::Start(0))?;
340 target.read_to_end(&mut observed)?;
341 if observed != expected {
342 return Err(NativeStorageAccelerationError::Invalid(
343 "block clone bytes diverged",
344 ));
345 }
346 source.seek(SeekFrom::Start(0))?;
347 source.write_all(&[0x5A_u8; 4_096])?;
348 source.sync_all()?;
349 target.seek(SeekFrom::Start(0))?;
350 let mut isolated = [0_u8; 4_096];
351 target.read_exact(&mut isolated)?;
352 if !expected.starts_with(&isolated) {
353 return Err(NativeStorageAccelerationError::Invalid(
354 "block clone did not preserve copy-on-write isolation",
355 ));
356 }
357 Ok(NativeBlockCloneAccelerationEvidence::Available {
358 cloned_bytes: clone_bytes,
359 copy_on_write_isolation: true,
360 })
361}
362
363#[cfg(windows)]
364fn probe_platform(
365 root: &Path,
366 allocation_unit_bytes: u64,
367) -> Result<NativeStorageAccelerationEvidence, NativeStorageAccelerationError> {
368 let directory = ProbeDirectory::create(root)?;
369 let sparse = sparse_probe(directory.path(), allocation_unit_bytes)?;
370 let block_clone = block_clone_probe(directory.path(), allocation_unit_bytes)?;
371 directory.remove()?;
372 Ok(NativeStorageAccelerationEvidence {
373 sparse,
374 block_clone,
375 })
376}
377
378#[cfg(target_os = "macos")]
379#[path = "storage_accelerations_macos.rs"]
380mod macos;
381
382#[cfg(target_os = "macos")]
383fn probe_platform(
384 root: &Path,
385 allocation_unit_bytes: u64,
386) -> Result<NativeStorageAccelerationEvidence, NativeStorageAccelerationError> {
387 macos::probe(root, allocation_unit_bytes)
388}
389
390#[cfg(target_os = "linux")]
391#[path = "storage_accelerations_linux.rs"]
392mod linux;
393
394#[cfg(target_os = "linux")]
395fn probe_platform(
396 root: &Path,
397 allocation_unit_bytes: u64,
398) -> Result<NativeStorageAccelerationEvidence, NativeStorageAccelerationError> {
399 linux::probe(root, allocation_unit_bytes)
400}
401
402#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
403fn probe_platform(
404 _root: &Path,
405 _allocation_unit_bytes: u64,
406) -> Result<NativeStorageAccelerationEvidence, NativeStorageAccelerationError> {
407 Err(NativeStorageAccelerationError::UnsupportedTarget)
408}
409
410#[cfg(all(test, windows))]
411mod tests {
412 use super::*;
413
414 #[cfg(windows)]
415 #[test]
416 fn windows_accelerations_execute_or_return_exact_unsupported_evidence()
417 -> Result<(), Box<dyn std::error::Error>> {
418 let directory = tempfile::tempdir()?;
419 let evidence = probe_native_storage_accelerations(directory.path())?;
420 match evidence.sparse {
421 NativeSparseAccelerationEvidence::Available {
422 logical_bytes,
423 allocated_bytes,
424 } => assert!(allocated_bytes < logical_bytes),
425 NativeSparseAccelerationEvidence::Unavailable {
426 platform_error_code,
427 } => assert_ne!(platform_error_code, 0),
428 }
429 match evidence.block_clone {
430 NativeBlockCloneAccelerationEvidence::Available {
431 cloned_bytes,
432 copy_on_write_isolation,
433 } => {
434 assert!(cloned_bytes > 0);
435 assert!(copy_on_write_isolation);
436 }
437 NativeBlockCloneAccelerationEvidence::Unavailable {
438 platform_error_code,
439 } => assert_ne!(platform_error_code, 0),
440 }
441 Ok(())
442 }
443
444 #[cfg(windows)]
445 #[test]
446 fn only_unambiguous_unsupported_hresult_values_are_classified_unavailable() {
447 use windows::Win32::Foundation::{
448 ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED,
449 };
450 use windows::core::{Error, HRESULT};
451
452 let invalid_function = Error::from_hresult(HRESULT::from_win32(ERROR_INVALID_FUNCTION.0));
453 let not_supported = Error::from_hresult(HRESULT::from_win32(ERROR_NOT_SUPPORTED.0));
454 let invalid_parameter = Error::from_hresult(HRESULT::from_win32(ERROR_INVALID_PARAMETER.0));
455 assert_eq!(
456 unavailable_platform_code(&invalid_function),
457 Some(invalid_function.code().0)
458 );
459 assert_eq!(
460 unavailable_platform_code(¬_supported),
461 Some(not_supported.code().0)
462 );
463 assert_eq!(unavailable_platform_code(&invalid_parameter), None);
464 }
465}