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