sqlite_wasm_vfs/
sahpool.rs

1//! opfs-sahpool vfs implementation, ported from sqlite-wasm.
2//!
3//! See [`opfs-sahpool`](https://sqlite.org/wasm/doc/trunk/persistence.md#vfs-opfs-sahpool) for details.
4//!
5//! ```rust
6//! use sqlite_wasm_rs as ffi;
7//! use sqlite_wasm_vfs::sahpool::{install as install_opfs_sahpool, OpfsSAHPoolCfg};
8//!
9//! async fn open_db() {
10//!     // install opfs-sahpool persistent vfs and set as default vfs
11//!     install_opfs_sahpool::<ffi::WasmOsCallback>(&OpfsSAHPoolCfg::default(), true)
12//!         .await
13//!         .unwrap();
14//!
15//!     // open with opfs-sahpool vfs
16//!     let mut db = std::ptr::null_mut();
17//!     let ret = unsafe {
18//!         ffi::sqlite3_open_v2(
19//!             c"opfs-sahpool.db".as_ptr().cast(),
20//!             &mut db as *mut _,
21//!             ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE,
22//!             std::ptr::null()
23//!         )
24//!     };
25//!     assert_eq!(ffi::SQLITE_OK, ret);
26//! }
27//! ```
28//!
29//! The VFS is based on
30//! [`FileSystemSyncAccessHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle)
31//! read and write, and you can install the
32//! [`opfs-explorer`](https://chromewebstore.google.com/detail/opfs-explorer/acndjpgkpaclldomagafnognkcgjignd)
33//! plugin to browse files.
34
35use rsqlite_vfs::{
36    check_import_db,
37    ffi::{
38        sqlite3_file, sqlite3_filename, sqlite3_vfs, sqlite3_vfs_register, sqlite3_vfs_unregister,
39        SQLITE_CANTOPEN, SQLITE_ERROR, SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN, SQLITE_IOERR,
40        SQLITE_IOERR_DELETE, SQLITE_OK, SQLITE_OPEN_DELETEONCLOSE, SQLITE_OPEN_MAIN_DB,
41        SQLITE_OPEN_MAIN_JOURNAL, SQLITE_OPEN_SUPER_JOURNAL, SQLITE_OPEN_WAL,
42    },
43    register_vfs, registered_vfs, ImportDbError, OsCallback, RegisterVfsError, SQLiteIoMethods,
44    SQLiteVfs, SQLiteVfsFile, VfsAppData, VfsError, VfsFile, VfsResult, VfsStore,
45};
46use std::collections::{HashMap, HashSet};
47use std::time::Duration;
48use std::{
49    cell::{Cell, RefCell},
50    marker::PhantomData,
51};
52
53use js_sys::{Array, DataView, IteratorNext, Reflect, Uint8Array};
54use wasm_bindgen::{JsCast, JsValue};
55use wasm_bindgen_futures::JsFuture;
56use web_sys::{
57    FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions,
58    FileSystemGetFileOptions, FileSystemReadWriteOptions, FileSystemSyncAccessHandle,
59    WorkerGlobalScope,
60};
61
62const SECTOR_SIZE: usize = 4096;
63const HEADER_MAX_FILENAME_SIZE: usize = 512;
64const HEADER_FLAGS_SIZE: usize = 4;
65const HEADER_CORPUS_SIZE: usize = HEADER_MAX_FILENAME_SIZE + HEADER_FLAGS_SIZE;
66const HEADER_OFFSET_FLAGS: usize = HEADER_MAX_FILENAME_SIZE;
67const HEADER_OFFSET_DATA: usize = SECTOR_SIZE;
68
69const PERSISTENT_FILE_TYPES: i32 =
70    SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_MAIN_JOURNAL | SQLITE_OPEN_SUPER_JOURNAL | SQLITE_OPEN_WAL;
71
72type Result<T, E = OpfsSAHError> = std::result::Result<T, E>;
73
74fn read_write_options(at: f64) -> FileSystemReadWriteOptions {
75    let options = FileSystemReadWriteOptions::new();
76    options.set_at(at);
77    options
78}
79
80struct SyncAccessFile {
81    handle: FileSystemSyncAccessHandle,
82    opaque: String,
83}
84
85struct OpfsSAHPool {
86    /// Directory handle to the `.opaque` subdirectory within the VFS root.
87    /// This directory holds the actual files, which have randomly-generated names.
88    dh_opaque: FileSystemDirectoryHandle,
89    /// A reusable buffer for reading and writing file headers.
90    header_buffer: Uint8Array,
91    /// A `DataView` for accessing the binary data in `header_buffer`.
92    header_buffer_view: DataView,
93    /// A pool of available `SyncAccessHandle`s that are not currently associated with a database file.
94    available_files: RefCell<Vec<SyncAccessFile>>,
95    /// Maps the user-facing database filenames to their underlying `SyncAccessFile`.
96    map_filename_to_file: RefCell<HashMap<String, SyncAccessFile>>,
97    /// A flag to indicate whether the VFS is currently paused.
98    is_paused: Cell<bool>,
99    /// A set of filenames for all currently open database connections.
100    open_files: RefCell<HashSet<String>>,
101    /// A tuple holding the raw pointer to the `sqlite3_vfs` struct and whether it was registered as the default.
102    vfs: Cell<(*mut sqlite3_vfs, bool)>,
103    random: fn(&mut [u8]),
104}
105
106impl OpfsSAHPool {
107    async fn new<C: OsCallback>(options: &OpfsSAHPoolCfg) -> Result<OpfsSAHPool> {
108        const OPAQUE_DIR_NAME: &str = ".opaque";
109
110        let vfs_dir = &options.directory;
111        let capacity = options.initial_capacity;
112        let clear_files = options.clear_on_init;
113
114        let create_option = FileSystemGetDirectoryOptions::new();
115        create_option.set_create(true);
116
117        let mut handle: FileSystemDirectoryHandle = JsFuture::from(
118            js_sys::global()
119                .dyn_into::<WorkerGlobalScope>()
120                .map_err(|_| OpfsSAHError::NotSupported)?
121                .navigator()
122                .storage()
123                .get_directory(),
124        )
125        .await
126        .map_err(OpfsSAHError::GetDirHandle)?
127        .into();
128
129        for dir in vfs_dir.split('/').filter(|x| !x.is_empty()) {
130            let next =
131                JsFuture::from(handle.get_directory_handle_with_options(dir, &create_option))
132                    .await
133                    .map_err(OpfsSAHError::GetDirHandle)?
134                    .into();
135            handle = next;
136        }
137
138        let dh_opaque = JsFuture::from(
139            handle.get_directory_handle_with_options(OPAQUE_DIR_NAME, &create_option),
140        )
141        .await
142        .map_err(OpfsSAHError::GetDirHandle)?
143        .into();
144
145        let ap_body = Uint8Array::new_with_length(HEADER_CORPUS_SIZE as _);
146        let dv_body = DataView::new(
147            &ap_body.buffer(),
148            ap_body.byte_offset() as usize,
149            (ap_body.byte_length() - ap_body.byte_offset()) as usize,
150        );
151
152        let pool = Self {
153            dh_opaque,
154            header_buffer: ap_body,
155            header_buffer_view: dv_body,
156            map_filename_to_file: RefCell::new(HashMap::new()),
157            available_files: RefCell::new(Vec::new()),
158            is_paused: Cell::new(false),
159            open_files: RefCell::new(HashSet::new()),
160            vfs: Cell::new((std::ptr::null_mut(), false)),
161            random: C::random,
162        };
163
164        pool.acquire_access_handles(clear_files).await?;
165        pool.reserve_minimum_capacity(capacity).await?;
166
167        Ok(pool)
168    }
169
170    async fn add_capacity(&self, n: u32) -> Result<u32> {
171        for _ in 0..n {
172            let opaque = rsqlite_vfs::random_name(self.random);
173            let handle: FileSystemFileHandle =
174                JsFuture::from(self.dh_opaque.get_file_handle_with_options(&opaque, &{
175                    let options = FileSystemGetFileOptions::new();
176                    options.set_create(true);
177                    options
178                }))
179                .await
180                .map_err(OpfsSAHError::GetFileHandle)?
181                .into();
182            let sah: FileSystemSyncAccessHandle =
183                JsFuture::from(handle.create_sync_access_handle())
184                    .await
185                    .map_err(OpfsSAHError::CreateSyncAccessHandle)?
186                    .into();
187            let file = SyncAccessFile {
188                handle: sah,
189                opaque,
190            };
191            self.set_associated_filename(&file.handle, None, 0)?;
192            self.available_files.borrow_mut().push(file);
193        }
194        Ok(self.get_capacity())
195    }
196
197    async fn reserve_minimum_capacity(&self, min: u32) -> Result<()> {
198        self.add_capacity(min.saturating_sub(self.get_capacity()))
199            .await?;
200        Ok(())
201    }
202
203    #[allow(clippy::await_holding_refcell_ref)]
204    async fn reduce_capacity(&self, n: u32) -> Result<u32> {
205        let mut available_files = self.available_files.borrow_mut();
206        let available_length = available_files.len();
207        let max_reduce = available_length.min(n as usize);
208        let files = available_files.split_off(available_length - max_reduce);
209        // The `RefMut` from `name2file` is explicitly dropped here to avoid holding the borrow across an `.await` point.
210        drop(available_files);
211
212        for file in files {
213            file.handle.close();
214            JsFuture::from(self.dh_opaque.remove_entry(&file.opaque))
215                .await
216                .map_err(OpfsSAHError::RemoveEntity)?;
217        }
218
219        Ok(max_reduce as u32)
220    }
221
222    fn get_capacity(&self) -> u32 {
223        (self.map_filename_to_file.borrow().len() + self.available_files.borrow().len()) as u32
224    }
225
226    fn get_file_count(&self) -> u32 {
227        self.map_filename_to_file.borrow().len() as u32
228    }
229
230    fn get_filenames(&self) -> Vec<String> {
231        self.map_filename_to_file.borrow().keys().cloned().collect()
232    }
233
234    fn get_associated_filename(&self, sah: &FileSystemSyncAccessHandle) -> Result<Option<String>> {
235        sah.read_with_buffer_source_and_options(&self.header_buffer, &read_write_options(0.0))
236            .map_err(OpfsSAHError::Read)?;
237        let flags = self.header_buffer_view.get_uint32(HEADER_OFFSET_FLAGS);
238        if self.header_buffer.get_index(0) != 0
239            && ((flags & SQLITE_OPEN_DELETEONCLOSE as u32 != 0)
240                || (flags & PERSISTENT_FILE_TYPES as u32) == 0)
241        {
242            return Ok(None);
243        }
244
245        let name_length = self
246            .header_buffer
247            .to_vec()
248            .iter()
249            .position(|&x| x == 0)
250            .unwrap_or_default();
251        if name_length == 0 {
252            sah.truncate_with_u32(HEADER_OFFSET_DATA as u32)
253                .map_err(OpfsSAHError::Truncate)?;
254            return Ok(None);
255        }
256        // set_associated_filename ensures that it is utf8
257        let filename =
258            String::from_utf8(self.header_buffer.subarray(0, name_length as u32).to_vec()).unwrap();
259        Ok(Some(filename))
260    }
261
262    fn set_associated_filename(
263        &self,
264        sah: &FileSystemSyncAccessHandle,
265        filename: Option<&str>,
266        flags: i32,
267    ) -> Result<()> {
268        self.header_buffer_view
269            .set_uint32(HEADER_OFFSET_FLAGS, flags as u32);
270
271        if let Some(filename) = filename {
272            if filename.is_empty() {
273                return Err(OpfsSAHError::Generic("Filename is empty".into()));
274            }
275            if HEADER_MAX_FILENAME_SIZE <= filename.len() + 1 {
276                return Err(OpfsSAHError::Generic(format!(
277                    "Filename too long: {filename}"
278                )));
279            }
280            self.header_buffer
281                .subarray(0, filename.len() as u32)
282                .copy_from(filename.as_bytes());
283            self.header_buffer
284                .fill(0, filename.len() as u32, HEADER_MAX_FILENAME_SIZE as u32);
285        } else {
286            self.header_buffer
287                .fill(0, 0, HEADER_MAX_FILENAME_SIZE as u32);
288            sah.truncate_with_u32(HEADER_OFFSET_DATA as u32)
289                .map_err(OpfsSAHError::Truncate)?;
290        }
291
292        sah.write_with_js_u8_array_and_options(&self.header_buffer, &read_write_options(0.0))
293            .map_err(OpfsSAHError::Write)?;
294
295        Ok(())
296    }
297
298    async fn acquire_access_handles(&self, clear_files: bool) -> Result<()> {
299        let iter = self.dh_opaque.entries();
300        while let Ok(future) = iter.next() {
301            let next: IteratorNext = JsFuture::from(future)
302                .await
303                .map_err(OpfsSAHError::IterHandle)?
304                .into();
305            if next.done() {
306                break;
307            }
308            let array: Array = next.value().into();
309            let opaque = array
310                .get(0)
311                .as_string()
312                .ok_or_else(|| OpfsSAHError::Generic("Failed to get file's opaque name".into()))?;
313            let value = array.get(1);
314            let kind = Reflect::get(&value, &JsValue::from("kind"))
315                .map_err(OpfsSAHError::Reflect)?
316                .as_string();
317            if kind.as_deref() == Some("file") {
318                let handle = FileSystemFileHandle::from(value);
319                let sah = JsFuture::from(handle.create_sync_access_handle())
320                    .await
321                    .map_err(OpfsSAHError::CreateSyncAccessHandle)?;
322                let sah = FileSystemSyncAccessHandle::from(sah);
323                let file = SyncAccessFile {
324                    handle: sah,
325                    opaque,
326                };
327                let clear_file = |file: SyncAccessFile| -> Result<()> {
328                    self.set_associated_filename(&file.handle, None, 0)?;
329                    self.available_files.borrow_mut().push(file);
330                    Ok(())
331                };
332                if clear_files {
333                    clear_file(file)?;
334                } else if let Some(filename) = self.get_associated_filename(&file.handle)? {
335                    self.map_filename_to_file
336                        .borrow_mut()
337                        .insert(filename, file);
338                } else {
339                    clear_file(file)?;
340                }
341            }
342        }
343
344        Ok(())
345    }
346
347    fn release_access_handles(&self) {
348        for file in std::mem::take(&mut *self.available_files.borrow_mut())
349            .into_iter()
350            .chain(std::mem::take(&mut *self.map_filename_to_file.borrow_mut()).into_values())
351        {
352            file.handle.close();
353        }
354    }
355
356    fn delete_file(&self, filename: &str) -> Result<bool> {
357        let mut map_filename_to_file = self.map_filename_to_file.borrow_mut();
358        let mut available_files = self.available_files.borrow_mut();
359
360        if let Some(file) = map_filename_to_file.remove(filename) {
361            available_files.push(file);
362            let Some(file) = available_files.last() else {
363                unreachable!();
364            };
365            self.set_associated_filename(&file.handle, None, 0)?;
366            Ok(true)
367        } else {
368            Ok(false)
369        }
370    }
371
372    fn has_filename(&self, filename: &str) -> bool {
373        self.map_filename_to_file.borrow().contains_key(filename)
374    }
375
376    fn with_file<E, R, F: Fn(&SyncAccessFile) -> Result<R, E>>(
377        &self,
378        filename: &str,
379        f: F,
380    ) -> Option<Result<R, E>> {
381        self.map_filename_to_file.borrow().get(filename).map(f)
382    }
383
384    fn with_file_mut<E, R, F: Fn(&mut SyncAccessFile) -> Result<R, E>>(
385        &self,
386        filename: &str,
387        f: F,
388    ) -> Option<Result<R, E>> {
389        self.map_filename_to_file
390            .borrow_mut()
391            .get_mut(filename)
392            .map(f)
393    }
394
395    fn with_new_file<E, F: Fn(&SyncAccessFile) -> Result<(), E>>(
396        &self,
397        filename: &str,
398        flags: i32,
399        f: F,
400    ) -> Result<Result<(), E>> {
401        let mut map_filename_to_file = self.map_filename_to_file.borrow_mut();
402        let mut available_files = self.available_files.borrow_mut();
403        if map_filename_to_file.contains_key(filename) {
404            return Err(OpfsSAHError::Generic(format!(
405                "{filename} file already exists"
406            )));
407        }
408        let file = available_files
409            .pop()
410            .ok_or_else(|| OpfsSAHError::Generic("No files available in the pool".into()))?;
411        map_filename_to_file.insert(filename.into(), file);
412
413        let Some(file) = map_filename_to_file.get(filename) else {
414            unreachable!();
415        };
416        self.set_associated_filename(&file.handle, Some(filename), flags)?;
417        Ok(f(file))
418    }
419
420    fn pause_vfs(&self) -> Result<()> {
421        if self.is_paused.get() {
422            return Ok(());
423        }
424
425        if !self.open_files.borrow().is_empty() {
426            return Err(OpfsSAHError::Generic(
427                "Cannot pause: files may be in use".to_string(),
428            ));
429        }
430
431        let (vfs, _) = self.vfs.get();
432        if !vfs.is_null() {
433            unsafe {
434                sqlite3_vfs_unregister(vfs);
435            }
436        }
437        self.release_access_handles();
438
439        self.is_paused.set(true);
440
441        Ok(())
442    }
443
444    async fn unpause_vfs(&self) -> Result<()> {
445        if !self.is_paused.get() {
446            return Ok(());
447        }
448
449        self.acquire_access_handles(false).await?;
450
451        let (vfs, make_default) = self.vfs.get();
452        if vfs.is_null() {
453            return Err(OpfsSAHError::Generic(
454                "VFS pointer is null. Did you forget to install?".to_string(),
455            ));
456        }
457
458        match unsafe { sqlite3_vfs_register(vfs, i32::from(make_default)) } {
459            SQLITE_OK => {
460                self.is_paused.set(false);
461                Ok(())
462            }
463            error_code => Err(OpfsSAHError::Generic(format!(
464                "Failed to register VFS (SQLite error code: {error_code})"
465            ))),
466        }
467    }
468
469    fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
470        let files = self.map_filename_to_file.borrow();
471        let file = files
472            .get(filename)
473            .ok_or_else(|| OpfsSAHError::Generic(format!("File not found: {filename}")))?;
474
475        let sah = &file.handle;
476        let actual_size = (sah.get_size().map_err(OpfsSAHError::GetSize)?
477            - HEADER_OFFSET_DATA as f64)
478            .max(0.0) as usize;
479
480        let mut data = vec![0; actual_size];
481        if actual_size > 0 {
482            let read = sah
483                .read_with_u8_array_and_options(
484                    &mut data,
485                    &read_write_options(HEADER_OFFSET_DATA as f64),
486                )
487                .map_err(OpfsSAHError::Read)?;
488            if read != actual_size as f64 {
489                return Err(OpfsSAHError::Generic(format!(
490                    "Expected to read {actual_size} bytes but read {read}.",
491                )));
492            }
493        }
494        Ok(data)
495    }
496
497    fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<()> {
498        check_import_db(bytes)?;
499        self.import_db_unchecked(filename, bytes, true)
500    }
501
502    fn import_db_unchecked(&self, filename: &str, bytes: &[u8], clear_wal: bool) -> Result<()> {
503        self.with_new_file(filename, SQLITE_OPEN_MAIN_DB, |file| {
504            let sah = &file.handle;
505            let length = bytes.len() as f64;
506            let written = sah
507                .write_with_u8_array_and_options(
508                    bytes,
509                    &read_write_options(HEADER_OFFSET_DATA as f64),
510                )
511                .map_err(OpfsSAHError::Write)?;
512
513            if written != length {
514                return Err(OpfsSAHError::Generic(format!(
515                    "Expected to write {length} bytes but wrote {written}.",
516                )));
517            }
518
519            if clear_wal {
520                // forced to write back to legacy mode
521                sah.write_with_u8_array_and_options(
522                    &[1, 1],
523                    &read_write_options((HEADER_OFFSET_DATA + 18) as f64),
524                )
525                .map_err(OpfsSAHError::Write)?;
526            }
527
528            Ok(())
529        })?
530    }
531}
532
533impl VfsFile for SyncAccessFile {
534    fn read(&self, buf: &mut [u8], offset: usize) -> VfsResult<bool> {
535        let n_read = self
536            .handle
537            .read_with_u8_array_and_options(
538                buf,
539                &read_write_options((HEADER_OFFSET_DATA + offset) as f64),
540            )
541            .map_err(OpfsSAHError::Read)
542            .map_err(|err| err.vfs_err(SQLITE_IOERR))?;
543
544        if (n_read as usize) < buf.len() {
545            buf[n_read as usize..].fill(0);
546            return Ok(false);
547        }
548
549        Ok(true)
550    }
551
552    fn write(&mut self, buf: &[u8], offset: usize) -> VfsResult<()> {
553        let n_write = self
554            .handle
555            .write_with_u8_array_and_options(
556                buf,
557                &read_write_options((HEADER_OFFSET_DATA + offset) as f64),
558            )
559            .map_err(OpfsSAHError::Write)
560            .map_err(|err| err.vfs_err(SQLITE_IOERR))?;
561
562        if buf.len() != n_write as usize {
563            return Err(VfsError::new(SQLITE_ERROR, "failed to write file".into()));
564        }
565
566        Ok(())
567    }
568
569    fn truncate(&mut self, size: usize) -> VfsResult<()> {
570        self.handle
571            .truncate_with_f64((HEADER_OFFSET_DATA + size) as f64)
572            .map_err(OpfsSAHError::Truncate)
573            .map_err(|err| err.vfs_err(SQLITE_IOERR))
574    }
575
576    fn flush(&mut self) -> VfsResult<()> {
577        FileSystemSyncAccessHandle::flush(&self.handle)
578            .map_err(OpfsSAHError::Flush)
579            .map_err(|err| err.vfs_err(SQLITE_IOERR))
580    }
581
582    fn size(&self) -> VfsResult<usize> {
583        Ok(self
584            .handle
585            .get_size()
586            .map_err(OpfsSAHError::GetSize)
587            .map_err(|err| err.vfs_err(SQLITE_IOERR))? as usize
588            - HEADER_OFFSET_DATA)
589    }
590}
591
592type SyncAccessHandleAppData = OpfsSAHPool;
593
594struct SyncAccessHandleStore;
595
596impl VfsStore<SyncAccessFile, SyncAccessHandleAppData> for SyncAccessHandleStore {
597    fn add_file(vfs: *mut sqlite3_vfs, filename: &str, flags: i32) -> VfsResult<()> {
598        let pool = unsafe { Self::app_data(vfs) };
599
600        pool.with_new_file(filename, flags, |_| Ok(()))
601            .map_err(|err| err.vfs_err(SQLITE_CANTOPEN))?
602    }
603
604    fn contains_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<bool> {
605        let pool = unsafe { Self::app_data(vfs) };
606        Ok(pool.has_filename(file))
607    }
608
609    fn delete_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<()> {
610        let pool = unsafe { Self::app_data(vfs) };
611        pool.delete_file(file)
612            .map_err(|err| err.vfs_err(SQLITE_IOERR_DELETE))?;
613        Ok(())
614    }
615
616    fn with_file<F: Fn(&SyncAccessFile) -> VfsResult<i32>>(
617        vfs_file: &SQLiteVfsFile,
618        f: F,
619    ) -> VfsResult<i32> {
620        let name = unsafe { vfs_file.name() };
621        let pool = unsafe { Self::app_data(vfs_file.vfs) };
622        pool.with_file(name, f)
623            .ok_or_else(|| VfsError::new(SQLITE_IOERR, format!("{name} not found")))?
624    }
625
626    fn with_file_mut<F: Fn(&mut SyncAccessFile) -> VfsResult<i32>>(
627        vfs_file: &SQLiteVfsFile,
628        f: F,
629    ) -> VfsResult<i32> {
630        let name = unsafe { vfs_file.name() };
631        let pool = unsafe { Self::app_data(vfs_file.vfs) };
632        pool.with_file_mut(name, f)
633            .ok_or_else(|| VfsError::new(SQLITE_IOERR, format!("{name} not found")))?
634    }
635}
636
637struct SyncAccessHandleIoMethods;
638
639impl SQLiteIoMethods for SyncAccessHandleIoMethods {
640    type File = SyncAccessFile;
641    type AppData = SyncAccessHandleAppData;
642    type Store = SyncAccessHandleStore;
643
644    const VERSION: ::std::os::raw::c_int = 1;
645
646    unsafe extern "C" fn xSectorSize(_pFile: *mut sqlite3_file) -> ::std::os::raw::c_int {
647        SECTOR_SIZE as i32
648    }
649
650    unsafe extern "C" fn xCheckReservedLock(
651        _pFile: *mut sqlite3_file,
652        pResOut: *mut ::std::os::raw::c_int,
653    ) -> ::std::os::raw::c_int {
654        *pResOut = 1;
655        SQLITE_OK
656    }
657
658    unsafe extern "C" fn xDeviceCharacteristics(
659        _pFile: *mut sqlite3_file,
660    ) -> ::std::os::raw::c_int {
661        SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
662    }
663
664    unsafe extern "C" fn xClose(pFile: *mut sqlite3_file) -> ::std::os::raw::c_int {
665        let vfs_file = SQLiteVfsFile::from_file(pFile);
666        // The VFS file handle will be dropped, so we must clone the filename to use it after the drop.
667        let file = vfs_file.name().to_string();
668        let app_data = SyncAccessHandleStore::app_data(vfs_file.vfs);
669        let ret = Self::xCloseImpl(pFile);
670        if ret == SQLITE_OK {
671            let exist = app_data.open_files.borrow_mut().remove(&file);
672            debug_assert!(exist, "DB closed without open");
673        }
674        ret
675    }
676}
677
678struct SyncAccessHandleVfs<C>(PhantomData<C>);
679
680impl<C> SQLiteVfs<SyncAccessHandleIoMethods> for SyncAccessHandleVfs<C>
681where
682    C: OsCallback,
683{
684    const VERSION: ::std::os::raw::c_int = 2;
685    const MAX_PATH_SIZE: ::std::os::raw::c_int = HEADER_MAX_FILENAME_SIZE as _;
686
687    unsafe extern "C" fn xOpen(
688        pVfs: *mut sqlite3_vfs,
689        zName: sqlite3_filename,
690        pFile: *mut sqlite3_file,
691        flags: ::std::os::raw::c_int,
692        pOutFlags: *mut ::std::os::raw::c_int,
693    ) -> ::std::os::raw::c_int {
694        let ret = Self::xOpenImpl(pVfs, zName, pFile, flags, pOutFlags);
695        if ret == SQLITE_OK {
696            let app_data = SyncAccessHandleStore::app_data(pVfs);
697
698            // At this point, SQLite has allocated the pFile structure for us.
699            let vfs_file = SQLiteVfsFile::from_file(pFile);
700            app_data
701                .open_files
702                .borrow_mut()
703                .insert(vfs_file.name().into());
704        }
705        ret
706    }
707
708    fn sleep(dur: Duration) {
709        C::sleep(dur);
710    }
711
712    fn random(buf: &mut [u8]) {
713        C::random(buf);
714    }
715
716    fn epoch_timestamp_in_ms() -> i64 {
717        C::epoch_timestamp_in_ms()
718    }
719}
720
721/// Build `OpfsSAHPoolCfg`
722pub struct OpfsSAHPoolCfgBuilder(OpfsSAHPoolCfg);
723
724impl OpfsSAHPoolCfgBuilder {
725    pub fn new() -> Self {
726        Self(OpfsSAHPoolCfg::default())
727    }
728
729    /// The SQLite VFS name under which this pool's VFS is registered.
730    pub fn vfs_name(mut self, name: &str) -> Self {
731        self.0.vfs_name = name.into();
732        self
733    }
734
735    /// Specifies the OPFS directory name in which to store metadata for the `vfs_name`
736    pub fn directory(mut self, directory: &str) -> Self {
737        self.0.directory = directory.into();
738        self
739    }
740
741    /// If truthy, contents and filename mapping are removed from each SAH
742    /// as it is acquired during initalization of the VFS, leaving the VFS's
743    /// storage in a pristine state. Use this only for databases which need not
744    /// survive a page reload.
745    pub fn clear_on_init(mut self, set: bool) -> Self {
746        self.0.clear_on_init = set;
747        self
748    }
749
750    /// Specifies the default capacity of the VFS, i.e. the number of files
751    /// it may contain.
752    pub fn initial_capacity(mut self, cap: u32) -> Self {
753        self.0.initial_capacity = cap;
754        self
755    }
756
757    /// Build `OpfsSAHPoolCfg`.
758    pub fn build(self) -> OpfsSAHPoolCfg {
759        self.0
760    }
761}
762
763impl Default for OpfsSAHPoolCfgBuilder {
764    fn default() -> Self {
765        Self::new()
766    }
767}
768
769/// `OpfsSAHPool` options
770pub struct OpfsSAHPoolCfg {
771    /// The SQLite VFS name under which this pool's VFS is registered.
772    pub vfs_name: String,
773    /// Specifies the OPFS directory name in which to store metadata for the `vfs_name`.
774    pub directory: String,
775    /// If truthy, contents and filename mapping are removed from each SAH
776    /// as it is acquired during initalization of the VFS, leaving the VFS's
777    /// storage in a pristine state. Use this only for databases which need not
778    /// survive a page reload.
779    pub clear_on_init: bool,
780    /// Specifies the default capacity of the VFS, i.e. the number of files
781    /// it may contain.
782    pub initial_capacity: u32,
783}
784
785impl Default for OpfsSAHPoolCfg {
786    fn default() -> Self {
787        Self {
788            vfs_name: "opfs-sahpool".into(),
789            directory: ".opfs-sahpool".into(),
790            clear_on_init: false,
791            initial_capacity: 6,
792        }
793    }
794}
795
796#[derive(thiserror::Error, Debug)]
797pub enum OpfsSAHError {
798    #[error(transparent)]
799    Vfs(#[from] RegisterVfsError),
800    #[error(transparent)]
801    ImportDb(#[from] ImportDbError),
802    #[error("This vfs is only available in dedicated worker")]
803    NotSupported,
804    #[error("An error occurred while getting the directory handle")]
805    GetDirHandle(JsValue),
806    #[error("An error occurred while getting the file handle")]
807    GetFileHandle(JsValue),
808    #[error("An error occurred while creating sync access handle")]
809    CreateSyncAccessHandle(JsValue),
810    #[error("An error occurred while iterating")]
811    IterHandle(JsValue),
812    #[error("An error occurred while getting filename")]
813    GetPath(JsValue),
814    #[error("An error occurred while removing entity")]
815    RemoveEntity(JsValue),
816    #[error("An error occurred while getting size")]
817    GetSize(JsValue),
818    #[error("An error occurred while reading data")]
819    Read(JsValue),
820    #[error("An error occurred while writing data")]
821    Write(JsValue),
822    #[error("An error occurred while flushing data")]
823    Flush(JsValue),
824    #[error("An error occurred while truncating data")]
825    Truncate(JsValue),
826    #[error("An error occurred while getting data using reflect")]
827    Reflect(JsValue),
828    #[error("Generic error: {0}")]
829    Generic(String),
830}
831
832impl OpfsSAHError {
833    fn vfs_err(&self, code: i32) -> VfsError {
834        VfsError::new(code, format!("{self}"))
835    }
836}
837
838/// SAHPoolVfs management tool.
839pub struct OpfsSAHPoolUtil {
840    pool: &'static VfsAppData<SyncAccessHandleAppData>,
841}
842
843impl OpfsSAHPoolUtil {
844    /// Returns the number of files currently contained in the SAH pool.
845    pub fn get_capacity(&self) -> u32 {
846        self.pool.get_capacity()
847    }
848
849    /// Adds n entries to the current pool.
850    pub async fn add_capacity(&self, n: u32) -> Result<u32> {
851        self.pool.add_capacity(n).await
852    }
853
854    /// Removes up to n entries from the pool, with the caveat that
855    /// it can only remove currently-unused entries.
856    pub async fn reduce_capacity(&self, n: u32) -> Result<u32> {
857        self.pool.reduce_capacity(n).await
858    }
859
860    /// Removes up to n entries from the pool, with the caveat that it can only
861    /// remove currently-unused entries.
862    pub async fn reserve_minimum_capacity(&self, min: u32) -> Result<()> {
863        self.pool.reserve_minimum_capacity(min).await
864    }
865}
866
867impl OpfsSAHPoolUtil {
868    /// Imports the contents of an SQLite database, provided as a byte array
869    /// under the given name, overwriting any existing content.
870    ///
871    /// If the database is imported with WAL mode enabled,
872    /// it will be forced to write back to legacy mode, see
873    /// <https://sqlite.org/forum/forumpost/67882c5b04>.
874    ///
875    /// If the imported database is encrypted, use `import_db_unchecked` instead.
876    pub fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<()> {
877        self.pool.import_db(filename, bytes)
878    }
879
880    /// `import_db` without checking, can be used to import encrypted database.
881    pub fn import_db_unchecked(&self, filename: &str, bytes: &[u8]) -> Result<()> {
882        self.pool.import_db_unchecked(filename, bytes, false)
883    }
884
885    /// Export the database.
886    pub fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
887        self.pool.export_db(filename)
888    }
889
890    /// Delete the specified database, make sure that the database is closed.
891    pub fn delete_db(&self, filename: &str) -> Result<bool> {
892        self.pool.delete_file(filename)
893    }
894
895    /// Delete all database, make sure that all database is closed.
896    pub async fn clear_all(&self) -> Result<()> {
897        self.pool.release_access_handles();
898        self.pool.acquire_access_handles(true).await?;
899        Ok(())
900    }
901
902    /// Does the database exists.
903    pub fn exists(&self, filename: &str) -> Result<bool> {
904        Ok(self.pool.has_filename(filename))
905    }
906
907    /// List all files.
908    pub fn list(&self) -> Vec<String> {
909        self.pool.get_filenames()
910    }
911
912    /// Number of files.
913    pub fn count(&self) -> u32 {
914        self.pool.get_file_count()
915    }
916
917    /// "Pauses" this VFS by unregistering it from SQLite and
918    /// relinquishing all open SAHs, leaving the associated files
919    /// intact. If this instance is already paused, this is a
920    /// no-op. Returns a Result.
921    ///
922    /// This method returns an error if SQLite has any opened file handles
923    /// hosted by this VFS, as the alternative would be to invoke
924    /// Undefined Behavior by closing file handles out from under the
925    /// library. Similarly, automatically closing any database handles
926    /// opened by this VFS would invoke Undefined Behavior in
927    /// downstream code which is holding those pointers.
928    ///
929    /// If this method returns and error due to open file handles then it has
930    /// no side effects. If the OPFS API returns an error while closing handles
931    /// then the VFS is left in an undefined state.
932    pub fn pause_vfs(&self) -> Result<()> {
933        self.pool.pause_vfs()
934    }
935
936    /// "Unpauses" this VFS, reacquiring all SAH's and (if successful)
937    /// re-registering it with SQLite. This is a no-op if the VFS is
938    /// not currently paused.
939    ///
940    /// The returned a Result. See acquire_access_handles() for how it
941    /// behaves if it returns an error due to SAH acquisition failure.
942    pub async fn unpause_vfs(&self) -> Result<()> {
943        self.pool.unpause_vfs().await
944    }
945
946    /// Check if VFS is paused.
947    pub fn is_paused(&self) -> bool {
948        self.pool.is_paused.get()
949    }
950}
951
952/// Register `opfs-sahpool` vfs and return a management tool which can be used
953/// to perform basic administration of the file pool.
954///
955/// If the vfs corresponding to `options.vfs_name` has been registered,
956/// only return a management tool without register.
957pub async fn install<C: OsCallback>(
958    options: &OpfsSAHPoolCfg,
959    default_vfs: bool,
960) -> Result<OpfsSAHPoolUtil> {
961    static REGISTER_GUARD: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
962    let _guard = REGISTER_GUARD.lock().await;
963
964    let vfs = match registered_vfs(&options.vfs_name)? {
965        Some(vfs) => vfs,
966        None => register_vfs::<SyncAccessHandleIoMethods, SyncAccessHandleVfs<C>>(
967            &options.vfs_name,
968            OpfsSAHPool::new::<C>(options).await?,
969            default_vfs,
970        )?,
971    };
972
973    let pool = unsafe { SyncAccessHandleStore::app_data(vfs) };
974    pool.vfs.set((vfs, default_vfs));
975
976    Ok(OpfsSAHPoolUtil { pool })
977}
978
979#[cfg(test)]
980mod tests {
981    use super::{
982        OpfsSAHPool, OpfsSAHPoolCfgBuilder, SyncAccessFile, SyncAccessHandleAppData,
983        SyncAccessHandleStore,
984    };
985    use rsqlite_vfs::{test_suite::test_vfs_store, VfsAppData};
986    use wasm_bindgen_test::wasm_bindgen_test;
987
988    #[wasm_bindgen_test]
989    async fn test_opfs_vfs_store() {
990        let data = OpfsSAHPool::new::<sqlite_wasm_rs::WasmOsCallback>(
991            &OpfsSAHPoolCfgBuilder::new()
992                .directory("test_opfs_suite")
993                .build(),
994        )
995        .await
996        .unwrap();
997
998        test_vfs_store::<SyncAccessHandleAppData, SyncAccessFile, SyncAccessHandleStore>(
999            VfsAppData::new(data),
1000        )
1001        .unwrap();
1002    }
1003}