Skip to main content

fsqlite_vfs/
traits.rs

1use std::path::{Path, PathBuf};
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use fsqlite_error::{FrankenError, Result};
5use fsqlite_types::LockLevel;
6use fsqlite_types::cx::Cx;
7use fsqlite_types::flags::{AccessFlags, SyncFlags, VfsOpenFlags};
8
9use crate::shm::{
10    SQLITE_SHM_EXCLUSIVE, SQLITE_SHM_LOCK, SQLITE_SHM_UNLOCK, ShmRegion, WAL_CKPT_LOCK,
11    WAL_WRITE_LOCK,
12};
13
14/// Opaque identity of an already-open filesystem object.
15///
16/// Identities are intended only as opaque comparison keys while the relevant
17/// file handles remain open. Their ordering carries no filesystem meaning;
18/// it exists only for ordered collections. They are not persistent database
19/// identifiers and must not be serialized or compared across machines or
20/// boots.
21#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct FileIdentity {
23    kind: FileIdentityKind,
24    namespace: u64,
25    object: [u8; 16],
26}
27
28/// Representation domain for an opaque [`FileIdentity`].
29///
30/// The discriminator is part of equality and ordering so a legacy Windows
31/// 64-bit file index can never compare equal to a 128-bit `FILE_ID_INFO`
32/// value that happens to contain the same bytes.
33#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34enum FileIdentityKind {
35    /// Stable identity for one file generation inside an in-process VFS.
36    Memory,
37    #[cfg(unix)]
38    Unix,
39    #[cfg(windows)]
40    WindowsFileId128,
41    #[cfg(windows)]
42    WindowsFileIndex64,
43}
44
45impl FileIdentity {
46    /// Construct an identity for a file owned by an in-process VFS.
47    ///
48    /// Both components are opaque process-local tokens. They only need to be
49    /// stable while the corresponding VFS/file storage objects are alive; the
50    /// identity is used to coalesce pager coordination gates, never persisted.
51    #[must_use]
52    pub(crate) fn from_memory_parts(namespace: u64, object: u64) -> Self {
53        let mut object_bytes = [0_u8; 16];
54        object_bytes[..8].copy_from_slice(&object.to_ne_bytes());
55        Self {
56            kind: FileIdentityKind::Memory,
57            namespace,
58            object: object_bytes,
59        }
60    }
61
62    /// Read the identity of an independently opened filesystem descriptor.
63    ///
64    /// On Unix this uses descriptor metadata (`st_dev`, `st_ino`). On Windows
65    /// it uses the volume serial number and full 128-bit file identifier
66    /// associated with the open handle. A later rename or pathname replacement
67    /// therefore does not change the result. Platforms without a stable
68    /// descriptor identity exposed by this crate return `Ok(None)`.
69    #[cfg(not(target_arch = "wasm32"))]
70    pub fn from_file(file: &std::fs::File) -> std::io::Result<Option<Self>> {
71        #[cfg(unix)]
72        {
73            use std::os::unix::fs::MetadataExt as _;
74
75            let metadata = file.metadata()?;
76            Ok(Some(Self::from_unix_parts(metadata.dev(), metadata.ino())))
77        }
78
79        #[cfg(windows)]
80        {
81            use std::os::windows::io::AsRawHandle as _;
82
83            let handle = file.as_raw_handle();
84            let file_id_result = query_windows_file_id(handle);
85            Self::from_windows_query_result(file_id_result, || {
86                query_windows_legacy_file_index(handle)
87            })
88        }
89
90        #[cfg(not(any(unix, windows)))]
91        {
92            let _ = file;
93            Ok(None)
94        }
95    }
96
97    #[cfg(unix)]
98    pub(crate) fn from_unix_parts(device: u64, inode: u64) -> Self {
99        let mut object = [0_u8; 16];
100        object[..8].copy_from_slice(&inode.to_be_bytes());
101        Self {
102            kind: FileIdentityKind::Unix,
103            namespace: device,
104            object,
105        }
106    }
107
108    #[cfg(windows)]
109    fn from_windows_parts(volume_serial_number: u64, file_id: [u8; 16]) -> Option<Self> {
110        // MS-FSCC 2.1.10 reserves all-zero for filesystems without a
111        // 128-bit file ID and all-ones for files whose unique ID cannot be
112        // established. Both values MUST be ignored, so neither can safely
113        // participate in an expected-identity comparison.
114        if file_id.iter().all(|byte| *byte == 0) || file_id.iter().all(|byte| *byte == u8::MAX) {
115            return None;
116        }
117        Some(Self {
118            kind: FileIdentityKind::WindowsFileId128,
119            namespace: volume_serial_number,
120            object: file_id,
121        })
122    }
123
124    #[cfg(windows)]
125    fn from_windows_legacy_parts(
126        volume_serial_number: u32,
127        file_index_high: u32,
128        file_index_low: u32,
129    ) -> Self {
130        let file_index = (u64::from(file_index_high) << 32) | u64::from(file_index_low);
131        let mut object = [0_u8; 16];
132        object[..8].copy_from_slice(&file_index.to_be_bytes());
133        Self {
134            kind: FileIdentityKind::WindowsFileIndex64,
135            namespace: u64::from(volume_serial_number),
136            object,
137        }
138    }
139
140    #[cfg(windows)]
141    fn from_windows_query_result<F>(
142        file_id_result: std::io::Result<(u64, [u8; 16])>,
143        legacy_query: F,
144    ) -> std::io::Result<Option<Self>>
145    where
146        F: FnOnce() -> std::io::Result<(u32, u32, u32)>,
147    {
148        match file_id_result {
149            Ok((volume_serial_number, file_id)) => {
150                Ok(Self::from_windows_parts(volume_serial_number, file_id))
151            }
152            Err(err) if is_windows_file_id_unsupported(&err) => {
153                let (volume_serial_number, file_index_high, file_index_low) = legacy_query()?;
154                Ok(Some(Self::from_windows_legacy_parts(
155                    volume_serial_number,
156                    file_index_high,
157                    file_index_low,
158                )))
159            }
160            Err(err) => Err(err),
161        }
162    }
163
164    /// Encode this identity for a namespace record.
165    ///
166    /// Byte 0 is the representation tag, bytes 1..9 are the big-endian
167    /// namespace, and bytes 9..25 are the exact object identifier.
168    #[cfg(any(unix, windows))]
169    pub(crate) fn to_namespace_bytes(self) -> [u8; 25] {
170        let tag = match self.kind {
171            // Memory identities are process-local. The tag round-trips for
172            // in-process coordination/tests only; callers must never persist
173            // it as a reusable cross-process namespace identity.
174            FileIdentityKind::Memory => 4,
175            #[cfg(unix)]
176            FileIdentityKind::Unix => 1,
177            #[cfg(windows)]
178            FileIdentityKind::WindowsFileId128 => 2,
179            #[cfg(windows)]
180            FileIdentityKind::WindowsFileIndex64 => 3,
181        };
182        let mut encoded = [0_u8; 25];
183        encoded[0] = tag;
184        encoded[1..9].copy_from_slice(&self.namespace.to_be_bytes());
185        encoded[9..].copy_from_slice(&self.object);
186        encoded
187    }
188
189    /// Decode and validate a namespace-record identity.
190    #[cfg(any(unix, windows))]
191    pub(crate) fn from_namespace_bytes(encoded: [u8; 25]) -> Option<Self> {
192        let mut namespace_bytes = [0_u8; 8];
193        namespace_bytes.copy_from_slice(&encoded[1..9]);
194        let namespace = u64::from_be_bytes(namespace_bytes);
195        let mut object = [0_u8; 16];
196        object.copy_from_slice(&encoded[9..]);
197
198        match encoded[0] {
199            4 if object[8..].iter().all(|byte| *byte == 0) => Some(Self {
200                kind: FileIdentityKind::Memory,
201                namespace,
202                object,
203            }),
204            #[cfg(unix)]
205            1 if object[8..].iter().all(|byte| *byte == 0) => Some(Self {
206                kind: FileIdentityKind::Unix,
207                namespace,
208                object,
209            }),
210            #[cfg(windows)]
211            2 => Self::from_windows_parts(namespace, object),
212            #[cfg(windows)]
213            3 if u32::try_from(namespace).is_ok() && object[8..].iter().all(|byte| *byte == 0) => {
214                Some(Self {
215                    kind: FileIdentityKind::WindowsFileIndex64,
216                    namespace,
217                    object,
218                })
219            }
220            _ => None,
221        }
222    }
223}
224
225#[cfg(all(test, any(unix, windows)))]
226mod memory_file_identity_codec_tests {
227    use super::FileIdentity;
228
229    #[test]
230    fn memory_namespace_bytes_round_trip_with_distinct_tag() {
231        let identity = FileIdentity::from_memory_parts(17, 29);
232        let encoded = identity.to_namespace_bytes();
233
234        assert_eq!(encoded[0], 4);
235        assert_eq!(FileIdentity::from_namespace_bytes(encoded), Some(identity));
236        assert_ne!(
237            identity,
238            FileIdentity::from_memory_parts(18, 29),
239            "independent MemoryVfs instances must remain isolated"
240        );
241        assert_ne!(
242            identity,
243            FileIdentity::from_memory_parts(17, 30),
244            "distinct named files in one MemoryVfs must remain isolated"
245        );
246    }
247}
248
249#[cfg(windows)]
250fn query_windows_file_id(
251    handle: std::os::windows::io::RawHandle,
252) -> std::io::Result<(u64, [u8; 16])> {
253    use std::mem::size_of;
254    use windows_sys::Win32::Storage::FileSystem::{
255        FILE_ID_INFO, FileIdInfo, GetFileInformationByHandleEx,
256    };
257
258    let mut identity = FILE_ID_INFO::default();
259    let identity_size = u32::try_from(size_of::<FILE_ID_INFO>())
260        .map_err(|_| std::io::Error::other("FILE_ID_INFO size does not fit in a Windows DWORD"))?;
261    // SAFETY: the caller supplies a live Windows file handle, `identity` is a
262    // correctly sized writable `FILE_ID_INFO` buffer, and both remain valid
263    // for the duration of this synchronous system call.
264    let succeeded = unsafe {
265        GetFileInformationByHandleEx(
266            handle,
267            FileIdInfo,
268            std::ptr::from_mut(&mut identity).cast(),
269            identity_size,
270        )
271    };
272    if succeeded == 0 {
273        return Err(std::io::Error::last_os_error());
274    }
275    Ok((identity.VolumeSerialNumber, identity.FileId.Identifier))
276}
277
278#[cfg(windows)]
279fn query_windows_legacy_file_index(
280    handle: std::os::windows::io::RawHandle,
281) -> std::io::Result<(u32, u32, u32)> {
282    use windows_sys::Win32::Storage::FileSystem::{
283        BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
284    };
285
286    let mut identity = BY_HANDLE_FILE_INFORMATION::default();
287    // SAFETY: the caller supplies a live Windows file handle and `identity`
288    // is a correctly sized writable buffer that remains valid for the
289    // duration of this synchronous system call.
290    let succeeded = unsafe { GetFileInformationByHandle(handle, &raw mut identity) };
291    if succeeded == 0 {
292        return Err(std::io::Error::last_os_error());
293    }
294    Ok((
295        identity.dwVolumeSerialNumber,
296        identity.nFileIndexHigh,
297        identity.nFileIndexLow,
298    ))
299}
300
301#[cfg(windows)]
302fn is_windows_file_id_unsupported(err: &std::io::Error) -> bool {
303    use windows_sys::Win32::Foundation::{
304        ERROR_CALL_NOT_IMPLEMENTED, ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER,
305        ERROR_NOT_SUPPORTED,
306    };
307
308    err.raw_os_error().is_some_and(|raw_error| {
309        raw_error == ERROR_INVALID_FUNCTION as i32
310            || raw_error == ERROR_NOT_SUPPORTED as i32
311            || raw_error == ERROR_INVALID_PARAMETER as i32
312            || raw_error == ERROR_CALL_NOT_IMPLEMENTED as i32
313    })
314}
315
316impl std::fmt::Debug for FileIdentity {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        f.write_str("FileIdentity(..)")
319    }
320}
321
322#[cfg(all(test, unix))]
323mod unix_file_identity_codec_tests {
324    use super::FileIdentity;
325
326    #[test]
327    fn unix_namespace_bytes_round_trip_exactly() {
328        let identity = FileIdentity::from_unix_parts(0x0102_0304_0506_0708, 0x1122_3344_5566_7788);
329        let encoded = identity.to_namespace_bytes();
330
331        assert_eq!(encoded[0], 1);
332        assert_eq!(&encoded[1..9], &0x0102_0304_0506_0708_u64.to_be_bytes());
333        assert_eq!(&encoded[9..17], &0x1122_3344_5566_7788_u64.to_be_bytes());
334        assert_eq!(&encoded[17..], &[0_u8; 8]);
335        assert_eq!(FileIdentity::from_namespace_bytes(encoded), Some(identity));
336    }
337
338    #[test]
339    fn unix_namespace_bytes_reject_unknown_and_noncanonical_records() {
340        let identity = FileIdentity::from_unix_parts(7, 11);
341        let mut unknown_tag = identity.to_namespace_bytes();
342        unknown_tag[0] = u8::MAX;
343        assert_eq!(FileIdentity::from_namespace_bytes(unknown_tag), None);
344
345        let mut nonzero_tail = identity.to_namespace_bytes();
346        nonzero_tail[24] = 1;
347        assert_eq!(FileIdentity::from_namespace_bytes(nonzero_tail), None);
348    }
349}
350
351#[cfg(all(test, windows))]
352mod file_identity_tests {
353    use super::FileIdentity;
354    use std::cell::Cell;
355    use std::io;
356    use windows_sys::Win32::Foundation::{
357        ERROR_CALL_NOT_IMPLEMENTED, ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER,
358        ERROR_NOT_SUPPORTED,
359    };
360
361    #[test]
362    fn windows_identity_compares_all_file_id_bits() {
363        let file_id = [0x5a_u8; 16];
364        let mut different_high_byte = file_id;
365        different_high_byte[15] ^= 0xff;
366
367        assert_ne!(
368            FileIdentity::from_windows_parts(7, file_id),
369            FileIdentity::from_windows_parts(7, different_high_byte),
370        );
371    }
372
373    #[test]
374    fn windows_identity_rejects_reserved_sentinels() {
375        assert_eq!(FileIdentity::from_windows_parts(7, [0_u8; 16]), None);
376        assert_eq!(FileIdentity::from_windows_parts(7, [u8::MAX; 16]), None);
377    }
378
379    #[test]
380    fn windows_identity_prefers_full_file_id_without_legacy_query() {
381        let expected = [0x3c_u8; 16];
382        let legacy_queried = Cell::new(false);
383
384        let actual = FileIdentity::from_windows_query_result(Ok((19, expected)), || {
385            legacy_queried.set(true);
386            Ok((19, 0, 7))
387        })
388        .expect("full FileIdInfo result should succeed");
389
390        assert_eq!(actual, FileIdentity::from_windows_parts(19, expected));
391        assert!(!legacy_queried.get());
392    }
393
394    #[test]
395    fn windows_identity_falls_back_for_unsupported_file_id_queries() {
396        for code in [
397            ERROR_INVALID_FUNCTION,
398            ERROR_NOT_SUPPORTED,
399            ERROR_INVALID_PARAMETER,
400            ERROR_CALL_NOT_IMPLEMENTED,
401        ] {
402            let actual = FileIdentity::from_windows_query_result(
403                Err(io::Error::from_raw_os_error(code as i32)),
404                || Ok((23, 0x1122_3344, 0x5566_7788)),
405            )
406            .expect("unsupported FileIdInfo should use the legacy query")
407            .expect("legacy file index should produce an identity");
408
409            assert_eq!(
410                actual,
411                FileIdentity::from_windows_legacy_parts(23, 0x1122_3344, 0x5566_7788)
412            );
413        }
414    }
415
416    #[test]
417    fn windows_identity_does_not_fallback_for_real_io_errors() {
418        let legacy_queried = Cell::new(false);
419        let err =
420            FileIdentity::from_windows_query_result(Err(io::Error::from_raw_os_error(6)), || {
421                legacy_queried.set(true);
422                Ok((1, 2, 3))
423            })
424            .expect_err("invalid handles must remain hard errors");
425
426        assert_eq!(err.raw_os_error(), Some(6));
427        assert!(!legacy_queried.get());
428    }
429
430    #[test]
431    fn windows_identity_separates_full_and_legacy_representation_domains() {
432        let file_index = 0x1122_3344_5566_7788_u64;
433        let mut full_file_id = [0_u8; 16];
434        full_file_id[..8].copy_from_slice(&file_index.to_be_bytes());
435
436        assert_ne!(
437            FileIdentity::from_windows_parts(23, full_file_id).expect("non-sentinel full file ID"),
438            FileIdentity::from_windows_legacy_parts(23, 0x1122_3344, 0x5566_7788),
439        );
440    }
441
442    #[test]
443    fn windows_namespace_bytes_round_trip_both_identity_domains() {
444        let full_object = [0x5a_u8; 16];
445        let full = FileIdentity::from_windows_parts(0x0102_0304_0506_0708, full_object)
446            .expect("non-sentinel full file ID");
447        let full_encoded = full.to_namespace_bytes();
448        assert_eq!(full_encoded[0], 2);
449        assert_eq!(
450            &full_encoded[1..9],
451            &0x0102_0304_0506_0708_u64.to_be_bytes()
452        );
453        assert_eq!(&full_encoded[9..], &full_object);
454        assert_eq!(FileIdentity::from_namespace_bytes(full_encoded), Some(full));
455
456        let legacy = FileIdentity::from_windows_legacy_parts(0x0102_0304, 0x1122_3344, 0x5566_7788);
457        let legacy_encoded = legacy.to_namespace_bytes();
458        assert_eq!(legacy_encoded[0], 3);
459        assert_eq!(&legacy_encoded[1..9], &0x0102_0304_u64.to_be_bytes());
460        assert_eq!(
461            &legacy_encoded[9..17],
462            &0x1122_3344_5566_7788_u64.to_be_bytes()
463        );
464        assert_eq!(&legacy_encoded[17..], &[0_u8; 8]);
465        assert_eq!(
466            FileIdentity::from_namespace_bytes(legacy_encoded),
467            Some(legacy)
468        );
469    }
470
471    #[test]
472    fn windows_namespace_bytes_reject_noncanonical_records() {
473        let mut unknown_tag = [0_u8; 25];
474        unknown_tag[0] = u8::MAX;
475        assert_eq!(FileIdentity::from_namespace_bytes(unknown_tag), None);
476
477        let mut full_zero_sentinel = [0_u8; 25];
478        full_zero_sentinel[0] = 2;
479        assert_eq!(FileIdentity::from_namespace_bytes(full_zero_sentinel), None);
480
481        let mut full_ones_sentinel = [u8::MAX; 25];
482        full_ones_sentinel[0] = 2;
483        assert_eq!(FileIdentity::from_namespace_bytes(full_ones_sentinel), None);
484
485        let legacy = FileIdentity::from_windows_legacy_parts(7, 11, 13);
486        let mut legacy_nonzero_tail = legacy.to_namespace_bytes();
487        legacy_nonzero_tail[24] = 1;
488        assert_eq!(
489            FileIdentity::from_namespace_bytes(legacy_nonzero_tail),
490            None
491        );
492
493        let mut legacy_wide_namespace = legacy.to_namespace_bytes();
494        legacy_wide_namespace[1..9].copy_from_slice(&(u64::from(u32::MAX) + 1).to_be_bytes());
495        assert_eq!(
496            FileIdentity::from_namespace_bytes(legacy_wide_namespace),
497            None
498        );
499    }
500}
501
502/// Durability level for `VfsFile::durable_sync`.
503///
504/// Centralizes per-filesystem sync policy so callers express intent
505/// ("make WAL frames durable") and the VFS maps it to the correct
506/// syscall: fdatasync on ext4/XFS, fsync on btrfs/ZFS, F_FULLFSYNC
507/// on APFS.
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
509pub enum SyncKind {
510    /// Data-only sync (fdatasync on Linux). Sufficient when file size
511    /// and metadata are unchanged (e.g. overwriting existing WAL frames).
512    DataOnly,
513    /// Data + metadata sync (fsync on Linux). Required when new allocation
514    /// blocks must be persisted (file growth, btrfs CoW).
515    DataAndMetadata,
516    /// Full durable barrier: the strongest sync the platform offers.
517    /// On APFS this maps to F_FULLFSYNC; elsewhere identical to
518    /// `DataAndMetadata`. Required for WAL commit frames.
519    FullDurable,
520}
521
522static DEFAULT_RANDOMNESS_CALL_SEQ: AtomicU64 = AtomicU64::new(0);
523
524/// A virtual filesystem implementation.
525///
526/// This trait abstracts all file system operations, allowing different
527/// backends: real files (Unix), in-memory (testing), or custom implementations.
528///
529/// Modeled after C SQLite's `sqlite3_vfs` struct from `os.h`.
530pub trait Vfs: Send + Sync {
531    /// The file handle type produced by this VFS.
532    type File: VfsFile;
533
534    /// The name of this VFS (e.g., "unix", "memory").
535    fn name(&self) -> &'static str;
536
537    /// Open a file.
538    ///
539    /// `path` is `None` for temporary files that should be auto-named.
540    /// `flags` describes what kind of file (main DB, journal, WAL, etc.)
541    /// and how to open it (create, read-write, exclusive, etc.).
542    ///
543    /// Returns the opened file and the flags that were actually used (the VFS
544    /// may add flags like `READWRITE` when `CREATE` is specified).
545    fn open(
546        &self,
547        cx: &Cx,
548        path: Option<&Path>,
549        flags: VfsOpenFlags,
550    ) -> Result<(Self::File, VfsOpenFlags)>;
551
552    /// Open an existing file only if its handle has `expected_identity`.
553    ///
554    /// The default implementation verifies the identity immediately after
555    /// opening. Filesystem backends whose normal open path can mutate related
556    /// artifacts should override this method and perform an earlier,
557    /// side-effect-free identity preflight as well. Because an expected
558    /// identity can only belong to an existing object, `CREATE` and
559    /// `EXCLUSIVE` are stripped before the open.
560    fn open_with_expected_identity(
561        &self,
562        cx: &Cx,
563        path: &Path,
564        flags: VfsOpenFlags,
565        expected_identity: FileIdentity,
566    ) -> Result<(Self::File, VfsOpenFlags)> {
567        let mut existing_flags = flags;
568        existing_flags.remove(VfsOpenFlags::CREATE | VfsOpenFlags::EXCLUSIVE);
569        let (file, actual_flags) = self.open(cx, Some(path), existing_flags)?;
570        if file.file_identity()? != Some(expected_identity) {
571            return Err(fsqlite_error::FrankenError::CannotOpen {
572                path: path.to_owned(),
573            });
574        }
575        Ok((file, actual_flags))
576    }
577
578    /// Open a caller-reserved empty file without creating it or recovering
579    /// any pre-existing database artifacts.
580    ///
581    /// Backends whose ordinary open path creates auxiliary files must
582    /// override this method so the identity, zero-length, and recovery-
583    /// artifact checks all occur before those side effects.
584    fn open_reserved_with_expected_identity(
585        &self,
586        cx: &Cx,
587        path: &Path,
588        flags: VfsOpenFlags,
589        expected_identity: FileIdentity,
590    ) -> Result<(Self::File, VfsOpenFlags)> {
591        let (file, actual_flags) =
592            self.open_with_expected_identity(cx, path, flags, expected_identity)?;
593        if file.file_size(cx)? != 0 {
594            return Err(fsqlite_error::FrankenError::CannotOpen {
595                path: path.to_owned(),
596            });
597        }
598
599        for suffix in ["-journal", "-wal", "-wal-fec", "-shm"] {
600            let mut artifact_path = path.as_os_str().to_owned();
601            artifact_path.push(suffix);
602            if self.path_entry_exists(cx, Path::new(&artifact_path))? {
603                return Err(fsqlite_error::FrankenError::CannotOpen {
604                    path: path.to_owned(),
605                });
606            }
607        }
608        Ok((file, actual_flags))
609    }
610
611    /// Delete a file.
612    ///
613    /// If `sync_dir` is true, the directory entry removal should be synced
614    /// to ensure durability.
615    fn delete(&self, cx: &Cx, path: &Path, sync_dir: bool) -> Result<()>;
616
617    /// Synchronize the parent directory containing `path`.
618    ///
619    /// Durable create-before-mutate protocols (notably rollback journals)
620    /// must make the newly-created directory entry stable before they modify
621    /// the protected file.  Filesystems that do not require or support an
622    /// explicit directory sync may keep the default no-op implementation.
623    fn sync_parent_directory(&self, _cx: &Cx, _path: &Path) -> Result<()> {
624        Ok(())
625    }
626
627    /// Check file access.
628    ///
629    /// Returns true if the file at `path` satisfies the access check
630    /// described by `flags`.
631    fn access(&self, cx: &Cx, path: &Path, flags: AccessFlags) -> Result<bool>;
632
633    /// Return whether a directory entry exists without following its final
634    /// symlink component.
635    ///
636    /// This is stricter than [`Path::exists`] and is required for gates that
637    /// must refuse dangling recovery-artifact symlinks. Virtual filesystems
638    /// without symlink semantics may delegate to [`Self::access`].
639    fn path_entry_exists(&self, cx: &Cx, path: &Path) -> Result<bool> {
640        self.access(cx, path, AccessFlags::EXISTS)
641    }
642
643    /// Resolve a potentially relative path into an absolute path.
644    fn full_pathname(&self, cx: &Cx, path: &Path) -> Result<PathBuf>;
645
646    /// Generate a random byte sequence for temporary file naming.
647    ///
648    /// Fills `buf` with bytes suitable for temporary file naming.
649    ///
650    /// The default implementation is deterministic (xorshift seeded from a
651    /// process-local counter) for reproducible tests; real VFS implementations
652    /// should override this and use OS-provided randomness to avoid collisions.
653    fn randomness(&self, cx: &Cx, buf: &mut [u8]) {
654        // Default: fill with pseudo-random bytes using a simple xorshift.
655        // Real VFS implementations should use OS-provided randomness.
656        let _ = cx; // Usage to silence unused variable warning
657        let seq = DEFAULT_RANDOMNESS_CALL_SEQ.fetch_add(1, Ordering::Relaxed);
658        let mut state: u64 = 0x5DEE_CE66_D1A4_F681 ^ seq.wrapping_mul(0x9E37_79B9_7F4A_7C15);
659        for chunk in buf.chunks_mut(8) {
660            state ^= state << 13;
661            state ^= state >> 7;
662            state ^= state << 17;
663            let bytes = state.to_le_bytes();
664            for (dst, &src) in chunk.iter_mut().zip(bytes.iter()) {
665                *dst = src;
666            }
667        }
668    }
669
670    /// Return the current time as a Julian day number (days since noon
671    /// on November 24, 4714 B.C.).
672    fn current_time(&self, cx: &Cx) -> f64 {
673        // Default: derive from `Cx` time capability (no ambient authority).
674        cx.current_time_julian_day()
675    }
676
677    /// Returns true if this VFS operates entirely in-process memory.
678    /// In-memory VFS backends can skip file locking, journal recovery,
679    /// and other I/O-oriented work in the pager hot path.
680    fn is_memory(&self) -> bool {
681        false
682    }
683}
684
685/// A file handle opened by a VFS.
686///
687/// Corresponds to C SQLite's `sqlite3_file` + `sqlite3_io_methods`.
688pub trait VfsFile: Send + Sync {
689    /// Close the file.
690    ///
691    /// After this call, the file handle should not be used.
692    fn close(&mut self, cx: &Cx) -> Result<()>;
693
694    /// Return the identity of the filesystem object held by this open handle.
695    ///
696    /// Implementations must derive this from the open descriptor (or from
697    /// descriptor metadata captured at open time), never by resolving the
698    /// current pathname. The default is `None` for memory and custom backends
699    /// that cannot provide a stable comparable identity.
700    fn file_identity(&self) -> Result<Option<FileIdentity>> {
701        Ok(None)
702    }
703
704    /// Read `buf.len()` bytes starting at byte offset `offset`.
705    ///
706    /// Returns the number of bytes actually read. If fewer bytes are read
707    /// than requested (short read), the remaining bytes in `buf` are zeroed.
708    fn read(&self, cx: &Cx, buf: &mut [u8], offset: u64) -> Result<usize>;
709
710    /// Write `buf` starting at byte offset `offset`.
711    fn write(&mut self, cx: &Cx, buf: &[u8], offset: u64) -> Result<()>;
712
713    /// Write multiple page-sized buffers in one logical operation.
714    ///
715    /// The default implementation preserves existing semantics by issuing the
716    /// writes sequentially through [`Self::write`]. VFS backends may override
717    /// this to amortize locking or syscall overhead for hot pager commit paths.
718    fn write_page_batch(&mut self, cx: &Cx, writes: &[(u64, &[u8])]) -> Result<()> {
719        for (offset, data) in writes {
720            self.write(cx, data, *offset)?;
721        }
722        Ok(())
723    }
724
725    /// Truncate the file to `size` bytes.
726    fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()>;
727
728    /// Sync the file contents to stable storage.
729    ///
730    /// `flags` indicates the type of sync (normal, full, data-only).
731    fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()>;
732
733    /// Durability-intent sync with per-filesystem policy.
734    ///
735    /// Callers express *what* must be durable (`SyncKind`); the VFS maps
736    /// it to the correct platform syscall. Default delegates to `sync()`
737    /// with `DATAONLY` / `FULL` flags; platform VFS backends override for
738    /// filesystem-specific behaviour (F_FULLFSYNC on APFS, etc.).
739    fn durable_sync(&mut self, cx: &Cx, kind: SyncKind) -> Result<()> {
740        let flags = match kind {
741            SyncKind::DataOnly => SyncFlags::DATAONLY,
742            SyncKind::DataAndMetadata | SyncKind::FullDurable => SyncFlags::FULL,
743        };
744        self.sync(cx, flags)
745    }
746
747    /// Return the current file size in bytes.
748    fn file_size(&self, cx: &Cx) -> Result<u64>;
749
750    /// Acquire a file lock at the given level.
751    ///
752    /// SQLite's five-level locking: None < Shared < Reserved < Pending < Exclusive.
753    fn lock(&mut self, cx: &Cx, level: LockLevel) -> Result<()>;
754
755    /// Release the file lock to the given level.
756    fn unlock(&mut self, cx: &Cx, level: LockLevel) -> Result<()>;
757
758    /// Acquire the cross-process SHARED fence used while capturing a coherent
759    /// main-database snapshot.
760    ///
761    /// Native Unix locks already use stock SQLite's main-file byte ranges, so
762    /// the default is the ordinary SHARED lock. Platform VFSes whose ordinary
763    /// locks are private coordination surfaces must additionally fence the
764    /// byte ranges used by the system SQLite VFS.
765    fn lock_external_shared_snapshot(&mut self, cx: &Cx) -> Result<()> {
766        self.lock(cx, LockLevel::Shared)
767    }
768
769    /// Release a fence acquired by [`Self::lock_external_shared_snapshot`].
770    fn unlock_external_shared_snapshot(&mut self, cx: &Cx) -> Result<()> {
771        self.unlock(cx, LockLevel::None)
772    }
773
774    /// Acquire the cross-process fence for an operation that replaces the
775    /// complete main-database image in place.
776    ///
777    /// The default implementation composes the ordinary SQLite lock surfaces:
778    /// in WAL mode it first excludes writers and checkpointers through the
779    /// adjacent WAL write/checkpoint slots, then obtains the main-database
780    /// EXCLUSIVE lock. Platform VFSes whose ordinary locks are not visible to
781    /// the system SQLite VFS must override this hook with a stock-compatible
782    /// external fence.
783    fn lock_external_maintenance(&mut self, cx: &Cx, wal_mode: bool) -> Result<()> {
784        if wal_mode {
785            self.shm_lock(
786                cx,
787                WAL_WRITE_LOCK,
788                WAL_CKPT_LOCK - WAL_WRITE_LOCK + 1,
789                SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE,
790            )?;
791        }
792
793        if let Err(lock_error) = self.lock(cx, LockLevel::Exclusive) {
794            if wal_mode
795                && let Err(unlock_error) = self.shm_lock(
796                    cx,
797                    WAL_WRITE_LOCK,
798                    WAL_CKPT_LOCK - WAL_WRITE_LOCK + 1,
799                    SQLITE_SHM_UNLOCK | SQLITE_SHM_EXCLUSIVE,
800                )
801            {
802                return Err(FrankenError::internal(format!(
803                    "external maintenance could not acquire the main database lock or release WAL maintenance locks: lock={lock_error}; unlock={unlock_error}"
804                )));
805            }
806            return Err(lock_error);
807        }
808        Ok(())
809    }
810
811    /// Release a fence acquired by [`Self::lock_external_maintenance`].
812    ///
813    /// Both lock surfaces are attempted even if one release fails, so a
814    /// cleanup error cannot silently strand the other cross-process lock.
815    fn unlock_external_maintenance(&mut self, cx: &Cx, wal_mode: bool) -> Result<()> {
816        let main_result = self.unlock(cx, LockLevel::None);
817        let wal_result = if wal_mode {
818            self.shm_lock(
819                cx,
820                WAL_WRITE_LOCK,
821                WAL_CKPT_LOCK - WAL_WRITE_LOCK + 1,
822                SQLITE_SHM_UNLOCK | SQLITE_SHM_EXCLUSIVE,
823            )
824        } else {
825            Ok(())
826        };
827
828        match (main_result, wal_result) {
829            (Ok(()), Ok(())) => Ok(()),
830            (Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
831            (Err(main_error), Err(wal_error)) => Err(FrankenError::internal(format!(
832                "external maintenance could not release all locks: main={main_error}; wal={wal_error}"
833            ))),
834        }
835    }
836
837    /// Check if another process holds a reserved lock.
838    ///
839    /// Returns true if a RESERVED or higher lock is held by another connection.
840    fn check_reserved_lock(&self, cx: &Cx) -> Result<bool>;
841
842    /// Return the sector size for this file.
843    ///
844    /// The sector size is the minimum write granularity for the underlying
845    /// storage. Defaults to 4096 bytes.
846    fn sector_size(&self) -> u32 {
847        4096
848    }
849
850    /// Return device characteristics flags.
851    ///
852    /// These flags describe capabilities of the underlying storage device,
853    /// such as whether it supports atomic writes. Returns 0 for no special
854    /// characteristics.
855    fn device_characteristics(&self) -> u32 {
856        0
857    }
858
859    // --- Shared-memory methods (required for WAL mode) ---
860
861    /// Map a region of shared memory. `region` is a 0-based index of 32KB
862    /// regions. If `extend` is true and the region does not exist, create it.
863    /// Returns a safe [`ShmRegion`] handle with bounds-checked accessors.
864    /// (Equivalent to sqlite3_io_methods.xShmMap)
865    fn shm_map(&mut self, cx: &Cx, region: u32, size: u32, extend: bool) -> Result<ShmRegion>;
866
867    /// Acquire or release a shared-memory lock.
868    /// `offset` and `n` define a range of lock slots.
869    /// `flags`: SHM_LOCK | (SHM_SHARED | SHM_EXCLUSIVE).
870    /// (Equivalent to sqlite3_io_methods.xShmLock)
871    fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()>;
872
873    /// Memory barrier for shared memory -- ensures all prior SHM writes are
874    /// visible to other processes before subsequent reads.
875    /// (Equivalent to sqlite3_io_methods.xShmBarrier)
876    fn shm_barrier(&self);
877
878    /// Unmap all shared-memory regions. If `delete` is true, also delete
879    /// the underlying SHM file.
880    /// (Equivalent to sqlite3_io_methods.xShmUnmap)
881    fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()>;
882
883    /// Set the busy-timeout for cross-process file-lock contention.
884    ///
885    /// When `ms > 0`, the VFS should retry `F_SETLK` with exponential
886    /// backoff instead of returning `SQLITE_BUSY` immediately on
887    /// `EAGAIN`/`EACCES`. A value of `0` disables retries (fail-fast).
888    ///
889    /// Default implementation is a no-op (memory and stub VFS backends
890    /// have no OS-level lock contention).
891    fn set_busy_timeout_ms(&mut self, _ms: u64) {}
892}
893
894/// Async data-path trait for VFS file I/O (bd-2jpu6.1 Phase 0).
895///
896/// Separates the async read/write data path from the sync `VfsFile` trait
897/// so callers that can drive a future (e.g. pager hot path with io_uring)
898/// avoid `pollster::block_on` overhead. Implementations that have a native
899/// async backend (io_uring via asupersync) override these to submit SQEs
900/// directly; sync-only backends get a default that delegates to `VfsFile`.
901pub trait AsyncVfsDataPath: VfsFile {
902    /// Async read into `buf` at byte `offset`. Returns bytes read; short
903    /// reads zero-fill the remainder (same contract as `VfsFile::read`).
904    fn read_async(
905        &self,
906        cx: &Cx,
907        buf: &mut [u8],
908        offset: u64,
909    ) -> impl std::future::Future<Output = Result<usize>> + Send
910    where
911        Self: Sync,
912    {
913        let result = self.read(cx, buf, offset);
914        async move { result }
915    }
916
917    /// Async write of `buf` at byte `offset`.
918    fn write_async(
919        &self,
920        cx: &Cx,
921        buf: &[u8],
922        offset: u64,
923    ) -> impl std::future::Future<Output = Result<()>> + Send
924    where
925        Self: Sync,
926    {
927        // Default: synchronous write — the `&mut self` requirement of
928        // `VfsFile::write` cannot be met through `&self`, so sync-only
929        // backends should override this if they want async write support.
930        let _ = (cx, buf, offset);
931        async { Err(fsqlite_error::FrankenError::Unsupported) }
932    }
933
934    /// Async batch write of page-sized buffers. Default delegates to
935    /// sequential `write_async` calls.
936    fn write_page_batch_async(
937        &self,
938        cx: &Cx,
939        writes: &[(u64, &[u8])],
940    ) -> impl std::future::Future<Output = Result<()>> + Send
941    where
942        Self: Sync,
943    {
944        let results: Vec<Result<()>> = writes
945            .iter()
946            .map(|(offset, data)| {
947                let _ = (cx, *data, *offset);
948                Ok(())
949            })
950            .collect();
951        async move {
952            for r in results {
953                r?;
954            }
955            Ok(())
956        }
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963
964    /// Verify that the trait is object-safe for VfsFile (can be used as dyn).
965    #[test]
966    fn vfs_file_is_object_safe() {
967        fn _accepts_dyn(_f: &dyn VfsFile) {}
968    }
969
970    /// Verify default implementations exist and don't panic.
971    #[test]
972    fn vfs_file_defaults() {
973        struct DummyFile;
974        impl VfsFile for DummyFile {
975            fn close(&mut self, _cx: &Cx) -> Result<()> {
976                Ok(())
977            }
978            fn read(&self, _cx: &Cx, _buf: &mut [u8], _offset: u64) -> Result<usize> {
979                Ok(0)
980            }
981            fn write(&mut self, _cx: &Cx, _buf: &[u8], _offset: u64) -> Result<()> {
982                Ok(())
983            }
984            fn truncate(&mut self, _cx: &Cx, _size: u64) -> Result<()> {
985                Ok(())
986            }
987            fn sync(&mut self, _cx: &Cx, _flags: SyncFlags) -> Result<()> {
988                Ok(())
989            }
990            fn file_size(&self, _cx: &Cx) -> Result<u64> {
991                Ok(0)
992            }
993            fn lock(&mut self, _cx: &Cx, _level: LockLevel) -> Result<()> {
994                Ok(())
995            }
996            fn unlock(&mut self, _cx: &Cx, _level: LockLevel) -> Result<()> {
997                Ok(())
998            }
999            fn check_reserved_lock(&self, _cx: &Cx) -> Result<bool> {
1000                Ok(false)
1001            }
1002            fn shm_map(
1003                &mut self,
1004                _cx: &Cx,
1005                _region: u32,
1006                _size: u32,
1007                _extend: bool,
1008            ) -> Result<ShmRegion> {
1009                Err(fsqlite_error::FrankenError::Unsupported)
1010            }
1011            fn shm_lock(&mut self, _cx: &Cx, _offset: u32, _n: u32, _flags: u32) -> Result<()> {
1012                Err(fsqlite_error::FrankenError::Unsupported)
1013            }
1014            fn shm_barrier(&self) {}
1015            fn shm_unmap(&mut self, _cx: &Cx, _delete: bool) -> Result<()> {
1016                Ok(())
1017            }
1018        }
1019
1020        let file = DummyFile;
1021        assert_eq!(file.sector_size(), 4096);
1022        assert_eq!(file.device_characteristics(), 0);
1023    }
1024
1025    /// Verify that VfsFile trait defaults are what we expect.
1026    #[test]
1027    fn vfs_file_sector_size_default_is_4096() {
1028        struct Stub;
1029        impl VfsFile for Stub {
1030            fn close(&mut self, _: &Cx) -> Result<()> {
1031                Ok(())
1032            }
1033            fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1034                Ok(0)
1035            }
1036            fn write(&mut self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1037                Ok(())
1038            }
1039            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1040                Ok(())
1041            }
1042            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1043                Ok(())
1044            }
1045            fn file_size(&self, _: &Cx) -> Result<u64> {
1046                Ok(0)
1047            }
1048            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1049                Ok(())
1050            }
1051            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1052                Ok(())
1053            }
1054            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1055                Ok(false)
1056            }
1057            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1058                Err(fsqlite_error::FrankenError::Unsupported)
1059            }
1060            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1061                Err(fsqlite_error::FrankenError::Unsupported)
1062            }
1063            fn shm_barrier(&self) {}
1064            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1065                Ok(())
1066            }
1067        }
1068
1069        let file = Stub;
1070        assert_eq!(file.sector_size(), 4096);
1071        assert_eq!(file.device_characteristics(), 0);
1072    }
1073
1074    /// Verify that default Vfs::randomness produces different sequences.
1075    #[test]
1076    fn vfs_default_randomness_varies() {
1077        use crate::memory::MemoryVfs;
1078        use crate::traits::Vfs;
1079
1080        let cx = Cx::new();
1081        let vfs = MemoryVfs::new();
1082        let mut buf1 = [0u8; 32];
1083        let mut buf2 = [0u8; 32];
1084        vfs.randomness(&cx, &mut buf1);
1085        vfs.randomness(&cx, &mut buf2);
1086        assert_ne!(buf1, buf2);
1087    }
1088
1089    /// Verify that default Vfs::current_time reads from Cx.
1090    #[test]
1091    fn vfs_default_current_time_from_cx() {
1092        use crate::memory::MemoryVfs;
1093        use crate::traits::Vfs;
1094
1095        let cx = Cx::new();
1096        cx.set_unix_millis_for_testing(0);
1097        let vfs = MemoryVfs::new();
1098        let t1 = vfs.current_time(&cx);
1099        // Unix epoch in Julian days is 2440587.5
1100        #[allow(clippy::approx_constant)]
1101        let expected = 2_440_587.5;
1102        assert!(
1103            (t1 - expected).abs() < 1e-6,
1104            "at unix epoch, julian day should be ~2440587.5, got {t1}"
1105        );
1106    }
1107
1108    /// Verify randomness with a zero-length buffer doesn't panic.
1109    #[test]
1110    fn vfs_randomness_zero_length_buffer() {
1111        use crate::memory::MemoryVfs;
1112        use crate::traits::Vfs;
1113
1114        let cx = Cx::new();
1115        let vfs = MemoryVfs::new();
1116        let mut buf = [];
1117        vfs.randomness(&cx, &mut buf);
1118    }
1119
1120    /// Verify randomness with a 1-byte buffer.
1121    #[test]
1122    fn vfs_randomness_single_byte() {
1123        use crate::memory::MemoryVfs;
1124        use crate::traits::Vfs;
1125
1126        let cx = Cx::new();
1127        let vfs = MemoryVfs::new();
1128        let mut buf = [0u8; 1];
1129        vfs.randomness(&cx, &mut buf);
1130        // Can't assert much about the value, just that it doesn't panic.
1131    }
1132
1133    #[test]
1134    fn vfs_is_memory_default_is_false() {
1135        use crate::memory::MemoryVfs;
1136        use crate::traits::Vfs;
1137
1138        let vfs = MemoryVfs::new();
1139        assert!(vfs.is_memory(), "MemoryVfs::is_memory must return true");
1140    }
1141
1142    #[test]
1143    fn vfs_trait_is_object_safe() {
1144        use crate::memory::MemoryVfs;
1145        fn _accepts_dyn(_v: &dyn Vfs<File = crate::memory::MemoryFile>) {}
1146        let _vfs = MemoryVfs::new();
1147    }
1148
1149    #[test]
1150    fn vfs_file_set_busy_timeout_is_noop() {
1151        struct Stub;
1152        impl VfsFile for Stub {
1153            fn close(&mut self, _: &Cx) -> Result<()> {
1154                Ok(())
1155            }
1156            fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1157                Ok(0)
1158            }
1159            fn write(&mut self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1160                Ok(())
1161            }
1162            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1163                Ok(())
1164            }
1165            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1166                Ok(())
1167            }
1168            fn file_size(&self, _: &Cx) -> Result<u64> {
1169                Ok(0)
1170            }
1171            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1172                Ok(())
1173            }
1174            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1175                Ok(())
1176            }
1177            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1178                Ok(false)
1179            }
1180            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1181                Err(fsqlite_error::FrankenError::Unsupported)
1182            }
1183            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1184                Err(fsqlite_error::FrankenError::Unsupported)
1185            }
1186            fn shm_barrier(&self) {}
1187            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1188                Ok(())
1189            }
1190        }
1191
1192        let mut file = Stub;
1193        file.set_busy_timeout_ms(5000);
1194        file.set_busy_timeout_ms(0);
1195    }
1196
1197    #[test]
1198    fn vfs_file_write_page_batch_default_delegates_to_write() {
1199        use std::sync::atomic::{AtomicUsize, Ordering};
1200
1201        static WRITE_COUNT: AtomicUsize = AtomicUsize::new(0);
1202
1203        struct CountingFile;
1204        impl VfsFile for CountingFile {
1205            fn close(&mut self, _: &Cx) -> Result<()> {
1206                Ok(())
1207            }
1208            fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1209                Ok(0)
1210            }
1211            fn write(&mut self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1212                WRITE_COUNT.fetch_add(1, Ordering::Relaxed);
1213                Ok(())
1214            }
1215            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1216                Ok(())
1217            }
1218            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1219                Ok(())
1220            }
1221            fn file_size(&self, _: &Cx) -> Result<u64> {
1222                Ok(0)
1223            }
1224            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1225                Ok(())
1226            }
1227            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1228                Ok(())
1229            }
1230            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1231                Ok(false)
1232            }
1233            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1234                Err(fsqlite_error::FrankenError::Unsupported)
1235            }
1236            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1237                Err(fsqlite_error::FrankenError::Unsupported)
1238            }
1239            fn shm_barrier(&self) {}
1240            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1241                Ok(())
1242            }
1243        }
1244
1245        WRITE_COUNT.store(0, Ordering::Relaxed);
1246        let cx = Cx::new();
1247        let mut file = CountingFile;
1248        let data = [0u8; 4096];
1249        let writes: Vec<(u64, &[u8])> = vec![(0, &data), (4096, &data), (8192, &data)];
1250        file.write_page_batch(&cx, &writes).unwrap();
1251        assert_eq!(WRITE_COUNT.load(Ordering::Relaxed), 3);
1252    }
1253
1254    #[test]
1255    fn vfs_randomness_fills_large_buffer() {
1256        use crate::memory::MemoryVfs;
1257        use crate::traits::Vfs;
1258
1259        let cx = Cx::new();
1260        let vfs = MemoryVfs::new();
1261        let mut buf = [0u8; 256];
1262        vfs.randomness(&cx, &mut buf);
1263        let all_zero = buf.iter().all(|&b| b == 0);
1264        assert!(
1265            !all_zero,
1266            "256-byte randomness buffer should not be all zeros"
1267        );
1268    }
1269
1270    #[test]
1271    fn vfs_randomness_non_aligned_buffer() {
1272        use crate::memory::MemoryVfs;
1273        use crate::traits::Vfs;
1274
1275        let cx = Cx::new();
1276        let vfs = MemoryVfs::new();
1277        let mut buf = [0u8; 13];
1278        vfs.randomness(&cx, &mut buf);
1279        let all_zero = buf.iter().all(|&b| b == 0);
1280        assert!(!all_zero, "13-byte non-aligned buffer should be filled");
1281    }
1282
1283    #[test]
1284    fn vfs_write_page_batch_empty_is_noop() {
1285        struct Stub;
1286        impl VfsFile for Stub {
1287            fn close(&mut self, _: &Cx) -> Result<()> {
1288                Ok(())
1289            }
1290            fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1291                Ok(0)
1292            }
1293            fn write(&mut self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1294                panic!("write should not be called for empty batch");
1295            }
1296            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1297                Ok(())
1298            }
1299            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1300                Ok(())
1301            }
1302            fn file_size(&self, _: &Cx) -> Result<u64> {
1303                Ok(0)
1304            }
1305            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1306                Ok(())
1307            }
1308            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1309                Ok(())
1310            }
1311            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1312                Ok(false)
1313            }
1314            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1315                Err(fsqlite_error::FrankenError::Unsupported)
1316            }
1317            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1318                Err(fsqlite_error::FrankenError::Unsupported)
1319            }
1320            fn shm_barrier(&self) {}
1321            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1322                Ok(())
1323            }
1324        }
1325
1326        let cx = Cx::new();
1327        let mut file = Stub;
1328        let writes: Vec<(u64, &[u8])> = vec![];
1329        file.write_page_batch(&cx, &writes).unwrap();
1330    }
1331
1332    #[test]
1333    fn memory_vfs_name_is_memory() {
1334        use crate::memory::MemoryVfs;
1335        use crate::traits::Vfs;
1336
1337        let vfs = MemoryVfs::new();
1338        assert_eq!(vfs.name(), "memory");
1339    }
1340
1341    #[test]
1342    fn vfs_current_time_advances_with_unix_millis() {
1343        use crate::memory::MemoryVfs;
1344        use crate::traits::Vfs;
1345
1346        let cx = Cx::new();
1347        let vfs = MemoryVfs::new();
1348        cx.set_unix_millis_for_testing(0);
1349        let t0 = vfs.current_time(&cx);
1350        cx.set_unix_millis_for_testing(86_400_000);
1351        let t1 = vfs.current_time(&cx);
1352        let delta = t1 - t0;
1353        assert!(
1354            (delta - 1.0).abs() < 1e-6,
1355            "86400000ms = 1 Julian day, got delta {delta}"
1356        );
1357    }
1358
1359    #[test]
1360    fn write_page_batch_short_circuits_on_error() {
1361        use std::sync::atomic::{AtomicUsize, Ordering};
1362
1363        static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
1364
1365        struct FailOnSecond;
1366        impl VfsFile for FailOnSecond {
1367            fn close(&mut self, _: &Cx) -> Result<()> {
1368                Ok(())
1369            }
1370            fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1371                Ok(0)
1372            }
1373            fn write(&mut self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1374                let n = CALL_COUNT.fetch_add(1, Ordering::Relaxed);
1375                if n >= 1 {
1376                    return Err(fsqlite_error::FrankenError::Io(std::io::Error::other(
1377                        "injected",
1378                    )));
1379                }
1380                Ok(())
1381            }
1382            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1383                Ok(())
1384            }
1385            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1386                Ok(())
1387            }
1388            fn file_size(&self, _: &Cx) -> Result<u64> {
1389                Ok(0)
1390            }
1391            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1392                Ok(())
1393            }
1394            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1395                Ok(())
1396            }
1397            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1398                Ok(false)
1399            }
1400            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1401                Err(fsqlite_error::FrankenError::Unsupported)
1402            }
1403            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1404                Err(fsqlite_error::FrankenError::Unsupported)
1405            }
1406            fn shm_barrier(&self) {}
1407            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1408                Ok(())
1409            }
1410        }
1411
1412        CALL_COUNT.store(0, Ordering::Relaxed);
1413        let cx = Cx::new();
1414        let mut file = FailOnSecond;
1415        let data = [0u8; 64];
1416        let writes: Vec<(u64, &[u8])> = vec![(0, &data), (64, &data), (128, &data)];
1417        let result = file.write_page_batch(&cx, &writes);
1418        assert!(result.is_err());
1419        assert_eq!(
1420            CALL_COUNT.load(Ordering::Relaxed),
1421            2,
1422            "should stop after second write fails, not call third"
1423        );
1424    }
1425
1426    #[test]
1427    fn vfs_file_defaults_can_be_overridden() {
1428        struct CustomFile;
1429        impl VfsFile for CustomFile {
1430            fn close(&mut self, _: &Cx) -> Result<()> {
1431                Ok(())
1432            }
1433            fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1434                Ok(0)
1435            }
1436            fn write(&mut self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1437                Ok(())
1438            }
1439            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1440                Ok(())
1441            }
1442            fn sync(&mut self, _: &Cx, _: SyncFlags) -> Result<()> {
1443                Ok(())
1444            }
1445            fn file_size(&self, _: &Cx) -> Result<u64> {
1446                Ok(0)
1447            }
1448            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1449                Ok(())
1450            }
1451            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1452                Ok(())
1453            }
1454            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1455                Ok(false)
1456            }
1457            fn sector_size(&self) -> u32 {
1458                512
1459            }
1460            fn device_characteristics(&self) -> u32 {
1461                0x0010
1462            }
1463            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1464                Err(fsqlite_error::FrankenError::Unsupported)
1465            }
1466            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1467                Err(fsqlite_error::FrankenError::Unsupported)
1468            }
1469            fn shm_barrier(&self) {}
1470            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1471                Ok(())
1472            }
1473        }
1474
1475        let file = CustomFile;
1476        assert_eq!(file.sector_size(), 512);
1477        assert_eq!(file.device_characteristics(), 0x0010);
1478    }
1479
1480    #[test]
1481    fn vfs_randomness_has_byte_level_entropy() {
1482        use crate::memory::MemoryVfs;
1483        use crate::traits::Vfs;
1484
1485        let cx = Cx::new();
1486        let vfs = MemoryVfs::new();
1487        let mut buf = [0u8; 64];
1488        vfs.randomness(&cx, &mut buf);
1489        let distinct: std::collections::HashSet<u8> = buf.iter().copied().collect();
1490        assert!(
1491            distinct.len() > 4,
1492            "64-byte buffer should have more than 4 distinct byte values, got {}",
1493            distinct.len()
1494        );
1495    }
1496
1497    #[test]
1498    fn vfs_current_time_default_returns_reasonable_julian_day() {
1499        use crate::memory::MemoryVfs;
1500        use crate::traits::Vfs;
1501
1502        let cx = Cx::new();
1503        let vfs = MemoryVfs::new();
1504        let jd = vfs.current_time(&cx);
1505        assert!(jd.is_finite(), "Julian day must be finite");
1506        assert!(
1507            jd > 2_440_000.0,
1508            "Julian day should be after ~1968, got {jd}"
1509        );
1510    }
1511
1512    #[test]
1513    fn durable_sync_default_delegates_to_sync() {
1514        use std::sync::atomic::{AtomicU8, Ordering};
1515        static LAST_FLAGS: AtomicU8 = AtomicU8::new(0);
1516
1517        struct RecordingFile;
1518        impl VfsFile for RecordingFile {
1519            fn close(&mut self, _: &Cx) -> Result<()> {
1520                Ok(())
1521            }
1522            fn read(&self, _: &Cx, _: &mut [u8], _: u64) -> Result<usize> {
1523                Ok(0)
1524            }
1525            fn write(&mut self, _: &Cx, _: &[u8], _: u64) -> Result<()> {
1526                Ok(())
1527            }
1528            fn truncate(&mut self, _: &Cx, _: u64) -> Result<()> {
1529                Ok(())
1530            }
1531            fn sync(&mut self, _: &Cx, flags: SyncFlags) -> Result<()> {
1532                LAST_FLAGS.store(flags.bits(), Ordering::Relaxed);
1533                Ok(())
1534            }
1535            fn file_size(&self, _: &Cx) -> Result<u64> {
1536                Ok(0)
1537            }
1538            fn lock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1539                Ok(())
1540            }
1541            fn unlock(&mut self, _: &Cx, _: LockLevel) -> Result<()> {
1542                Ok(())
1543            }
1544            fn check_reserved_lock(&self, _: &Cx) -> Result<bool> {
1545                Ok(false)
1546            }
1547            fn shm_map(&mut self, _: &Cx, _: u32, _: u32, _: bool) -> Result<ShmRegion> {
1548                Err(fsqlite_error::FrankenError::Unsupported)
1549            }
1550            fn shm_lock(&mut self, _: &Cx, _: u32, _: u32, _: u32) -> Result<()> {
1551                Err(fsqlite_error::FrankenError::Unsupported)
1552            }
1553            fn shm_barrier(&self) {}
1554            fn shm_unmap(&mut self, _: &Cx, _: bool) -> Result<()> {
1555                Ok(())
1556            }
1557        }
1558
1559        let cx = Cx::new();
1560        let mut f = RecordingFile;
1561
1562        f.durable_sync(&cx, SyncKind::DataOnly).unwrap();
1563        assert_eq!(
1564            LAST_FLAGS.load(Ordering::Relaxed),
1565            SyncFlags::DATAONLY.bits()
1566        );
1567
1568        f.durable_sync(&cx, SyncKind::FullDurable).unwrap();
1569        assert_eq!(LAST_FLAGS.load(Ordering::Relaxed), SyncFlags::FULL.bits());
1570
1571        f.durable_sync(&cx, SyncKind::DataAndMetadata).unwrap();
1572        assert_eq!(LAST_FLAGS.load(Ordering::Relaxed), SyncFlags::FULL.bits());
1573    }
1574
1575    #[test]
1576    fn sync_kind_variants_are_distinct() {
1577        assert_ne!(SyncKind::DataOnly, SyncKind::DataAndMetadata);
1578        assert_ne!(SyncKind::DataAndMetadata, SyncKind::FullDurable);
1579        assert_ne!(SyncKind::DataOnly, SyncKind::FullDurable);
1580    }
1581
1582    #[test]
1583    fn async_vfs_data_path_trait_is_implementable() {
1584        use crate::memory::MemoryFile;
1585
1586        fn assert_impl<T: AsyncVfsDataPath>() {}
1587        assert_impl::<MemoryFile>();
1588    }
1589
1590    #[test]
1591    fn async_vfs_data_path_default_read_resolves_immediately() {
1592        use crate::memory::MemoryVfs;
1593
1594        let cx = Cx::new();
1595        let vfs = MemoryVfs::new();
1596        let flags = fsqlite_types::flags::VfsOpenFlags::MAIN_DB
1597            | fsqlite_types::flags::VfsOpenFlags::CREATE
1598            | fsqlite_types::flags::VfsOpenFlags::READWRITE;
1599        let (mut file, _) = vfs.open(&cx, None, flags).unwrap();
1600
1601        let payload = b"hello async vfs";
1602        file.write(&cx, payload, 0).unwrap();
1603
1604        let mut buf = [0u8; 15];
1605        let n = pollster::block_on(AsyncVfsDataPath::read_async(&file, &cx, &mut buf, 0)).unwrap();
1606        assert_eq!(n, 15);
1607        assert_eq!(&buf, payload);
1608    }
1609}