Skip to main content

acyclic_fs/native_mount/
storage_accelerations.rs

1//! Live native sparse-allocation and block-clone capability execution.
2
3use crate::{NativeStorageCapabilityError, probe_native_storage_capabilities};
4use std::path::Path;
5use thiserror::Error;
6
7/// Exact result of executing the native sparse-allocation operation.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum NativeSparseAccelerationEvidence {
10    /// Sparse allocation executed and retained an observable physical hole.
11    Available {
12        /// Logical bytes retained by the probe file.
13        logical_bytes: u64,
14        /// Physically allocated bytes retained by the probe file.
15        allocated_bytes: u64,
16    },
17    /// The operating system explicitly rejected sparse allocation as unsupported.
18    Unavailable {
19        /// Exact failing HRESULT represented as its signed 32-bit value.
20        platform_error_code: i32,
21    },
22}
23
24/// Exact result of executing native same-volume block-reference cloning.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum NativeBlockCloneAccelerationEvidence {
27    /// Block cloning executed and retained copy-on-write isolation.
28    Available {
29        /// Bytes cloned by the native operation.
30        cloned_bytes: u64,
31        /// Whether the clone retained its original bytes after source mutation.
32        copy_on_write_isolation: bool,
33    },
34    /// The operating system explicitly rejected block cloning as unsupported.
35    Unavailable {
36        /// Exact failing HRESULT represented as its signed 32-bit value.
37        platform_error_code: i32,
38    },
39}
40
41/// Exact evidence from independent live sparse and block-clone probes.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub struct NativeStorageAccelerationEvidence {
44    /// Sparse-allocation execution result.
45    pub sparse: NativeSparseAccelerationEvidence,
46    /// Same-volume block-clone execution result.
47    pub block_clone: NativeBlockCloneAccelerationEvidence,
48}
49
50/// A live native storage acceleration probe failed or was unavailable.
51#[derive(Debug, Error)]
52pub enum NativeStorageAccelerationError {
53    /// Static capability discovery failed before mutation began.
54    #[error(transparent)]
55    Capability(#[from] NativeStorageCapabilityError),
56    /// This target has no implemented live acceleration probe.
57    #[error("native storage acceleration probing is unavailable on this target")]
58    UnsupportedTarget,
59    /// Probe fixture creation, synchronization, or cleanup failed.
60    #[error("native storage acceleration I/O failed: {0}")]
61    Io(#[from] std::io::Error),
62    /// The operating system rejected a native sparse/clone operation.
63    #[error("native storage acceleration probe failed: {0}")]
64    Platform(String),
65    /// Native success did not preserve the required sparse or COW semantics.
66    #[error("native storage acceleration evidence is invalid: {0}")]
67    Invalid(&'static str),
68}
69
70/// Independently executes sparse allocation and same-volume block cloning
71/// below `root`.
72/// Temporary fixtures are removed before success returns.
73///
74/// # Errors
75///
76/// Returns exact unavailable outcomes only for operating-system errors that
77/// unambiguously mean unsupported. Rejects unsupported targets, every other
78/// native API failure, byte or allocation divergence, COW aliasing,
79/// synchronization failure, or cleanup failure.
80pub 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    // SAFETY: the handle is borrowed from a live file and the output pointer
137    // names one correctly sized, exclusively borrowed structure.
138    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    // SAFETY: the handle is borrowed from a live file. This control accepts no
161    // input or output buffer and is synchronous because no OVERLAPPED exists.
162    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    // SAFETY: both handles remain live; the input pointer names one complete
309    // immutable structure and the synchronous control returns no output bytes.
310    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(&not_supported),
461            Some(not_supported.code().0)
462        );
463        assert_eq!(unavailable_platform_code(&invalid_parameter), None);
464    }
465}