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#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct FileIdentity {
23 kind: FileIdentityKind,
24 namespace: u64,
25 object: [u8; 16],
26}
27
28#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34enum FileIdentityKind {
35 Memory,
37 #[cfg(unix)]
38 Unix,
39 #[cfg(windows)]
40 WindowsFileId128,
41 #[cfg(windows)]
42 WindowsFileIndex64,
43}
44
45impl FileIdentity {
46 #[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 #[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 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 #[cfg(any(unix, windows))]
169 pub(crate) fn to_namespace_bytes(self) -> [u8; 25] {
170 let tag = match self.kind {
171 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 #[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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
509pub enum SyncKind {
510 DataOnly,
513 DataAndMetadata,
516 FullDurable,
520}
521
522static DEFAULT_RANDOMNESS_CALL_SEQ: AtomicU64 = AtomicU64::new(0);
523
524pub trait Vfs: Send + Sync {
531 type File: VfsFile;
533
534 fn name(&self) -> &'static str;
536
537 fn open(
546 &self,
547 cx: &Cx,
548 path: Option<&Path>,
549 flags: VfsOpenFlags,
550 ) -> Result<(Self::File, VfsOpenFlags)>;
551
552 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 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 fn delete(&self, cx: &Cx, path: &Path, sync_dir: bool) -> Result<()>;
616
617 fn sync_parent_directory(&self, _cx: &Cx, _path: &Path) -> Result<()> {
624 Ok(())
625 }
626
627 fn access(&self, cx: &Cx, path: &Path, flags: AccessFlags) -> Result<bool>;
632
633 fn path_entry_exists(&self, cx: &Cx, path: &Path) -> Result<bool> {
640 self.access(cx, path, AccessFlags::EXISTS)
641 }
642
643 fn full_pathname(&self, cx: &Cx, path: &Path) -> Result<PathBuf>;
645
646 fn randomness(&self, cx: &Cx, buf: &mut [u8]) {
654 let _ = cx; 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 fn current_time(&self, cx: &Cx) -> f64 {
673 cx.current_time_julian_day()
675 }
676
677 fn is_memory(&self) -> bool {
681 false
682 }
683}
684
685pub trait VfsFile: Send + Sync {
689 fn close(&mut self, cx: &Cx) -> Result<()>;
693
694 fn file_identity(&self) -> Result<Option<FileIdentity>> {
701 Ok(None)
702 }
703
704 fn read(&self, cx: &Cx, buf: &mut [u8], offset: u64) -> Result<usize>;
709
710 fn write(&mut self, cx: &Cx, buf: &[u8], offset: u64) -> Result<()>;
712
713 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 fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()>;
727
728 fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()>;
732
733 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 fn file_size(&self, cx: &Cx) -> Result<u64>;
749
750 fn lock(&mut self, cx: &Cx, level: LockLevel) -> Result<()>;
754
755 fn unlock(&mut self, cx: &Cx, level: LockLevel) -> Result<()>;
757
758 fn lock_external_shared_snapshot(&mut self, cx: &Cx) -> Result<()> {
766 self.lock(cx, LockLevel::Shared)
767 }
768
769 fn unlock_external_shared_snapshot(&mut self, cx: &Cx) -> Result<()> {
771 self.unlock(cx, LockLevel::None)
772 }
773
774 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 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 fn check_reserved_lock(&self, cx: &Cx) -> Result<bool>;
841
842 fn sector_size(&self) -> u32 {
847 4096
848 }
849
850 fn device_characteristics(&self) -> u32 {
856 0
857 }
858
859 fn shm_map(&mut self, cx: &Cx, region: u32, size: u32, extend: bool) -> Result<ShmRegion>;
866
867 fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()>;
872
873 fn shm_barrier(&self);
877
878 fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()>;
882
883 fn set_busy_timeout_ms(&mut self, _ms: u64) {}
892}
893
894pub trait AsyncVfsDataPath: VfsFile {
902 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 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 let _ = (cx, buf, offset);
931 async { Err(fsqlite_error::FrankenError::Unsupported) }
932 }
933
934 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 #[test]
966 fn vfs_file_is_object_safe() {
967 fn _accepts_dyn(_f: &dyn VfsFile) {}
968 }
969
970 #[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 #[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 #[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 #[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 #[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 #[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 #[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 }
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}