Skip to main content

fsqlite_vfs/
traits.rs

1use std::path::{Path, PathBuf};
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::{Arc, Mutex};
4use std::task::{Context, Poll, Waker};
5
6use fsqlite_error::Result;
7use fsqlite_types::LockLevel;
8use fsqlite_types::cx::Cx;
9use fsqlite_types::flags::{AccessFlags, SyncFlags, VfsOpenFlags};
10
11use crate::shm::ShmRegion;
12
13/// Opaque identity of an already-open filesystem object.
14///
15/// Identities are intended only as opaque comparison keys while the relevant
16/// file handles remain open. Their ordering carries no filesystem meaning;
17/// it exists only for ordered collections. They are not persistent database
18/// identifiers and must not be serialized or compared across machines or
19/// boots.
20#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub struct FileIdentity {
22    kind: FileIdentityKind,
23    namespace: u64,
24    object: [u8; 16],
25}
26
27/// Representation domain for an opaque [`FileIdentity`].
28///
29/// The discriminator is part of equality and ordering so a legacy Windows
30/// 64-bit file index can never compare equal to a 128-bit `FILE_ID_INFO`
31/// value that happens to contain the same bytes.
32#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33enum FileIdentityKind {
34    /// Stable identity for one file generation inside an in-process VFS.
35    Memory,
36    #[cfg(unix)]
37    Unix,
38    #[cfg(windows)]
39    WindowsFileId128,
40    #[cfg(windows)]
41    WindowsFileIndex64,
42}
43
44impl FileIdentity {
45    /// Construct an identity for a file owned by an in-process VFS.
46    ///
47    /// Both components are opaque process-local tokens. They only need to be
48    /// stable while the corresponding VFS/file storage objects are alive; the
49    /// identity is used to coalesce pager coordination gates, never persisted.
50    #[must_use]
51    pub(crate) fn from_memory_parts(namespace: u64, object: u64) -> Self {
52        let mut object_bytes = [0_u8; 16];
53        object_bytes[..8].copy_from_slice(&object.to_ne_bytes());
54        Self {
55            kind: FileIdentityKind::Memory,
56            namespace,
57            object: object_bytes,
58        }
59    }
60
61    /// Read the identity of an independently opened filesystem descriptor.
62    ///
63    /// On Unix this uses descriptor metadata (`st_dev`, `st_ino`). On Windows
64    /// it uses the volume serial number and full 128-bit file identifier
65    /// associated with the open handle. A later rename or pathname replacement
66    /// therefore does not change the result. Platforms without a stable
67    /// descriptor identity exposed by this crate return `Ok(None)`.
68    #[cfg(not(target_arch = "wasm32"))]
69    pub fn from_file(file: &std::fs::File) -> std::io::Result<Option<Self>> {
70        #[cfg(unix)]
71        {
72            use std::os::unix::fs::MetadataExt as _;
73
74            let metadata = file.metadata()?;
75            Ok(Some(Self::from_unix_parts(metadata.dev(), metadata.ino())))
76        }
77
78        #[cfg(windows)]
79        {
80            use std::os::windows::io::AsRawHandle as _;
81
82            let handle = file.as_raw_handle();
83            let file_id_result = query_windows_file_id(handle);
84            Self::from_windows_query_result(file_id_result, || {
85                query_windows_legacy_file_index(handle)
86            })
87        }
88
89        #[cfg(not(any(unix, windows)))]
90        {
91            let _ = file;
92            Ok(None)
93        }
94    }
95
96    #[cfg(unix)]
97    pub(crate) fn from_unix_parts(device: u64, inode: u64) -> Self {
98        let mut object = [0_u8; 16];
99        object[..8].copy_from_slice(&inode.to_be_bytes());
100        Self {
101            kind: FileIdentityKind::Unix,
102            namespace: device,
103            object,
104        }
105    }
106
107    #[cfg(windows)]
108    fn from_windows_parts(volume_serial_number: u64, file_id: [u8; 16]) -> Option<Self> {
109        // MS-FSCC 2.1.10 reserves all-zero for filesystems without a
110        // 128-bit file ID and all-ones for files whose unique ID cannot be
111        // established. Both values MUST be ignored, so neither can safely
112        // participate in an expected-identity comparison.
113        if file_id.iter().all(|byte| *byte == 0) || file_id.iter().all(|byte| *byte == u8::MAX) {
114            return None;
115        }
116        Some(Self {
117            kind: FileIdentityKind::WindowsFileId128,
118            namespace: volume_serial_number,
119            object: file_id,
120        })
121    }
122
123    #[cfg(windows)]
124    fn from_windows_legacy_parts(
125        volume_serial_number: u32,
126        file_index_high: u32,
127        file_index_low: u32,
128    ) -> Self {
129        let file_index = (u64::from(file_index_high) << 32) | u64::from(file_index_low);
130        let mut object = [0_u8; 16];
131        object[..8].copy_from_slice(&file_index.to_be_bytes());
132        Self {
133            kind: FileIdentityKind::WindowsFileIndex64,
134            namespace: u64::from(volume_serial_number),
135            object,
136        }
137    }
138
139    #[cfg(windows)]
140    fn from_windows_query_result<F>(
141        file_id_result: std::io::Result<(u64, [u8; 16])>,
142        legacy_query: F,
143    ) -> std::io::Result<Option<Self>>
144    where
145        F: FnOnce() -> std::io::Result<(u32, u32, u32)>,
146    {
147        match file_id_result {
148            Ok((volume_serial_number, file_id)) => {
149                Ok(Self::from_windows_parts(volume_serial_number, file_id))
150            }
151            Err(err) if is_windows_file_id_unsupported(&err) => {
152                let (volume_serial_number, file_index_high, file_index_low) = legacy_query()?;
153                Ok(Some(Self::from_windows_legacy_parts(
154                    volume_serial_number,
155                    file_index_high,
156                    file_index_low,
157                )))
158            }
159            Err(err) => Err(err),
160        }
161    }
162
163    /// Encode this identity for a namespace record.
164    ///
165    /// Byte 0 is the representation tag, bytes 1..9 are the big-endian
166    /// namespace, and bytes 9..25 are the exact object identifier.
167    #[cfg(any(unix, windows))]
168    pub(crate) fn to_namespace_bytes(self) -> [u8; 25] {
169        let tag = match self.kind {
170            // Memory identities are process-local. The tag round-trips for
171            // in-process coordination/tests only; callers must never persist
172            // it as a reusable cross-process namespace identity.
173            FileIdentityKind::Memory => 4,
174            #[cfg(unix)]
175            FileIdentityKind::Unix => 1,
176            #[cfg(windows)]
177            FileIdentityKind::WindowsFileId128 => 2,
178            #[cfg(windows)]
179            FileIdentityKind::WindowsFileIndex64 => 3,
180        };
181        let mut encoded = [0_u8; 25];
182        encoded[0] = tag;
183        encoded[1..9].copy_from_slice(&self.namespace.to_be_bytes());
184        encoded[9..].copy_from_slice(&self.object);
185        encoded
186    }
187
188    /// Decode and validate a namespace-record identity.
189    #[cfg(any(unix, windows))]
190    pub(crate) fn from_namespace_bytes(encoded: [u8; 25]) -> Option<Self> {
191        let mut namespace_bytes = [0_u8; 8];
192        namespace_bytes.copy_from_slice(&encoded[1..9]);
193        let namespace = u64::from_be_bytes(namespace_bytes);
194        let mut object = [0_u8; 16];
195        object.copy_from_slice(&encoded[9..]);
196
197        match encoded[0] {
198            4 if object[8..].iter().all(|byte| *byte == 0) => Some(Self {
199                kind: FileIdentityKind::Memory,
200                namespace,
201                object,
202            }),
203            #[cfg(unix)]
204            1 if object[8..].iter().all(|byte| *byte == 0) => Some(Self {
205                kind: FileIdentityKind::Unix,
206                namespace,
207                object,
208            }),
209            #[cfg(windows)]
210            2 => Self::from_windows_parts(namespace, object),
211            #[cfg(windows)]
212            3 if u32::try_from(namespace).is_ok() && object[8..].iter().all(|byte| *byte == 0) => {
213                Some(Self {
214                    kind: FileIdentityKind::WindowsFileIndex64,
215                    namespace,
216                    object,
217                })
218            }
219            _ => None,
220        }
221    }
222}
223
224#[cfg(all(test, any(unix, windows)))]
225mod memory_file_identity_codec_tests {
226    use super::FileIdentity;
227
228    #[test]
229    fn memory_namespace_bytes_round_trip_with_distinct_tag() {
230        let identity = FileIdentity::from_memory_parts(17, 29);
231        let encoded = identity.to_namespace_bytes();
232
233        assert_eq!(encoded[0], 4);
234        assert_eq!(FileIdentity::from_namespace_bytes(encoded), Some(identity));
235        assert_ne!(
236            identity,
237            FileIdentity::from_memory_parts(18, 29),
238            "independent MemoryVfs instances must remain isolated"
239        );
240        assert_ne!(
241            identity,
242            FileIdentity::from_memory_parts(17, 30),
243            "distinct named files in one MemoryVfs must remain isolated"
244        );
245    }
246}
247
248#[cfg(windows)]
249fn query_windows_file_id(
250    handle: std::os::windows::io::RawHandle,
251) -> std::io::Result<(u64, [u8; 16])> {
252    use std::mem::size_of;
253    use windows_sys::Win32::Storage::FileSystem::{
254        FILE_ID_INFO, FileIdInfo, GetFileInformationByHandleEx,
255    };
256
257    let mut identity = FILE_ID_INFO::default();
258    let identity_size = u32::try_from(size_of::<FILE_ID_INFO>())
259        .map_err(|_| std::io::Error::other("FILE_ID_INFO size does not fit in a Windows DWORD"))?;
260    // SAFETY: the caller supplies a live Windows file handle, `identity` is a
261    // correctly sized writable `FILE_ID_INFO` buffer, and both remain valid
262    // for the duration of this synchronous system call.
263    let succeeded = unsafe {
264        GetFileInformationByHandleEx(
265            handle,
266            FileIdInfo,
267            std::ptr::from_mut(&mut identity).cast(),
268            identity_size,
269        )
270    };
271    if succeeded == 0 {
272        return Err(std::io::Error::last_os_error());
273    }
274    Ok((identity.VolumeSerialNumber, identity.FileId.Identifier))
275}
276
277#[cfg(windows)]
278fn query_windows_legacy_file_index(
279    handle: std::os::windows::io::RawHandle,
280) -> std::io::Result<(u32, u32, u32)> {
281    use windows_sys::Win32::Storage::FileSystem::{
282        BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
283    };
284
285    let mut identity = BY_HANDLE_FILE_INFORMATION::default();
286    // SAFETY: the caller supplies a live Windows file handle and `identity`
287    // is a correctly sized writable buffer that remains valid for the
288    // duration of this synchronous system call.
289    let succeeded = unsafe { GetFileInformationByHandle(handle, &raw mut identity) };
290    if succeeded == 0 {
291        return Err(std::io::Error::last_os_error());
292    }
293    Ok((
294        identity.dwVolumeSerialNumber,
295        identity.nFileIndexHigh,
296        identity.nFileIndexLow,
297    ))
298}
299
300#[cfg(windows)]
301fn is_windows_file_id_unsupported(err: &std::io::Error) -> bool {
302    use windows_sys::Win32::Foundation::{
303        ERROR_CALL_NOT_IMPLEMENTED, ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER,
304        ERROR_NOT_SUPPORTED,
305    };
306
307    err.raw_os_error().is_some_and(|raw_error| {
308        raw_error == ERROR_INVALID_FUNCTION as i32
309            || raw_error == ERROR_NOT_SUPPORTED as i32
310            || raw_error == ERROR_INVALID_PARAMETER as i32
311            || raw_error == ERROR_CALL_NOT_IMPLEMENTED as i32
312    })
313}
314
315impl std::fmt::Debug for FileIdentity {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        f.write_str("FileIdentity(..)")
318    }
319}
320
321#[cfg(all(test, unix))]
322mod unix_file_identity_codec_tests {
323    use super::FileIdentity;
324
325    #[test]
326    fn unix_namespace_bytes_round_trip_exactly() {
327        let identity = FileIdentity::from_unix_parts(0x0102_0304_0506_0708, 0x1122_3344_5566_7788);
328        let encoded = identity.to_namespace_bytes();
329
330        assert_eq!(encoded[0], 1);
331        assert_eq!(&encoded[1..9], &0x0102_0304_0506_0708_u64.to_be_bytes());
332        assert_eq!(&encoded[9..17], &0x1122_3344_5566_7788_u64.to_be_bytes());
333        assert_eq!(&encoded[17..], &[0_u8; 8]);
334        assert_eq!(FileIdentity::from_namespace_bytes(encoded), Some(identity));
335    }
336
337    #[test]
338    fn unix_namespace_bytes_reject_unknown_and_noncanonical_records() {
339        let identity = FileIdentity::from_unix_parts(7, 11);
340        let mut unknown_tag = identity.to_namespace_bytes();
341        unknown_tag[0] = u8::MAX;
342        assert_eq!(FileIdentity::from_namespace_bytes(unknown_tag), None);
343
344        let mut nonzero_tail = identity.to_namespace_bytes();
345        nonzero_tail[24] = 1;
346        assert_eq!(FileIdentity::from_namespace_bytes(nonzero_tail), None);
347    }
348}
349
350#[cfg(all(test, windows))]
351mod file_identity_tests {
352    use super::FileIdentity;
353    use std::cell::Cell;
354    use std::io;
355    use windows_sys::Win32::Foundation::{
356        ERROR_CALL_NOT_IMPLEMENTED, ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER,
357        ERROR_NOT_SUPPORTED,
358    };
359
360    #[test]
361    fn windows_identity_compares_all_file_id_bits() {
362        let file_id = [0x5a_u8; 16];
363        let mut different_high_byte = file_id;
364        different_high_byte[15] ^= 0xff;
365
366        assert_ne!(
367            FileIdentity::from_windows_parts(7, file_id),
368            FileIdentity::from_windows_parts(7, different_high_byte),
369        );
370    }
371
372    #[test]
373    fn windows_identity_rejects_reserved_sentinels() {
374        assert_eq!(FileIdentity::from_windows_parts(7, [0_u8; 16]), None);
375        assert_eq!(FileIdentity::from_windows_parts(7, [u8::MAX; 16]), None);
376    }
377
378    #[test]
379    fn windows_identity_prefers_full_file_id_without_legacy_query() {
380        let expected = [0x3c_u8; 16];
381        let legacy_queried = Cell::new(false);
382
383        let actual = FileIdentity::from_windows_query_result(Ok((19, expected)), || {
384            legacy_queried.set(true);
385            Ok((19, 0, 7))
386        })
387        .expect("full FileIdInfo result should succeed");
388
389        assert_eq!(actual, FileIdentity::from_windows_parts(19, expected));
390        assert!(!legacy_queried.get());
391    }
392
393    #[test]
394    fn windows_identity_falls_back_for_unsupported_file_id_queries() {
395        for code in [
396            ERROR_INVALID_FUNCTION,
397            ERROR_NOT_SUPPORTED,
398            ERROR_INVALID_PARAMETER,
399            ERROR_CALL_NOT_IMPLEMENTED,
400        ] {
401            let actual = FileIdentity::from_windows_query_result(
402                Err(io::Error::from_raw_os_error(code as i32)),
403                || Ok((23, 0x1122_3344, 0x5566_7788)),
404            )
405            .expect("unsupported FileIdInfo should use the legacy query")
406            .expect("legacy file index should produce an identity");
407
408            assert_eq!(
409                actual,
410                FileIdentity::from_windows_legacy_parts(23, 0x1122_3344, 0x5566_7788)
411            );
412        }
413    }
414
415    #[test]
416    fn windows_identity_does_not_fallback_for_real_io_errors() {
417        let legacy_queried = Cell::new(false);
418        let err =
419            FileIdentity::from_windows_query_result(Err(io::Error::from_raw_os_error(6)), || {
420                legacy_queried.set(true);
421                Ok((1, 2, 3))
422            })
423            .expect_err("invalid handles must remain hard errors");
424
425        assert_eq!(err.raw_os_error(), Some(6));
426        assert!(!legacy_queried.get());
427    }
428
429    #[test]
430    fn windows_identity_separates_full_and_legacy_representation_domains() {
431        let file_index = 0x1122_3344_5566_7788_u64;
432        let mut full_file_id = [0_u8; 16];
433        full_file_id[..8].copy_from_slice(&file_index.to_be_bytes());
434
435        assert_ne!(
436            FileIdentity::from_windows_parts(23, full_file_id).expect("non-sentinel full file ID"),
437            FileIdentity::from_windows_legacy_parts(23, 0x1122_3344, 0x5566_7788),
438        );
439    }
440
441    #[test]
442    fn windows_namespace_bytes_round_trip_both_identity_domains() {
443        let full_object = [0x5a_u8; 16];
444        let full = FileIdentity::from_windows_parts(0x0102_0304_0506_0708, full_object)
445            .expect("non-sentinel full file ID");
446        let full_encoded = full.to_namespace_bytes();
447        assert_eq!(full_encoded[0], 2);
448        assert_eq!(
449            &full_encoded[1..9],
450            &0x0102_0304_0506_0708_u64.to_be_bytes()
451        );
452        assert_eq!(&full_encoded[9..], &full_object);
453        assert_eq!(FileIdentity::from_namespace_bytes(full_encoded), Some(full));
454
455        let legacy = FileIdentity::from_windows_legacy_parts(0x0102_0304, 0x1122_3344, 0x5566_7788);
456        let legacy_encoded = legacy.to_namespace_bytes();
457        assert_eq!(legacy_encoded[0], 3);
458        assert_eq!(&legacy_encoded[1..9], &0x0102_0304_u64.to_be_bytes());
459        assert_eq!(
460            &legacy_encoded[9..17],
461            &0x1122_3344_5566_7788_u64.to_be_bytes()
462        );
463        assert_eq!(&legacy_encoded[17..], &[0_u8; 8]);
464        assert_eq!(
465            FileIdentity::from_namespace_bytes(legacy_encoded),
466            Some(legacy)
467        );
468    }
469
470    #[test]
471    fn windows_namespace_bytes_reject_noncanonical_records() {
472        let mut unknown_tag = [0_u8; 25];
473        unknown_tag[0] = u8::MAX;
474        assert_eq!(FileIdentity::from_namespace_bytes(unknown_tag), None);
475
476        let mut full_zero_sentinel = [0_u8; 25];
477        full_zero_sentinel[0] = 2;
478        assert_eq!(FileIdentity::from_namespace_bytes(full_zero_sentinel), None);
479
480        let mut full_ones_sentinel = [u8::MAX; 25];
481        full_ones_sentinel[0] = 2;
482        assert_eq!(FileIdentity::from_namespace_bytes(full_ones_sentinel), None);
483
484        let legacy = FileIdentity::from_windows_legacy_parts(7, 11, 13);
485        let mut legacy_nonzero_tail = legacy.to_namespace_bytes();
486        legacy_nonzero_tail[24] = 1;
487        assert_eq!(
488            FileIdentity::from_namespace_bytes(legacy_nonzero_tail),
489            None
490        );
491
492        let mut legacy_wide_namespace = legacy.to_namespace_bytes();
493        legacy_wide_namespace[1..9].copy_from_slice(&(u64::from(u32::MAX) + 1).to_be_bytes());
494        assert_eq!(
495            FileIdentity::from_namespace_bytes(legacy_wide_namespace),
496            None
497        );
498    }
499}
500
501/// Durability level for `VfsFile::durable_sync`.
502///
503/// Centralizes per-filesystem sync policy so callers express intent
504/// ("make WAL frames durable") and the VFS maps it to the correct
505/// syscall: fdatasync on ext4/XFS, fsync on btrfs/ZFS, F_FULLFSYNC
506/// on APFS.
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
508pub enum SyncKind {
509    /// Data-only sync (fdatasync on Linux). Sufficient when file size
510    /// and metadata are unchanged (e.g. overwriting existing WAL frames).
511    DataOnly,
512    /// Data + metadata sync (fsync on Linux). Required when new allocation
513    /// blocks must be persisted (file growth, btrfs CoW).
514    DataAndMetadata,
515    /// Full durable barrier: the strongest sync the platform offers.
516    /// On APFS this maps to F_FULLFSYNC; elsewhere identical to
517    /// `DataAndMetadata`. Required for WAL commit frames.
518    FullDurable,
519}
520
521static DEFAULT_RANDOMNESS_CALL_SEQ: AtomicU64 = AtomicU64::new(0);
522
523/// A virtual filesystem implementation.
524///
525/// This trait abstracts all file system operations, allowing different
526/// backends: real files (Unix), in-memory (testing), or custom implementations.
527///
528/// Modeled after C SQLite's `sqlite3_vfs` struct from `os.h`.
529pub trait Vfs: Send + Sync {
530    /// The file handle type produced by this VFS.
531    type File: VfsFile;
532
533    /// The name of this VFS (e.g., "unix", "memory").
534    fn name(&self) -> &'static str;
535
536    /// Open a file.
537    ///
538    /// `path` is `None` for temporary files that should be auto-named.
539    /// `flags` describes what kind of file (main DB, journal, WAL, etc.)
540    /// and how to open it (create, read-write, exclusive, etc.).
541    ///
542    /// Returns the opened file and the flags that were actually used (the VFS
543    /// may add flags like `READWRITE` when `CREATE` is specified).
544    fn open(
545        &self,
546        cx: &Cx,
547        path: Option<&Path>,
548        flags: VfsOpenFlags,
549    ) -> Result<(Self::File, VfsOpenFlags)>;
550
551    /// Open an existing file only if its handle has `expected_identity`.
552    ///
553    /// The default implementation verifies the identity immediately after
554    /// opening. Filesystem backends whose normal open path can mutate related
555    /// artifacts should override this method and perform an earlier,
556    /// side-effect-free identity preflight as well. Because an expected
557    /// identity can only belong to an existing object, `CREATE` and
558    /// `EXCLUSIVE` are stripped before the open.
559    fn open_with_expected_identity(
560        &self,
561        cx: &Cx,
562        path: &Path,
563        flags: VfsOpenFlags,
564        expected_identity: FileIdentity,
565    ) -> Result<(Self::File, VfsOpenFlags)> {
566        let mut existing_flags = flags;
567        existing_flags.remove(VfsOpenFlags::CREATE | VfsOpenFlags::EXCLUSIVE);
568        let (file, actual_flags) = self.open(cx, Some(path), existing_flags)?;
569        if file.file_identity()? != Some(expected_identity) {
570            return Err(fsqlite_error::FrankenError::CannotOpen {
571                path: path.to_owned(),
572            });
573        }
574        Ok((file, actual_flags))
575    }
576
577    /// Open a caller-reserved empty file without creating it or recovering
578    /// any pre-existing database artifacts.
579    ///
580    /// Backends whose ordinary open path creates auxiliary files must
581    /// override this method so the identity, zero-length, and recovery-
582    /// artifact checks all occur before those side effects.
583    fn open_reserved_with_expected_identity(
584        &self,
585        cx: &Cx,
586        path: &Path,
587        flags: VfsOpenFlags,
588        expected_identity: FileIdentity,
589    ) -> Result<(Self::File, VfsOpenFlags)> {
590        let (file, actual_flags) =
591            self.open_with_expected_identity(cx, path, flags, expected_identity)?;
592        if file.file_size(cx)? != 0 {
593            return Err(fsqlite_error::FrankenError::CannotOpen {
594                path: path.to_owned(),
595            });
596        }
597
598        for suffix in ["-journal", "-wal", "-wal-fec", "-shm"] {
599            let mut artifact_path = path.as_os_str().to_owned();
600            artifact_path.push(suffix);
601            if self.path_entry_exists(cx, Path::new(&artifact_path))? {
602                return Err(fsqlite_error::FrankenError::CannotOpen {
603                    path: path.to_owned(),
604                });
605            }
606        }
607        Ok((file, actual_flags))
608    }
609
610    /// Delete a file.
611    ///
612    /// If `sync_dir` is true, the directory entry removal should be synced
613    /// to ensure durability.
614    fn delete(&self, cx: &Cx, path: &Path, sync_dir: bool) -> Result<()>;
615
616    /// Synchronize the parent directory containing `path`.
617    ///
618    /// Durable create-before-mutate protocols (notably rollback journals)
619    /// must make the newly-created directory entry stable before they modify
620    /// the protected file.  Filesystems that do not require or support an
621    /// explicit directory sync may keep the default no-op implementation.
622    fn sync_parent_directory(&self, _cx: &Cx, _path: &Path) -> Result<()> {
623        Ok(())
624    }
625
626    /// Check file access.
627    ///
628    /// Returns true if the file at `path` satisfies the access check
629    /// described by `flags`.
630    fn access(&self, cx: &Cx, path: &Path, flags: AccessFlags) -> Result<bool>;
631
632    /// Return whether a directory entry exists without following its final
633    /// symlink component.
634    ///
635    /// This is stricter than [`Path::exists`] and is required for gates that
636    /// must refuse dangling recovery-artifact symlinks. Virtual filesystems
637    /// without symlink semantics may delegate to [`Self::access`].
638    fn path_entry_exists(&self, cx: &Cx, path: &Path) -> Result<bool> {
639        self.access(cx, path, AccessFlags::EXISTS)
640    }
641
642    /// Resolve a potentially relative path into an absolute path.
643    fn full_pathname(&self, cx: &Cx, path: &Path) -> Result<PathBuf>;
644
645    /// Generate a random byte sequence for temporary file naming.
646    ///
647    /// Fills `buf` with bytes suitable for temporary file naming.
648    ///
649    /// The default implementation is deterministic (xorshift seeded from a
650    /// process-local counter) for reproducible tests; real VFS implementations
651    /// should override this and use OS-provided randomness to avoid collisions.
652    fn randomness(&self, cx: &Cx, buf: &mut [u8]) {
653        // Default: fill with pseudo-random bytes using a simple xorshift.
654        // Real VFS implementations should use OS-provided randomness.
655        let _ = cx; // Usage to silence unused variable warning
656        let seq = DEFAULT_RANDOMNESS_CALL_SEQ.fetch_add(1, Ordering::Relaxed);
657        let mut state: u64 = 0x5DEE_CE66_D1A4_F681 ^ seq.wrapping_mul(0x9E37_79B9_7F4A_7C15);
658        for chunk in buf.chunks_mut(8) {
659            state ^= state << 13;
660            state ^= state >> 7;
661            state ^= state << 17;
662            let bytes = state.to_le_bytes();
663            for (dst, &src) in chunk.iter_mut().zip(bytes.iter()) {
664                *dst = src;
665            }
666        }
667    }
668
669    /// Return the current time as a Julian day number (days since noon
670    /// on November 24, 4714 B.C.).
671    fn current_time(&self, cx: &Cx) -> f64 {
672        // Default: derive from `Cx` time capability (no ambient authority).
673        cx.current_time_julian_day()
674    }
675
676    /// Returns true if this VFS operates entirely in-process memory.
677    /// In-memory VFS backends can skip file locking, journal recovery,
678    /// and other I/O-oriented work in the pager hot path.
679    fn is_memory(&self) -> bool {
680        false
681    }
682}
683
684/// Observable terminal state of one tracked VFS write.
685///
686/// `Pending` is deliberately fail-closed: it means only that the write's
687/// actual completion source has not reported a terminal result. It must never
688/// be interpreted as proof that no bytes reached the file.
689#[derive(Clone, Copy, Debug, Eq, PartialEq)]
690pub enum VfsWriteCompletionState {
691    /// The write has not yet reached a terminal completion source.
692    Pending,
693    /// The complete requested byte range was written.
694    Success,
695    /// The write terminated with an error or cancellation.
696    ///
697    /// This does not prove that zero bytes were written; a caller reconciling
698    /// an append must still validate the on-disk interval before aborting it.
699    Error,
700}
701
702#[derive(Debug)]
703struct VfsWriteCompletionInner {
704    state: VfsWriteCompletionState,
705    next_waiter_id: u64,
706    waiters: Vec<(u64, Waker)>,
707    terminal_relay: Option<(VfsWriteCompletion, VfsWriteCompletionState)>,
708}
709
710/// Cloneable observation handle for one VFS write operation.
711///
712/// Production VFS backends complete this token at the source that owns the
713/// side effect: the blocking I/O closure on Unix and Windows, the io_uring CQE
714/// driver on Linux, or the immediate in-memory mutation path. Consequently a
715/// clone remains useful even when the future returned by
716/// [`VfsFile::write_tracked`] is dropped before it can observe completion.
717///
718/// A token is single-use. The first terminal transition wins.
719#[derive(Clone, Debug)]
720pub struct VfsWriteCompletion {
721    inner: Arc<Mutex<VfsWriteCompletionInner>>,
722}
723
724impl VfsWriteCompletion {
725    /// Create a pending completion token for one logical write.
726    #[must_use]
727    pub fn new() -> Self {
728        Self {
729            inner: Arc::new(Mutex::new(VfsWriteCompletionInner {
730                state: VfsWriteCompletionState::Pending,
731                next_waiter_id: 0,
732                waiters: Vec::new(),
733                terminal_relay: None,
734            })),
735        }
736    }
737
738    /// Return the currently observable state.
739    #[must_use]
740    pub fn state(&self) -> VfsWriteCompletionState {
741        self.inner
742            .lock()
743            .unwrap_or_else(std::sync::PoisonError::into_inner)
744            .state
745    }
746
747    /// Wait until the write reaches `Success` or `Error`.
748    ///
749    /// Dropping the returned future only unregisters that waiter. It neither
750    /// cancels the write nor consumes the terminal result.
751    #[must_use]
752    pub fn wait(&self) -> VfsWriteCompletionWait {
753        VfsWriteCompletionWait {
754            completion: self.clone(),
755            waiter_id: None,
756        }
757    }
758
759    /// Record successful completion at the source that owns the write.
760    ///
761    /// Returns `true` when this call performed the terminal transition.
762    #[doc(hidden)]
763    pub fn complete_success(&self) -> bool {
764        self.complete(VfsWriteCompletionState::Success)
765    }
766
767    /// Record failed or cancelled completion at the source that owns the write.
768    ///
769    /// Returns `true` when this call performed the terminal transition.
770    #[doc(hidden)]
771    pub fn complete_error(&self) -> bool {
772        self.complete(VfsWriteCompletionState::Error)
773    }
774
775    /// Create a child whose source completion terminates this token as Error.
776    ///
777    /// Fault wrappers use this for intentional partial writes: the lower
778    /// backend still owns the completion instant, while the outer operation
779    /// can never truthfully report full-write success.
780    #[doc(hidden)]
781    #[must_use]
782    pub fn error_mapped_child(&self) -> Self {
783        Self {
784            inner: Arc::new(Mutex::new(VfsWriteCompletionInner {
785                state: VfsWriteCompletionState::Pending,
786                next_waiter_id: 0,
787                waiters: Vec::new(),
788                terminal_relay: Some((self.clone(), VfsWriteCompletionState::Error)),
789            })),
790        }
791    }
792
793    fn complete(&self, terminal: VfsWriteCompletionState) -> bool {
794        debug_assert_ne!(terminal, VfsWriteCompletionState::Pending);
795        let (waiters, terminal_relay) = {
796            let mut inner = self
797                .inner
798                .lock()
799                .unwrap_or_else(std::sync::PoisonError::into_inner);
800            if inner.state != VfsWriteCompletionState::Pending {
801                return false;
802            }
803            inner.state = terminal;
804            (
805                std::mem::take(&mut inner.waiters),
806                inner.terminal_relay.take(),
807            )
808        };
809        for (_, waiter) in waiters {
810            waiter.wake();
811        }
812        if let Some((relay, mapped_terminal)) = terminal_relay {
813            relay.complete(mapped_terminal);
814        }
815        true
816    }
817}
818
819impl Default for VfsWriteCompletion {
820    fn default() -> Self {
821        Self::new()
822    }
823}
824
825/// Source-owned guard that fails closed if an accepted write is abandoned.
826///
827/// Blocking-pool cancellation can discard a queued closure before executing
828/// it, and driver teardown can discard an uncompleted request. Keeping this
829/// guard with that source ensures those paths terminate as `Error` instead of
830/// leaving a caller-retained token pending forever.
831#[cfg(not(target_arch = "wasm32"))]
832#[derive(Debug)]
833pub(crate) struct VfsWriteCompletionSource {
834    completion: VfsWriteCompletion,
835    armed: bool,
836}
837
838#[cfg(not(target_arch = "wasm32"))]
839impl VfsWriteCompletionSource {
840    pub(crate) fn new(completion: VfsWriteCompletion) -> Self {
841        Self {
842            completion,
843            armed: true,
844        }
845    }
846
847    pub(crate) fn complete_success(&mut self) {
848        self.completion.complete_success();
849        self.armed = false;
850    }
851
852    pub(crate) fn complete_error(&mut self) {
853        self.completion.complete_error();
854        self.armed = false;
855    }
856}
857
858#[cfg(not(target_arch = "wasm32"))]
859impl Drop for VfsWriteCompletionSource {
860    fn drop(&mut self) {
861        if self.armed {
862            self.completion.complete_error();
863        }
864    }
865}
866
867/// Cancel-safe future returned by [`VfsWriteCompletion::wait`].
868#[derive(Debug)]
869pub struct VfsWriteCompletionWait {
870    completion: VfsWriteCompletion,
871    waiter_id: Option<u64>,
872}
873
874impl std::future::Future for VfsWriteCompletionWait {
875    type Output = VfsWriteCompletionState;
876
877    fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
878        let this = self.as_mut().get_mut();
879        let completion_inner = Arc::clone(&this.completion.inner);
880        let mut inner = completion_inner
881            .lock()
882            .unwrap_or_else(std::sync::PoisonError::into_inner);
883        if inner.state != VfsWriteCompletionState::Pending {
884            if let Some(waiter_id) = this.waiter_id.take()
885                && let Some(index) = inner
886                    .waiters
887                    .iter()
888                    .position(|(registered_id, _)| *registered_id == waiter_id)
889            {
890                inner.waiters.swap_remove(index);
891            }
892            return Poll::Ready(inner.state);
893        }
894
895        if let Some(waiter_id) = this.waiter_id {
896            let (_, registered_waker) = inner
897                .waiters
898                .iter_mut()
899                .find(|(registered_id, _)| *registered_id == waiter_id)
900                .expect("pending completion waiter must remain registered");
901            if !registered_waker.will_wake(cx.waker()) {
902                registered_waker.clone_from(cx.waker());
903            }
904        } else {
905            let waiter_id = loop {
906                let candidate = inner.next_waiter_id;
907                inner.next_waiter_id = inner.next_waiter_id.wrapping_add(1);
908                if inner
909                    .waiters
910                    .iter()
911                    .all(|(registered_id, _)| *registered_id != candidate)
912                {
913                    break candidate;
914                }
915            };
916            inner.waiters.push((waiter_id, cx.waker().clone()));
917            this.waiter_id = Some(waiter_id);
918        }
919        Poll::Pending
920    }
921}
922
923impl Drop for VfsWriteCompletionWait {
924    fn drop(&mut self) {
925        let Some(waiter_id) = self.waiter_id.take() else {
926            return;
927        };
928        let mut inner = self
929            .completion
930            .inner
931            .lock()
932            .unwrap_or_else(std::sync::PoisonError::into_inner);
933        if let Some(index) = inner
934            .waiters
935            .iter()
936            .position(|(registered_id, _)| *registered_id == waiter_id)
937        {
938            inner.waiters.swap_remove(index);
939        }
940    }
941}
942
943/// A file handle opened by a VFS.
944///
945/// Corresponds to C SQLite's `sqlite3_file` + `sqlite3_io_methods`.
946pub trait VfsFile: Send + Sync {
947    /// Close the file.
948    ///
949    /// After this call, the file handle should not be used.
950    fn close(&mut self, cx: &Cx) -> Result<()>;
951
952    /// Return the identity of the filesystem object held by this open handle.
953    ///
954    /// Implementations must derive this from the open descriptor (or from
955    /// descriptor metadata captured at open time), never by resolving the
956    /// current pathname. The default is `None` for custom backends that cannot
957    /// provide a stable comparable identity.
958    fn file_identity(&self) -> Result<Option<FileIdentity>> {
959        Ok(None)
960    }
961
962    /// Read `buf.len()` bytes starting at byte offset `offset`.
963    ///
964    /// Returns the number of bytes actually read. If fewer bytes are read
965    /// than requested (short read), the remaining bytes in `buf` are zeroed.
966    fn read<'a>(
967        &'a self,
968        cx: &'a Cx,
969        buf: &'a mut [u8],
970        offset: u64,
971    ) -> impl std::future::Future<Output = Result<usize>> + Send + 'a;
972
973    /// Write `buf` starting at byte offset `offset`.
974    fn write<'a>(
975        &'a self,
976        cx: &'a Cx,
977        buf: &'a [u8],
978        offset: u64,
979    ) -> impl std::future::Future<Output = Result<()>> + Send + 'a;
980
981    /// Write `buf` and report terminal side-effect completion through `completion`.
982    ///
983    /// The ordinary [`Self::write`] contract is unchanged. This additive path
984    /// lets durability coordinators retain an observation handle when their
985    /// caller future is externally dropped. Backends whose hidden side effect
986    /// outlives their returned future must override this method and complete
987    /// the token at that hidden source. The conservative default can remain
988    /// `Pending` after such a drop; callers must treat that as in-doubt.
989    fn write_tracked<'a>(
990        &'a self,
991        cx: &'a Cx,
992        buf: &'a [u8],
993        offset: u64,
994        completion: VfsWriteCompletion,
995    ) -> impl std::future::Future<Output = Result<()>> + Send + 'a {
996        async move {
997            let result = self.write(cx, buf, offset).await;
998            if result.is_ok() {
999                completion.complete_success();
1000            } else {
1001                completion.complete_error();
1002            }
1003            result
1004        }
1005    }
1006
1007    /// Write multiple page-sized buffers in one logical operation.
1008    ///
1009    /// The default implementation preserves existing semantics by issuing the
1010    /// writes sequentially through [`Self::write`]. VFS backends may override
1011    /// this to amortize locking or syscall overhead for hot pager commit paths.
1012    fn write_page_batch<'a>(
1013        &'a self,
1014        cx: &'a Cx,
1015        writes: &'a [(u64, &'a [u8])],
1016    ) -> impl std::future::Future<Output = Result<()>> + Send + 'a {
1017        async move {
1018            for (offset, data) in writes {
1019                self.write(cx, data, *offset).await?;
1020            }
1021            Ok(())
1022        }
1023    }
1024
1025    /// Truncate the file to `size` bytes.
1026    fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()>;
1027
1028    /// Sync the file contents to stable storage.
1029    ///
1030    /// `flags` indicates the type of sync (normal, full, data-only).
1031    fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()>;
1032
1033    /// Durability-intent sync with per-filesystem policy.
1034    ///
1035    /// Callers express *what* must be durable (`SyncKind`); the VFS maps
1036    /// it to the correct platform syscall. Default delegates to `sync()`
1037    /// with `DATAONLY` / `FULL` flags; platform VFS backends override for
1038    /// filesystem-specific behaviour (F_FULLFSYNC on APFS, etc.).
1039    fn durable_sync(&mut self, cx: &Cx, kind: SyncKind) -> Result<()> {
1040        let flags = match kind {
1041            SyncKind::DataOnly => SyncFlags::DATAONLY,
1042            SyncKind::DataAndMetadata | SyncKind::FullDurable => SyncFlags::FULL,
1043        };
1044        self.sync(cx, flags)
1045    }
1046
1047    /// Return the current file size in bytes.
1048    fn file_size(&self, cx: &Cx) -> Result<u64>;
1049
1050    /// Acquire a file lock at the given level.
1051    ///
1052    /// SQLite's five-level locking: None < Shared < Reserved < Pending < Exclusive.
1053    fn lock(&mut self, cx: &Cx, level: LockLevel) -> Result<()>;
1054
1055    /// Release the file lock to the given level.
1056    fn unlock(&mut self, cx: &Cx, level: LockLevel) -> Result<()>;
1057
1058    /// Acquire the cross-process SHARED fence used while capturing a coherent
1059    /// main-database snapshot.
1060    ///
1061    /// The backend must publish an exact attempt marker before its first raw
1062    /// lock side effect. A clean or partial acquisition error retains that
1063    /// marker until [`Self::restore_external_shared_snapshot_attempt`]
1064    /// succeeds.
1065    fn lock_external_shared_snapshot(&mut self, cx: &Cx) -> Result<()>;
1066
1067    /// Restore the exact baseline of an external shared-snapshot attempt.
1068    ///
1069    /// This is an attempt obligation, not an ordinary unlock. It must be safe
1070    /// before any raw lock was acquired, after clean or partial acquisition
1071    /// failure, after success, and on repeated calls. An error must retain
1072    /// enough exact backend state for a later retry.
1073    fn restore_external_shared_snapshot_attempt(&mut self, cx: &Cx) -> Result<()>;
1074
1075    /// Acquire the cross-process fence for an operation that replaces the
1076    /// complete main-database image in place.
1077    ///
1078    /// The backend must record the requested journal mode, exact prior main
1079    /// lock level, and only the WAL slots newly acquired by this attempt before
1080    /// returning control to the caller.
1081    fn lock_external_maintenance(&mut self, cx: &Cx, wal_mode: bool) -> Result<()>;
1082
1083    /// Restore the exact baseline of an external maintenance acquisition
1084    /// attempt.
1085    ///
1086    /// This is deliberately distinct from strict ordinary SHM unlocks. The
1087    /// backend-owned attempt marker is authoritative about whether WAL
1088    /// surfaces participated; callers cannot supply a second, possibly stale
1089    /// mode. Restoration must be idempotent before acquisition, after clean or
1090    /// partial failure, after success, and across retries. Every retained
1091    /// surface is attempted even if another restoration fails. Each
1092    /// successfully restored surface is forgotten only after its raw unlock
1093    /// succeeds.
1094    fn restore_external_maintenance_attempt(&mut self, cx: &Cx) -> Result<()>;
1095
1096    /// Check if another process holds a reserved lock.
1097    ///
1098    /// Returns true if a RESERVED or higher lock is held by another connection.
1099    fn check_reserved_lock(&self, cx: &Cx) -> Result<bool>;
1100
1101    /// Return the sector size for this file.
1102    ///
1103    /// The sector size is the minimum write granularity for the underlying
1104    /// storage. Defaults to 4096 bytes.
1105    fn sector_size(&self) -> u32 {
1106        4096
1107    }
1108
1109    /// Return device characteristics flags.
1110    ///
1111    /// These flags describe capabilities of the underlying storage device,
1112    /// such as whether it supports atomic writes. Returns 0 for no special
1113    /// characteristics.
1114    fn device_characteristics(&self) -> u32 {
1115        0
1116    }
1117
1118    // --- Shared-memory methods (required for WAL mode) ---
1119
1120    /// Map a region of shared memory. `region` is a 0-based index of 32KB
1121    /// regions. If `extend` is true and the region does not exist, create it.
1122    /// Returns a safe [`ShmRegion`] handle with bounds-checked accessors.
1123    /// (Equivalent to sqlite3_io_methods.xShmMap)
1124    fn shm_map(&mut self, cx: &Cx, region: u32, size: u32, extend: bool) -> Result<ShmRegion>;
1125
1126    /// Acquire or release a shared-memory lock.
1127    /// `offset` and `n` define a range of lock slots.
1128    /// `flags`: SHM_LOCK | (SHM_SHARED | SHM_EXCLUSIVE).
1129    /// (Equivalent to sqlite3_io_methods.xShmLock)
1130    fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()>;
1131
1132    /// Memory barrier for shared memory -- ensures all prior SHM writes are
1133    /// visible to other processes before subsequent reads.
1134    /// (Equivalent to sqlite3_io_methods.xShmBarrier)
1135    fn shm_barrier(&self);
1136
1137    /// Unmap all shared-memory regions. If `delete` is true, also delete
1138    /// the underlying SHM file.
1139    /// (Equivalent to sqlite3_io_methods.xShmUnmap)
1140    fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()>;
1141
1142    /// Set the busy-timeout for cross-process file-lock contention.
1143    ///
1144    /// When `ms > 0`, the VFS should retry `F_SETLK` with exponential
1145    /// backoff instead of returning `SQLITE_BUSY` immediately on
1146    /// `EAGAIN`/`EACCES`. A value of `0` disables retries (fail-fast).
1147    ///
1148    /// Default implementation is a no-op (memory and stub VFS backends
1149    /// have no OS-level lock contention).
1150    fn set_busy_timeout_ms(&mut self, _ms: u64) {}
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155    // Test doubles implement the async `VfsFile` surface with synchronous
1156    // bodies; the futures must stay lazy to honor the trait contract, so the
1157    // `async` is deliberate even though no body awaits.
1158    #![allow(clippy::unused_async_trait_impl)]
1159
1160    use super::*;
1161
1162    fn poll_ready<F: std::future::Future>(future: F) -> F::Output {
1163        use std::task::{Context, Poll, Waker};
1164
1165        let mut future = std::pin::pin!(future);
1166        let mut task_cx = Context::from_waker(Waker::noop());
1167        match future.as_mut().poll(&mut task_cx) {
1168            Poll::Ready(output) => output,
1169            Poll::Pending => panic!("test future unexpectedly yielded"),
1170        }
1171    }
1172
1173    /// The async data path uses static dispatch so each backend can expose a
1174    /// concrete future without boxing page I/O.
1175    #[test]
1176    fn vfs_file_supports_static_dispatch_without_boxing() {
1177        fn accepts_static<T: VfsFile>(_file: &T) {}
1178
1179        let cx = Cx::new();
1180        let vfs = crate::memory::MemoryVfs::new();
1181        let (file, _) = vfs
1182            .open(&cx, None, VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE)
1183            .expect("open memory file");
1184        accepts_static(&file);
1185    }
1186
1187    /// Verify default implementations exist and don't panic.
1188    #[test]
1189    fn vfs_file_defaults() {
1190        struct DummyFile;
1191        impl VfsFile for DummyFile {
1192            fn close(&mut self, _cx: &Cx) -> Result<()> {
1193                Ok(())
1194            }
1195            async fn read(&self, _cx: &Cx, _buf: &mut [u8], _offset: u64) -> Result<usize> {
1196                Ok(0)
1197            }
1198            async fn write(&self, _cx: &Cx, _buf: &[u8], _offset: u64) -> Result<()> {
1199                Ok(())
1200            }
1201            fn truncate(&mut self, _cx: &Cx, _size: u64) -> Result<()> {
1202                Ok(())
1203            }
1204            fn sync(&mut self, _cx: &Cx, _flags: SyncFlags) -> Result<()> {
1205                Ok(())
1206            }
1207            fn file_size(&self, _cx: &Cx) -> Result<u64> {
1208                Ok(0)
1209            }
1210            fn lock(&mut self, _cx: &Cx, _level: LockLevel) -> Result<()> {
1211                Ok(())
1212            }
1213            fn unlock(&mut self, _cx: &Cx, _level: LockLevel) -> Result<()> {
1214                Ok(())
1215            }
1216            fn lock_external_shared_snapshot(&mut self, _: &Cx) -> Result<()> {
1217                Err(fsqlite_error::FrankenError::Unsupported)
1218            }
1219            fn restore_external_shared_snapshot_attempt(&mut self, _: &Cx) -> Result<()> {
1220                Ok(())
1221            }
1222            fn lock_external_maintenance(&mut self, _: &Cx, _: bool) -> Result<()> {
1223                Err(fsqlite_error::FrankenError::Unsupported)
1224            }
1225            fn restore_external_maintenance_attempt(&mut self, _: &Cx) -> Result<()> {
1226                Ok(())
1227            }
1228            fn check_reserved_lock(&self, _cx: &Cx) -> Result<bool> {
1229                Ok(false)
1230            }
1231            fn shm_map(
1232                &mut self,
1233                _cx: &Cx,
1234                _region: u32,
1235                _size: u32,
1236                _extend: bool,
1237            ) -> Result<ShmRegion> {
1238                Err(fsqlite_error::FrankenError::Unsupported)
1239            }
1240            fn shm_lock(&mut self, _cx: &Cx, _offset: u32, _n: u32, _flags: u32) -> Result<()> {
1241                Err(fsqlite_error::FrankenError::Unsupported)
1242            }
1243            fn shm_barrier(&self) {}
1244            fn shm_unmap(&mut self, _cx: &Cx, _delete: bool) -> Result<()> {
1245                Ok(())
1246            }
1247        }
1248
1249        let cx = Cx::new();
1250        let mut file = DummyFile;
1251        assert_eq!(file.sector_size(), 4096);
1252        assert_eq!(file.device_characteristics(), 0);
1253        assert!(matches!(
1254            file.lock_external_shared_snapshot(&cx),
1255            Err(fsqlite_error::FrankenError::Unsupported)
1256        ));
1257        file.restore_external_shared_snapshot_attempt(&cx)
1258            .expect("restoring an unsupported snapshot attempt is idempotent");
1259        assert!(matches!(
1260            file.lock_external_maintenance(&cx, true),
1261            Err(fsqlite_error::FrankenError::Unsupported)
1262        ));
1263        file.restore_external_maintenance_attempt(&cx)
1264            .expect("restoring an unsupported maintenance attempt is idempotent");
1265    }
1266
1267    /// Verify that VfsFile trait defaults are what we expect.
1268    #[test]
1269    fn vfs_file_sector_size_default_is_4096() {
1270        struct Stub;
1271        impl VfsFile for Stub {
1272            fn close(&mut self, _: &Cx) -> Result<()> {
1273                Ok(())
1274            }
1275            async fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1276                Ok(0)
1277            }
1278            async fn write(&self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1279                Ok(())
1280            }
1281            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1282                Ok(())
1283            }
1284            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1285                Ok(())
1286            }
1287            fn file_size(&self, _: &Cx) -> Result<u64> {
1288                Ok(0)
1289            }
1290            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1291                Ok(())
1292            }
1293            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1294                Ok(())
1295            }
1296            fn lock_external_shared_snapshot(&mut self, _: &Cx) -> Result<()> {
1297                Err(fsqlite_error::FrankenError::Unsupported)
1298            }
1299            fn restore_external_shared_snapshot_attempt(&mut self, _: &Cx) -> Result<()> {
1300                Ok(())
1301            }
1302            fn lock_external_maintenance(&mut self, _: &Cx, _: bool) -> Result<()> {
1303                Err(fsqlite_error::FrankenError::Unsupported)
1304            }
1305            fn restore_external_maintenance_attempt(&mut self, _: &Cx) -> Result<()> {
1306                Ok(())
1307            }
1308            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1309                Ok(false)
1310            }
1311            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1312                Err(fsqlite_error::FrankenError::Unsupported)
1313            }
1314            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1315                Err(fsqlite_error::FrankenError::Unsupported)
1316            }
1317            fn shm_barrier(&self) {}
1318            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1319                Ok(())
1320            }
1321        }
1322
1323        let file = Stub;
1324        assert_eq!(file.sector_size(), 4096);
1325        assert_eq!(file.device_characteristics(), 0);
1326    }
1327
1328    /// Verify that default Vfs::randomness produces different sequences.
1329    #[test]
1330    fn vfs_default_randomness_varies() {
1331        use crate::memory::MemoryVfs;
1332        use crate::traits::Vfs;
1333
1334        let cx = Cx::new();
1335        let vfs = MemoryVfs::new();
1336        let mut buf1 = [0u8; 32];
1337        let mut buf2 = [0u8; 32];
1338        vfs.randomness(&cx, &mut buf1);
1339        vfs.randomness(&cx, &mut buf2);
1340        assert_ne!(buf1, buf2);
1341    }
1342
1343    /// Verify that default Vfs::current_time reads from Cx.
1344    #[test]
1345    fn vfs_default_current_time_from_cx() {
1346        use crate::memory::MemoryVfs;
1347        use crate::traits::Vfs;
1348
1349        let cx = Cx::new();
1350        cx.set_unix_millis_for_testing(0);
1351        let vfs = MemoryVfs::new();
1352        let t1 = vfs.current_time(&cx);
1353        // Unix epoch in Julian days is 2440587.5
1354        #[allow(clippy::approx_constant)]
1355        let expected = 2_440_587.5;
1356        assert!(
1357            (t1 - expected).abs() < 1e-6,
1358            "at unix epoch, julian day should be ~2440587.5, got {t1}"
1359        );
1360    }
1361
1362    /// Verify randomness with a zero-length buffer doesn't panic.
1363    #[test]
1364    fn vfs_randomness_zero_length_buffer() {
1365        use crate::memory::MemoryVfs;
1366        use crate::traits::Vfs;
1367
1368        let cx = Cx::new();
1369        let vfs = MemoryVfs::new();
1370        let mut buf = [];
1371        vfs.randomness(&cx, &mut buf);
1372    }
1373
1374    /// Verify randomness with a 1-byte buffer.
1375    #[test]
1376    fn vfs_randomness_single_byte() {
1377        use crate::memory::MemoryVfs;
1378        use crate::traits::Vfs;
1379
1380        let cx = Cx::new();
1381        let vfs = MemoryVfs::new();
1382        let mut buf = [0u8; 1];
1383        vfs.randomness(&cx, &mut buf);
1384        // Can't assert much about the value, just that it doesn't panic.
1385    }
1386
1387    #[test]
1388    fn vfs_is_memory_default_is_false() {
1389        use crate::memory::MemoryVfs;
1390        use crate::traits::Vfs;
1391
1392        let vfs = MemoryVfs::new();
1393        assert!(vfs.is_memory(), "MemoryVfs::is_memory must return true");
1394    }
1395
1396    #[test]
1397    fn vfs_trait_is_object_safe() {
1398        use crate::memory::MemoryVfs;
1399        fn _accepts_dyn(_v: &dyn Vfs<File = crate::memory::MemoryFile>) {}
1400        let _vfs = MemoryVfs::new();
1401    }
1402
1403    #[test]
1404    fn vfs_file_set_busy_timeout_is_noop() {
1405        struct Stub;
1406        impl VfsFile for Stub {
1407            fn close(&mut self, _: &Cx) -> Result<()> {
1408                Ok(())
1409            }
1410            async fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1411                Ok(0)
1412            }
1413            async fn write(&self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1414                Ok(())
1415            }
1416            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1417                Ok(())
1418            }
1419            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1420                Ok(())
1421            }
1422            fn file_size(&self, _: &Cx) -> Result<u64> {
1423                Ok(0)
1424            }
1425            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1426                Ok(())
1427            }
1428            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1429                Ok(())
1430            }
1431            fn lock_external_shared_snapshot(&mut self, _: &Cx) -> Result<()> {
1432                Err(fsqlite_error::FrankenError::Unsupported)
1433            }
1434            fn restore_external_shared_snapshot_attempt(&mut self, _: &Cx) -> Result<()> {
1435                Ok(())
1436            }
1437            fn lock_external_maintenance(&mut self, _: &Cx, _: bool) -> Result<()> {
1438                Err(fsqlite_error::FrankenError::Unsupported)
1439            }
1440            fn restore_external_maintenance_attempt(&mut self, _: &Cx) -> Result<()> {
1441                Ok(())
1442            }
1443            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1444                Ok(false)
1445            }
1446            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1447                Err(fsqlite_error::FrankenError::Unsupported)
1448            }
1449            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1450                Err(fsqlite_error::FrankenError::Unsupported)
1451            }
1452            fn shm_barrier(&self) {}
1453            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1454                Ok(())
1455            }
1456        }
1457
1458        let mut file = Stub;
1459        file.set_busy_timeout_ms(5000);
1460        file.set_busy_timeout_ms(0);
1461    }
1462
1463    #[test]
1464    fn vfs_file_write_page_batch_default_delegates_to_write() {
1465        use std::sync::atomic::{AtomicUsize, Ordering};
1466
1467        static WRITE_COUNT: AtomicUsize = AtomicUsize::new(0);
1468
1469        struct CountingFile;
1470        impl VfsFile for CountingFile {
1471            fn close(&mut self, _: &Cx) -> Result<()> {
1472                Ok(())
1473            }
1474            async fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1475                Ok(0)
1476            }
1477            async fn write(&self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1478                WRITE_COUNT.fetch_add(1, Ordering::Relaxed);
1479                Ok(())
1480            }
1481            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1482                Ok(())
1483            }
1484            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1485                Ok(())
1486            }
1487            fn file_size(&self, _: &Cx) -> Result<u64> {
1488                Ok(0)
1489            }
1490            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1491                Ok(())
1492            }
1493            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1494                Ok(())
1495            }
1496            fn lock_external_shared_snapshot(&mut self, _: &Cx) -> Result<()> {
1497                Err(fsqlite_error::FrankenError::Unsupported)
1498            }
1499            fn restore_external_shared_snapshot_attempt(&mut self, _: &Cx) -> Result<()> {
1500                Ok(())
1501            }
1502            fn lock_external_maintenance(&mut self, _: &Cx, _: bool) -> Result<()> {
1503                Err(fsqlite_error::FrankenError::Unsupported)
1504            }
1505            fn restore_external_maintenance_attempt(&mut self, _: &Cx) -> Result<()> {
1506                Ok(())
1507            }
1508            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1509                Ok(false)
1510            }
1511            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1512                Err(fsqlite_error::FrankenError::Unsupported)
1513            }
1514            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1515                Err(fsqlite_error::FrankenError::Unsupported)
1516            }
1517            fn shm_barrier(&self) {}
1518            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1519                Ok(())
1520            }
1521        }
1522
1523        WRITE_COUNT.store(0, Ordering::Relaxed);
1524        let cx = Cx::new();
1525        let file = CountingFile;
1526        let data = [0u8; 4096];
1527        let writes: Vec<(u64, &[u8])> = vec![(0, &data), (4096, &data), (8192, &data)];
1528        poll_ready(file.write_page_batch(&cx, &writes)).unwrap();
1529        assert_eq!(WRITE_COUNT.load(Ordering::Relaxed), 3);
1530    }
1531
1532    #[test]
1533    fn vfs_randomness_fills_large_buffer() {
1534        use crate::memory::MemoryVfs;
1535        use crate::traits::Vfs;
1536
1537        let cx = Cx::new();
1538        let vfs = MemoryVfs::new();
1539        let mut buf = [0u8; 256];
1540        vfs.randomness(&cx, &mut buf);
1541        let all_zero = buf.iter().all(|&b| b == 0);
1542        assert!(
1543            !all_zero,
1544            "256-byte randomness buffer should not be all zeros"
1545        );
1546    }
1547
1548    #[test]
1549    fn vfs_randomness_non_aligned_buffer() {
1550        use crate::memory::MemoryVfs;
1551        use crate::traits::Vfs;
1552
1553        let cx = Cx::new();
1554        let vfs = MemoryVfs::new();
1555        let mut buf = [0u8; 13];
1556        vfs.randomness(&cx, &mut buf);
1557        let all_zero = buf.iter().all(|&b| b == 0);
1558        assert!(!all_zero, "13-byte non-aligned buffer should be filled");
1559    }
1560
1561    #[test]
1562    fn vfs_write_page_batch_empty_is_noop() {
1563        struct Stub;
1564        impl VfsFile for Stub {
1565            fn close(&mut self, _: &Cx) -> Result<()> {
1566                Ok(())
1567            }
1568            async fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1569                Ok(0)
1570            }
1571            async fn write(&self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1572                panic!("write should not be called for empty batch");
1573            }
1574            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1575                Ok(())
1576            }
1577            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1578                Ok(())
1579            }
1580            fn file_size(&self, _: &Cx) -> Result<u64> {
1581                Ok(0)
1582            }
1583            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1584                Ok(())
1585            }
1586            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1587                Ok(())
1588            }
1589            fn lock_external_shared_snapshot(&mut self, _: &Cx) -> Result<()> {
1590                Err(fsqlite_error::FrankenError::Unsupported)
1591            }
1592            fn restore_external_shared_snapshot_attempt(&mut self, _: &Cx) -> Result<()> {
1593                Ok(())
1594            }
1595            fn lock_external_maintenance(&mut self, _: &Cx, _: bool) -> Result<()> {
1596                Err(fsqlite_error::FrankenError::Unsupported)
1597            }
1598            fn restore_external_maintenance_attempt(&mut self, _: &Cx) -> Result<()> {
1599                Ok(())
1600            }
1601            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1602                Ok(false)
1603            }
1604            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1605                Err(fsqlite_error::FrankenError::Unsupported)
1606            }
1607            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1608                Err(fsqlite_error::FrankenError::Unsupported)
1609            }
1610            fn shm_barrier(&self) {}
1611            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1612                Ok(())
1613            }
1614        }
1615
1616        let cx = Cx::new();
1617        let file = Stub;
1618        let writes: Vec<(u64, &[u8])> = vec![];
1619        poll_ready(file.write_page_batch(&cx, &writes)).unwrap();
1620    }
1621
1622    #[test]
1623    fn memory_vfs_name_is_memory() {
1624        use crate::memory::MemoryVfs;
1625        use crate::traits::Vfs;
1626
1627        let vfs = MemoryVfs::new();
1628        assert_eq!(vfs.name(), "memory");
1629    }
1630
1631    #[test]
1632    fn vfs_current_time_advances_with_unix_millis() {
1633        use crate::memory::MemoryVfs;
1634        use crate::traits::Vfs;
1635
1636        let cx = Cx::new();
1637        let vfs = MemoryVfs::new();
1638        cx.set_unix_millis_for_testing(0);
1639        let t0 = vfs.current_time(&cx);
1640        cx.set_unix_millis_for_testing(86_400_000);
1641        let t1 = vfs.current_time(&cx);
1642        let delta = t1 - t0;
1643        assert!(
1644            (delta - 1.0).abs() < 1e-6,
1645            "86400000ms = 1 Julian day, got delta {delta}"
1646        );
1647    }
1648
1649    #[test]
1650    fn write_page_batch_short_circuits_on_error() {
1651        use std::sync::atomic::{AtomicUsize, Ordering};
1652
1653        static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
1654
1655        struct FailOnSecond;
1656        impl VfsFile for FailOnSecond {
1657            fn close(&mut self, _: &Cx) -> Result<()> {
1658                Ok(())
1659            }
1660            async fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1661                Ok(0)
1662            }
1663            async fn write(&self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1664                let n = CALL_COUNT.fetch_add(1, Ordering::Relaxed);
1665                if n >= 1 {
1666                    return Err(fsqlite_error::FrankenError::Io(std::io::Error::other(
1667                        "injected",
1668                    )));
1669                }
1670                Ok(())
1671            }
1672            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1673                Ok(())
1674            }
1675            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1676                Ok(())
1677            }
1678            fn file_size(&self, _: &Cx) -> Result<u64> {
1679                Ok(0)
1680            }
1681            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1682                Ok(())
1683            }
1684            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1685                Ok(())
1686            }
1687            fn lock_external_shared_snapshot(&mut self, _: &Cx) -> Result<()> {
1688                Err(fsqlite_error::FrankenError::Unsupported)
1689            }
1690            fn restore_external_shared_snapshot_attempt(&mut self, _: &Cx) -> Result<()> {
1691                Ok(())
1692            }
1693            fn lock_external_maintenance(&mut self, _: &Cx, _: bool) -> Result<()> {
1694                Err(fsqlite_error::FrankenError::Unsupported)
1695            }
1696            fn restore_external_maintenance_attempt(&mut self, _: &Cx) -> Result<()> {
1697                Ok(())
1698            }
1699            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1700                Ok(false)
1701            }
1702            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1703                Err(fsqlite_error::FrankenError::Unsupported)
1704            }
1705            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1706                Err(fsqlite_error::FrankenError::Unsupported)
1707            }
1708            fn shm_barrier(&self) {}
1709            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1710                Ok(())
1711            }
1712        }
1713
1714        CALL_COUNT.store(0, Ordering::Relaxed);
1715        let cx = Cx::new();
1716        let file = FailOnSecond;
1717        let data = [0u8; 64];
1718        let writes: Vec<(u64, &[u8])> = vec![(0, &data), (64, &data), (128, &data)];
1719        let result = poll_ready(file.write_page_batch(&cx, &writes));
1720        assert!(result.is_err());
1721        assert_eq!(
1722            CALL_COUNT.load(Ordering::Relaxed),
1723            2,
1724            "should stop after second write fails, not call third"
1725        );
1726    }
1727
1728    #[test]
1729    fn vfs_file_defaults_can_be_overridden() {
1730        struct CustomFile;
1731        impl VfsFile for CustomFile {
1732            fn close(&mut self, _: &Cx) -> Result<()> {
1733                Ok(())
1734            }
1735            async fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1736                Ok(0)
1737            }
1738            async fn write(&self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1739                Ok(())
1740            }
1741            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1742                Ok(())
1743            }
1744            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1745                Ok(())
1746            }
1747            fn file_size(&self, _: &Cx) -> Result<u64> {
1748                Ok(0)
1749            }
1750            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1751                Ok(())
1752            }
1753            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1754                Ok(())
1755            }
1756            fn lock_external_shared_snapshot(&mut self, _: &Cx) -> Result<()> {
1757                Err(fsqlite_error::FrankenError::Unsupported)
1758            }
1759            fn restore_external_shared_snapshot_attempt(&mut self, _: &Cx) -> Result<()> {
1760                Ok(())
1761            }
1762            fn lock_external_maintenance(&mut self, _: &Cx, _: bool) -> Result<()> {
1763                Err(fsqlite_error::FrankenError::Unsupported)
1764            }
1765            fn restore_external_maintenance_attempt(&mut self, _: &Cx) -> Result<()> {
1766                Ok(())
1767            }
1768            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1769                Ok(false)
1770            }
1771            fn sector_size(&self) -> u32 {
1772                512
1773            }
1774            fn device_characteristics(&self) -> u32 {
1775                0x0010
1776            }
1777            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1778                Err(fsqlite_error::FrankenError::Unsupported)
1779            }
1780            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1781                Err(fsqlite_error::FrankenError::Unsupported)
1782            }
1783            fn shm_barrier(&self) {}
1784            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1785                Ok(())
1786            }
1787        }
1788
1789        let file = CustomFile;
1790        assert_eq!(file.sector_size(), 512);
1791        assert_eq!(file.device_characteristics(), 0x0010);
1792    }
1793
1794    #[test]
1795    fn vfs_randomness_has_byte_level_entropy() {
1796        use crate::memory::MemoryVfs;
1797        use crate::traits::Vfs;
1798
1799        let cx = Cx::new();
1800        let vfs = MemoryVfs::new();
1801        let mut buf = [0u8; 64];
1802        vfs.randomness(&cx, &mut buf);
1803        let distinct: std::collections::HashSet<u8> = buf.iter().copied().collect();
1804        assert!(
1805            distinct.len() > 4,
1806            "64-byte buffer should have more than 4 distinct byte values, got {}",
1807            distinct.len()
1808        );
1809    }
1810
1811    #[test]
1812    fn vfs_current_time_default_returns_reasonable_julian_day() {
1813        use crate::memory::MemoryVfs;
1814        use crate::traits::Vfs;
1815
1816        let cx = Cx::new();
1817        let vfs = MemoryVfs::new();
1818        let jd = vfs.current_time(&cx);
1819        assert!(jd.is_finite(), "Julian day must be finite");
1820        assert!(
1821            jd > 2_440_000.0,
1822            "Julian day should be after ~1968, got {jd}"
1823        );
1824    }
1825
1826    #[test]
1827    fn durable_sync_default_delegates_to_sync() {
1828        use std::sync::atomic::{AtomicU8, Ordering};
1829        static LAST_FLAGS: AtomicU8 = AtomicU8::new(0);
1830
1831        struct RecordingFile;
1832        impl VfsFile for RecordingFile {
1833            fn close(&mut self, _: &Cx) -> Result<()> {
1834                Ok(())
1835            }
1836            async fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1837                Ok(0)
1838            }
1839            async fn write(&self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1840                Ok(())
1841            }
1842            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1843                Ok(())
1844            }
1845            fn sync(&mut self, _: &Cx, flags: SyncFlags) -> Result<()> {
1846                LAST_FLAGS.store(flags.bits(), Ordering::Relaxed);
1847                Ok(())
1848            }
1849            fn file_size(&self, _: &Cx) -> Result<u64> {
1850                Ok(0)
1851            }
1852            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1853                Ok(())
1854            }
1855            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1856                Ok(())
1857            }
1858            fn lock_external_shared_snapshot(&mut self, _: &Cx) -> Result<()> {
1859                Err(fsqlite_error::FrankenError::Unsupported)
1860            }
1861            fn restore_external_shared_snapshot_attempt(&mut self, _: &Cx) -> Result<()> {
1862                Ok(())
1863            }
1864            fn lock_external_maintenance(&mut self, _: &Cx, _: bool) -> Result<()> {
1865                Err(fsqlite_error::FrankenError::Unsupported)
1866            }
1867            fn restore_external_maintenance_attempt(&mut self, _: &Cx) -> Result<()> {
1868                Ok(())
1869            }
1870            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1871                Ok(false)
1872            }
1873            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1874                Err(fsqlite_error::FrankenError::Unsupported)
1875            }
1876            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1877                Err(fsqlite_error::FrankenError::Unsupported)
1878            }
1879            fn shm_barrier(&self) {}
1880            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1881                Ok(())
1882            }
1883        }
1884
1885        let cx = Cx::new();
1886        let mut f = RecordingFile;
1887
1888        f.durable_sync(&cx, SyncKind::DataOnly).unwrap();
1889        assert_eq!(
1890            LAST_FLAGS.load(Ordering::Relaxed),
1891            SyncFlags::DATAONLY.bits()
1892        );
1893
1894        f.durable_sync(&cx, SyncKind::FullDurable).unwrap();
1895        assert_eq!(LAST_FLAGS.load(Ordering::Relaxed), SyncFlags::FULL.bits());
1896
1897        f.durable_sync(&cx, SyncKind::DataAndMetadata).unwrap();
1898        assert_eq!(LAST_FLAGS.load(Ordering::Relaxed), SyncFlags::FULL.bits());
1899    }
1900
1901    #[test]
1902    fn sync_kind_variants_are_distinct() {
1903        assert_ne!(SyncKind::DataOnly, SyncKind::DataAndMetadata);
1904        assert_ne!(SyncKind::DataAndMetadata, SyncKind::FullDurable);
1905        assert_ne!(SyncKind::DataOnly, SyncKind::FullDurable);
1906    }
1907
1908    #[test]
1909    fn vfs_file_async_data_path_is_implementable() {
1910        use crate::memory::MemoryFile;
1911
1912        fn assert_impl<T: VfsFile>() {}
1913        assert_impl::<MemoryFile>();
1914    }
1915
1916    #[test]
1917    fn write_completion_wait_is_cancel_safe_and_terminal_state_is_sticky() {
1918        use std::future::Future as _;
1919
1920        let completion = VfsWriteCompletion::new();
1921        let mut first_waiter = Box::pin(completion.wait());
1922        let mut task_cx = Context::from_waker(Waker::noop());
1923        assert!(matches!(
1924            first_waiter.as_mut().poll(&mut task_cx),
1925            Poll::Pending
1926        ));
1927        assert_eq!(
1928            completion
1929                .inner
1930                .lock()
1931                .unwrap_or_else(std::sync::PoisonError::into_inner)
1932                .waiters
1933                .len(),
1934            1
1935        );
1936        drop(first_waiter);
1937        assert!(
1938            completion
1939                .inner
1940                .lock()
1941                .unwrap_or_else(std::sync::PoisonError::into_inner)
1942                .waiters
1943                .is_empty(),
1944            "dropping a wait future must unregister it"
1945        );
1946
1947        assert!(completion.complete_success());
1948        assert!(!completion.complete_error());
1949        assert_eq!(completion.state(), VfsWriteCompletionState::Success);
1950        assert_eq!(
1951            poll_ready(completion.wait()),
1952            VfsWriteCompletionState::Success
1953        );
1954
1955        let faulted_write = VfsWriteCompletion::new();
1956        let partial_source = faulted_write.error_mapped_child();
1957        assert!(partial_source.complete_success());
1958        assert_eq!(
1959            faulted_write.state(),
1960            VfsWriteCompletionState::Error,
1961            "a completed partial-write source must map to outer Error"
1962        );
1963    }
1964
1965    #[test]
1966    fn async_memory_write_completion_is_immediate_and_bytes_are_real() {
1967        use std::future::Future as _;
1968        use std::task::{Context, Poll, Waker};
1969
1970        use crate::memory::{MemoryFile, MemoryVfs};
1971
1972        let cx = Cx::new();
1973        let vfs = MemoryVfs::new();
1974        let flags = fsqlite_types::flags::VfsOpenFlags::MAIN_DB
1975            | fsqlite_types::flags::VfsOpenFlags::CREATE
1976            | fsqlite_types::flags::VfsOpenFlags::READWRITE;
1977        let (file, _) = vfs.open(&cx, None, flags).unwrap();
1978
1979        let payload = b"hello async vfs";
1980        let waker = Waker::noop();
1981        let mut task_cx = Context::from_waker(waker);
1982        {
1983            let completion = VfsWriteCompletion::new();
1984            let mut write = std::pin::pin!(<MemoryFile as VfsFile>::write_tracked(
1985                &file,
1986                &cx,
1987                payload,
1988                0,
1989                completion.clone(),
1990            ));
1991            assert!(matches!(
1992                write.as_mut().poll(&mut task_cx),
1993                Poll::Ready(Ok(()))
1994            ));
1995            assert_eq!(
1996                completion.state(),
1997                VfsWriteCompletionState::Success,
1998                "the immediate memory mutation source must complete the token before Ready"
1999            );
2000        }
2001
2002        let mut buf = [0u8; 15];
2003        {
2004            let mut read = std::pin::pin!(<MemoryFile as VfsFile>::read(&file, &cx, &mut buf, 0));
2005            assert!(matches!(
2006                read.as_mut().poll(&mut task_cx),
2007                Poll::Ready(Ok(15))
2008            ));
2009        }
2010        assert_eq!(&buf, payload);
2011
2012        let writes: &[(u64, &[u8])] = &[(0, b"real"), (8, b"batch")];
2013        {
2014            let mut batch = std::pin::pin!(<MemoryFile as VfsFile>::write_page_batch(
2015                &file, &cx, writes,
2016            ));
2017            assert!(matches!(
2018                batch.as_mut().poll(&mut task_cx),
2019                Poll::Ready(Ok(()))
2020            ));
2021        }
2022
2023        let mut batch_buf = [0_u8; 13];
2024        {
2025            let mut verify =
2026                std::pin::pin!(<MemoryFile as VfsFile>::read(&file, &cx, &mut batch_buf, 0,));
2027            assert!(matches!(
2028                verify.as_mut().poll(&mut task_cx),
2029                Poll::Ready(Ok(13))
2030            ));
2031        }
2032        assert_eq!(&batch_buf, b"realo asbatch");
2033    }
2034}