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