sqlite_wasm_rs/vfs/
sahpool.rs

1//! opfs-sahpool vfs implementation, ported from sqlite-wasm
2//!
3//! No large-scale reconstruction is required to facilitate future maintenance.
4//!
5//! <https://github.com/sqlite/sqlite/blob/master/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js>
6
7use crate::libsqlite3::*;
8use crate::utils::LazyCell;
9use crate::vfs::utils::{
10    check_import_db, random_name, register_vfs, ImportDbError, RegisterVfsError, SQLiteIoMethods,
11    SQLiteVfs, VfsAppData, VfsError, VfsFile, VfsResult, VfsStore,
12};
13
14use js_sys::{Array, DataView, IteratorNext, Map, Reflect, Set, Uint8Array};
15use std::collections::HashMap;
16use wasm_bindgen::{JsCast, JsValue};
17use wasm_bindgen_futures::JsFuture;
18use web_sys::{
19    FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions,
20    FileSystemGetFileOptions, FileSystemReadWriteOptions, FileSystemSyncAccessHandle,
21    WorkerGlobalScope,
22};
23
24const SECTOR_SIZE: usize = 4096;
25const HEADER_MAX_FILENAME_SIZE: usize = 512;
26const HEADER_FLAGS_SIZE: usize = 4;
27const HEADER_CORPUS_SIZE: usize = HEADER_MAX_FILENAME_SIZE + HEADER_FLAGS_SIZE;
28const HEADER_OFFSET_FLAGS: usize = HEADER_MAX_FILENAME_SIZE;
29const HEADER_OFFSET_DATA: usize = SECTOR_SIZE;
30
31// Will be enabled after sqlite fix is released
32//
33// <https://sqlite.org/src/forumpost/042d53c928382021>
34//
35// <https://github.com/sqlite/sqlite-wasm/issues/97>
36//
37// const HEADER_DIGEST_SIZE: usize = 8;
38// const HEADER_OFFSET_DIGEST: usize = HEADER_CORPUS_SIZE;
39
40const PERSISTENT_FILE_TYPES: i32 =
41    SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_MAIN_JOURNAL | SQLITE_OPEN_SUPER_JOURNAL | SQLITE_OPEN_WAL;
42
43type Result<T> = std::result::Result<T, OpfsSAHError>;
44
45fn read_write_options(at: f64) -> FileSystemReadWriteOptions {
46    let options = FileSystemReadWriteOptions::new();
47    options.set_at(at);
48    options
49}
50
51struct OpfsSAHPool {
52    /// Directory handle to the subdir of vfs root which holds
53    /// the randomly-named "opaque" files. This subdir exists in the
54    /// hope that we can eventually support client-created files in
55    dh_opaque: FileSystemDirectoryHandle,
56    /// Buffer used by [sg]et_associated_filename()
57    header_buffer: Uint8Array,
58    /// DataView for self.header_buffer
59    header_buffer_view: DataView,
60    /// Set of currently-unused SAHs
61    available_sah: Set,
62    /// Maps client-side file names to SAHs
63    map_filename_to_sah: Map,
64    /// Maps SAHs to their opaque file names
65    map_sah_to_opaque_name: Map,
66}
67
68impl OpfsSAHPool {
69    async fn new(options: &OpfsSAHPoolCfg) -> Result<OpfsSAHPool> {
70        const OPAQUE_DIR_NAME: &str = ".opaque";
71
72        let vfs_dir = &options.directory;
73        let capacity = options.initial_capacity;
74        let clear_files = options.clear_on_init;
75
76        let create_option = FileSystemGetDirectoryOptions::new();
77        create_option.set_create(true);
78
79        let mut handle: FileSystemDirectoryHandle = JsFuture::from(
80            js_sys::global()
81                .dyn_into::<WorkerGlobalScope>()
82                .map_err(|_| OpfsSAHError::NotSuported)?
83                .navigator()
84                .storage()
85                .get_directory(),
86        )
87        .await
88        .map_err(OpfsSAHError::GetDirHandle)?
89        .into();
90
91        for dir in vfs_dir.split('/').filter(|x| !x.is_empty()) {
92            let next =
93                JsFuture::from(handle.get_directory_handle_with_options(dir, &create_option))
94                    .await
95                    .map_err(OpfsSAHError::GetDirHandle)?
96                    .into();
97            handle = next;
98        }
99
100        let dh_opaque = JsFuture::from(
101            handle.get_directory_handle_with_options(OPAQUE_DIR_NAME, &create_option),
102        )
103        .await
104        .map_err(OpfsSAHError::GetDirHandle)?
105        .into();
106
107        let ap_body = Uint8Array::new_with_length(HEADER_CORPUS_SIZE as _);
108        let dv_body = DataView::new(
109            &ap_body.buffer(),
110            ap_body.byte_offset() as usize,
111            (ap_body.byte_length() - ap_body.byte_offset()) as usize,
112        );
113
114        let pool = Self {
115            dh_opaque,
116            header_buffer: ap_body,
117            header_buffer_view: dv_body,
118            map_filename_to_sah: Map::new(),
119            available_sah: Set::default(),
120            map_sah_to_opaque_name: Map::new(),
121        };
122
123        pool.acquire_access_handles(clear_files).await?;
124        pool.reserve_minimum_capacity(capacity).await?;
125
126        Ok(pool)
127    }
128
129    async fn add_capacity(&self, n: u32) -> Result<u32> {
130        for _ in 0..n {
131            let name = random_name();
132            let handle: FileSystemFileHandle =
133                JsFuture::from(self.dh_opaque.get_file_handle_with_options(&name, &{
134                    let options = FileSystemGetFileOptions::new();
135                    options.set_create(true);
136                    options
137                }))
138                .await
139                .map_err(OpfsSAHError::GetFileHandle)?
140                .into();
141            let sah: FileSystemSyncAccessHandle =
142                JsFuture::from(handle.create_sync_access_handle())
143                    .await
144                    .map_err(OpfsSAHError::CreateSyncAccessHandle)?
145                    .into();
146            self.map_sah_to_opaque_name.set(&sah, &JsValue::from(name));
147            self.set_associated_filename(&sah, None, 0)?;
148        }
149        Ok(self.get_capacity())
150    }
151
152    async fn reserve_minimum_capacity(&self, min: u32) -> Result<()> {
153        self.add_capacity(min.saturating_sub(self.get_capacity()))
154            .await?;
155        Ok(())
156    }
157
158    async fn reduce_capacity(&self, n: u32) -> Result<u32> {
159        let mut result = 0;
160        for sah in Array::from(&self.available_sah) {
161            if result == n || self.get_capacity() == self.get_file_count() {
162                break;
163            }
164            let sah = FileSystemSyncAccessHandle::from(sah);
165            let name = self.map_sah_to_opaque_name.get(&sah).as_string().unwrap();
166
167            sah.close();
168            JsFuture::from(self.dh_opaque.remove_entry(&name))
169                .await
170                .map_err(OpfsSAHError::RemoveEntity)?;
171            self.map_sah_to_opaque_name.delete(&sah);
172            self.available_sah.delete(&sah);
173
174            result += 1;
175        }
176        Ok(result)
177    }
178
179    fn get_capacity(&self) -> u32 {
180        self.map_sah_to_opaque_name.size()
181    }
182
183    fn get_file_count(&self) -> u32 {
184        self.map_filename_to_sah.size()
185    }
186
187    fn get_filenames(&self) -> Vec<String> {
188        self.map_filename_to_sah
189            .keys()
190            .into_iter()
191            .flatten()
192            .map(|x| x.as_string().unwrap())
193            .collect()
194    }
195
196    fn get_associated_filename(&self, sah: &FileSystemSyncAccessHandle) -> Result<Option<String>> {
197        sah.read_with_buffer_source_and_options(&self.header_buffer, &read_write_options(0.0))
198            .map_err(OpfsSAHError::Read)?;
199        let flags = self.header_buffer_view.get_uint32(HEADER_OFFSET_FLAGS);
200        if self.header_buffer.get_index(0) != 0
201            && ((flags & SQLITE_OPEN_DELETEONCLOSE as u32 != 0)
202                || (flags & PERSISTENT_FILE_TYPES as u32) == 0)
203        {
204            self.set_associated_filename(sah, None, 0)?;
205            return Ok(None);
206        }
207
208        let name_length = Array::from(&self.header_buffer)
209            .iter()
210            .position(|x| x.as_f64().unwrap() as u8 == 0)
211            .unwrap_or_default();
212        if name_length == 0 {
213            sah.truncate_with_u32(HEADER_OFFSET_DATA as u32)
214                .map_err(OpfsSAHError::Truncate)?;
215            return Ok(None);
216        }
217        // set_associated_filename ensures that it is utf8
218        let filename =
219            String::from_utf8(self.header_buffer.subarray(0, name_length as u32).to_vec()).unwrap();
220        Ok(Some(filename))
221    }
222
223    fn set_associated_filename(
224        &self,
225        sah: &FileSystemSyncAccessHandle,
226        filename: Option<&str>,
227        flags: i32,
228    ) -> Result<()> {
229        self.header_buffer_view
230            .set_uint32(HEADER_OFFSET_FLAGS, flags as u32);
231
232        if let Some(filename) = filename {
233            if filename.is_empty() {
234                return Err(OpfsSAHError::Generic("Filename is empty".into()));
235            }
236            if HEADER_MAX_FILENAME_SIZE <= filename.len() + 1 {
237                return Err(OpfsSAHError::Generic(format!(
238                    "Filename too long: {filename}"
239                )));
240            }
241            self.header_buffer
242                .subarray(0, filename.len() as u32)
243                .copy_from(filename.as_bytes());
244            self.header_buffer
245                .fill(0, filename.len() as u32, HEADER_MAX_FILENAME_SIZE as u32);
246            self.map_filename_to_sah.set(&JsValue::from(filename), sah);
247            self.available_sah.delete(sah);
248        } else {
249            self.header_buffer
250                .fill(0, 0, HEADER_MAX_FILENAME_SIZE as u32);
251            sah.truncate_with_u32(HEADER_OFFSET_DATA as u32)
252                .map_err(OpfsSAHError::Truncate)?;
253            self.available_sah.add(sah);
254        }
255
256        sah.write_with_js_u8_array_and_options(&self.header_buffer, &read_write_options(0.0))
257            .map_err(OpfsSAHError::Write)?;
258
259        Ok(())
260    }
261
262    async fn acquire_access_handles(&self, clear_files: bool) -> Result<()> {
263        let iter = self.dh_opaque.entries();
264        while let Ok(future) = iter.next() {
265            let next: IteratorNext = JsFuture::from(future)
266                .await
267                .map_err(OpfsSAHError::IterHandle)?
268                .into();
269            if next.done() {
270                break;
271            }
272            let array: Array = next.value().into();
273            let name = array.get(0);
274            let value = array.get(1);
275            let kind = Reflect::get(&value, &JsValue::from("kind"))
276                .map_err(OpfsSAHError::Reflect)?
277                .as_string();
278            if kind.as_deref() == Some("file") {
279                let handle = FileSystemFileHandle::from(value);
280                let sah = JsFuture::from(handle.create_sync_access_handle())
281                    .await
282                    .map_err(OpfsSAHError::CreateSyncAccessHandle)?;
283                self.map_sah_to_opaque_name.set(&sah, &name);
284                let sah = FileSystemSyncAccessHandle::from(sah);
285                if clear_files {
286                    self.set_associated_filename(&sah, None, 0)?;
287                } else if let Some(filename) = self.get_associated_filename(&sah)? {
288                    self.map_filename_to_sah.set(&JsValue::from(filename), &sah);
289                } else {
290                    self.available_sah.add(&sah);
291                }
292            }
293        }
294
295        Ok(())
296    }
297
298    fn release_access_handles(&self) {
299        for sah in self.map_sah_to_opaque_name.keys().into_iter().flatten() {
300            let sah = FileSystemSyncAccessHandle::from(sah);
301            sah.close();
302        }
303        self.map_sah_to_opaque_name.clear();
304        self.map_filename_to_sah.clear();
305        self.available_sah.clear();
306    }
307
308    fn delete_file(&self, filename: &str) -> Result<bool> {
309        let sah = self.map_filename_to_sah.get(&JsValue::from(filename));
310        let found = !sah.is_undefined();
311        if found {
312            let sah: FileSystemSyncAccessHandle = sah.into();
313            self.map_filename_to_sah.delete(&JsValue::from(filename));
314            self.set_associated_filename(&sah, None, 0)?;
315        }
316        Ok(found)
317    }
318
319    fn has_filename(&self, filename: &str) -> bool {
320        self.map_filename_to_sah.has(&JsValue::from(filename))
321    }
322
323    fn get_sah(&self, filename: &str) -> Option<FileSystemSyncAccessHandle> {
324        self.has_filename(filename).then(|| {
325            self.map_filename_to_sah
326                .get(&JsValue::from(filename))
327                .into()
328        })
329    }
330
331    fn next_available_sah(&self) -> Option<FileSystemSyncAccessHandle> {
332        self.available_sah
333            .keys()
334            .next()
335            .ok()
336            .filter(|x| !x.done())
337            .map(|x| x.value().into())
338    }
339
340    fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
341        let sah = self.map_filename_to_sah.get(&JsValue::from(filename));
342        if sah.is_undefined() {
343            return Err(OpfsSAHError::Generic(format!("File not found: {filename}")));
344        }
345
346        let sah = FileSystemSyncAccessHandle::from(sah);
347        let actual_size = (sah.get_size().map_err(OpfsSAHError::GetSize)?
348            - HEADER_OFFSET_DATA as f64)
349            .max(0.0) as usize;
350
351        let mut data = vec![0; actual_size];
352        if actual_size > 0 {
353            let read = sah
354                .read_with_u8_array_and_options(
355                    &mut data,
356                    &read_write_options(HEADER_OFFSET_DATA as f64),
357                )
358                .map_err(OpfsSAHError::Read)?;
359            if read != actual_size as f64 {
360                return Err(OpfsSAHError::Generic(format!(
361                    "Expected to read {actual_size} bytes but read {read}.",
362                )));
363            }
364        }
365        Ok(data)
366    }
367
368    fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<()> {
369        check_import_db(bytes)?;
370        self.import_db_unchecked(filename, bytes, true)
371    }
372
373    fn import_db_unchecked(&self, filename: &str, bytes: &[u8], clear_wal: bool) -> Result<()> {
374        if self.has_filename(filename) {
375            return Err(OpfsSAHError::Generic(format!(
376                "{filename} file already exists."
377            )));
378        }
379
380        let sah = self
381            .next_available_sah()
382            .ok_or_else(|| OpfsSAHError::Generic("No available handles to import to.".into()))?;
383
384        let length = bytes.len() as f64;
385        let written = sah
386            .write_with_u8_array_and_options(bytes, &read_write_options(HEADER_OFFSET_DATA as f64))
387            .map_err(OpfsSAHError::Write)?;
388        if written != length {
389            self.set_associated_filename(&sah, None, 0)?;
390            return Err(OpfsSAHError::Generic(format!(
391                "Expected to write {length} bytes but wrote {written}.",
392            )));
393        }
394
395        if clear_wal {
396            // forced to write back to legacy mode
397            sah.write_with_u8_array_and_options(
398                &[1, 1],
399                &read_write_options((HEADER_OFFSET_DATA + 18) as f64),
400            )
401            .map_err(OpfsSAHError::Write)?;
402        }
403
404        self.set_associated_filename(&sah, Some(filename), SQLITE_OPEN_MAIN_DB)?;
405
406        Ok(())
407    }
408}
409
410impl VfsFile for FileSystemSyncAccessHandle {
411    fn read(&self, buf: &mut [u8], offset: usize) -> VfsResult<bool> {
412        let n_read = self
413            .read_with_u8_array_and_options(
414                buf,
415                &read_write_options((HEADER_OFFSET_DATA + offset) as f64),
416            )
417            .map_err(OpfsSAHError::Read)
418            .map_err(|err| err.vfs_err(SQLITE_IOERR))?;
419
420        if (n_read as usize) < buf.len() {
421            buf[n_read as usize..].fill(0);
422            return Ok(false);
423        }
424
425        Ok(true)
426    }
427
428    fn write(&mut self, buf: &[u8], offset: usize) -> VfsResult<()> {
429        let n_write = self
430            .write_with_u8_array_and_options(
431                buf,
432                &read_write_options((HEADER_OFFSET_DATA + offset) as f64),
433            )
434            .map_err(OpfsSAHError::Write)
435            .map_err(|err| err.vfs_err(SQLITE_IOERR))?;
436
437        if buf.len() != n_write as usize {
438            return Err(VfsError::new(SQLITE_ERROR, "failed to write file".into()));
439        }
440
441        Ok(())
442    }
443
444    fn truncate(&mut self, size: usize) -> VfsResult<()> {
445        self.truncate_with_f64((HEADER_OFFSET_DATA + size) as f64)
446            .map_err(OpfsSAHError::Truncate)
447            .map_err(|err| err.vfs_err(SQLITE_IOERR))
448    }
449
450    fn flush(&mut self) -> VfsResult<()> {
451        FileSystemSyncAccessHandle::flush(self)
452            .map_err(OpfsSAHError::Flush)
453            .map_err(|err| err.vfs_err(SQLITE_IOERR))
454    }
455
456    fn size(&self) -> VfsResult<usize> {
457        Ok(self
458            .get_size()
459            .map_err(OpfsSAHError::GetSize)
460            .map_err(|err| err.vfs_err(SQLITE_IOERR))? as usize
461            - HEADER_OFFSET_DATA)
462    }
463}
464
465type SyncAccessHandleAppData = OpfsSAHPool;
466
467struct SyncAccessHandleStore;
468
469impl VfsStore<FileSystemSyncAccessHandle, SyncAccessHandleAppData> for SyncAccessHandleStore {
470    fn add_file(vfs: *mut sqlite3_vfs, file: &str, flags: i32) -> VfsResult<()> {
471        let pool = unsafe { Self::app_data(vfs) };
472
473        if let Some(sah) = pool.next_available_sah() {
474            pool.set_associated_filename(&sah, Some(file), flags)
475                .map_err(|err| err.vfs_err(SQLITE_CANTOPEN))?;
476        } else {
477            return Err(VfsError::new(
478                SQLITE_CANTOPEN,
479                "SAH pool is full. Cannot create file".into(),
480            ));
481        };
482
483        Ok(())
484    }
485
486    fn contains_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<bool> {
487        let pool = unsafe { Self::app_data(vfs) };
488        Ok(pool.has_filename(file))
489    }
490
491    fn delete_file(vfs: *mut sqlite3_vfs, file: &str) -> VfsResult<()> {
492        let pool = unsafe { Self::app_data(vfs) };
493        if let Err(err) = pool.delete_file(file) {
494            return Err(err.vfs_err(SQLITE_IOERR_DELETE));
495        }
496        Ok(())
497    }
498
499    fn with_file<F: Fn(&FileSystemSyncAccessHandle) -> VfsResult<i32>>(
500        vfs_file: &super::utils::SQLiteVfsFile,
501        f: F,
502    ) -> VfsResult<i32> {
503        let name = unsafe { vfs_file.name() };
504        let pool = unsafe { Self::app_data(vfs_file.vfs) };
505        match pool.get_sah(name) {
506            Some(file) => f(&file),
507            None => Err(VfsError::new(SQLITE_IOERR, format!("{name} not found"))),
508        }
509    }
510
511    fn with_file_mut<F: Fn(&mut FileSystemSyncAccessHandle) -> VfsResult<i32>>(
512        vfs_file: &super::utils::SQLiteVfsFile,
513        f: F,
514    ) -> VfsResult<i32> {
515        let name = unsafe { vfs_file.name() };
516        let pool = unsafe { Self::app_data(vfs_file.vfs) };
517        match pool.get_sah(name) {
518            Some(mut file) => f(&mut file),
519            None => Err(VfsError::new(SQLITE_IOERR, format!("{name} not found"))),
520        }
521    }
522}
523
524struct SyncAccessHandleIoMethods;
525
526impl SQLiteIoMethods for SyncAccessHandleIoMethods {
527    type File = FileSystemSyncAccessHandle;
528    type AppData = SyncAccessHandleAppData;
529    type Store = SyncAccessHandleStore;
530
531    const VERSION: ::std::os::raw::c_int = 1;
532
533    unsafe extern "C" fn xSectorSize(_pFile: *mut sqlite3_file) -> ::std::os::raw::c_int {
534        SECTOR_SIZE as i32
535    }
536
537    unsafe extern "C" fn xCheckReservedLock(
538        _pFile: *mut sqlite3_file,
539        pResOut: *mut ::std::os::raw::c_int,
540    ) -> ::std::os::raw::c_int {
541        *pResOut = 1;
542        SQLITE_OK
543    }
544
545    unsafe extern "C" fn xDeviceCharacteristics(
546        _pFile: *mut sqlite3_file,
547    ) -> ::std::os::raw::c_int {
548        SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
549    }
550}
551
552struct SyncAccessHandleVfs;
553
554impl SQLiteVfs<SyncAccessHandleIoMethods> for SyncAccessHandleVfs {
555    const VERSION: ::std::os::raw::c_int = 2;
556    const MAX_PATH_SIZE: ::std::os::raw::c_int = HEADER_MAX_FILENAME_SIZE as _;
557}
558
559/// Build `OpfsSAHPoolCfg`
560pub struct OpfsSAHPoolCfgBuilder(OpfsSAHPoolCfg);
561
562impl OpfsSAHPoolCfgBuilder {
563    pub fn new() -> Self {
564        Self(OpfsSAHPoolCfg::default())
565    }
566
567    /// The SQLite VFS name under which this pool's VFS is registered.
568    pub fn vfs_name(mut self, name: &str) -> Self {
569        self.0.vfs_name = name.into();
570        self
571    }
572
573    /// Specifies the OPFS directory name in which to store metadata for the `vfs_name`
574    pub fn directory(mut self, directory: &str) -> Self {
575        self.0.directory = directory.into();
576        self
577    }
578
579    /// If truthy, contents and filename mapping are removed from each SAH
580    /// as it is acquired during initalization of the VFS, leaving the VFS's
581    /// storage in a pristine state. Use this only for databases which need not
582    /// survive a page reload.
583    pub fn clear_on_init(mut self, set: bool) -> Self {
584        self.0.clear_on_init = set;
585        self
586    }
587
588    /// Specifies the default capacity of the VFS, i.e. the number of files
589    /// it may contain.
590    pub fn initial_capacity(mut self, cap: u32) -> Self {
591        self.0.initial_capacity = cap;
592        self
593    }
594
595    /// Build OpfsSAHPoolCfg
596    pub fn build(self) -> OpfsSAHPoolCfg {
597        self.0
598    }
599}
600
601impl Default for OpfsSAHPoolCfgBuilder {
602    fn default() -> Self {
603        Self::new()
604    }
605}
606
607/// `OpfsSAHPool` options
608pub struct OpfsSAHPoolCfg {
609    /// The SQLite VFS name under which this pool's VFS is registered.
610    pub vfs_name: String,
611    /// Specifies the OPFS directory name in which to store metadata for the `vfs_name`
612    pub directory: String,
613    /// If truthy, contents and filename mapping are removed from each SAH
614    /// as it is acquired during initalization of the VFS, leaving the VFS's
615    /// storage in a pristine state. Use this only for databases which need not
616    /// survive a page reload.
617    pub clear_on_init: bool,
618    /// Specifies the default capacity of the VFS, i.e. the number of files
619    /// it may contain.
620    pub initial_capacity: u32,
621}
622
623impl Default for OpfsSAHPoolCfg {
624    fn default() -> Self {
625        Self {
626            vfs_name: "opfs-sahpool".into(),
627            directory: ".opfs-sahpool".into(),
628            clear_on_init: false,
629            initial_capacity: 6,
630        }
631    }
632}
633
634#[derive(thiserror::Error, Debug)]
635pub enum OpfsSAHError {
636    #[error(transparent)]
637    Vfs(#[from] RegisterVfsError),
638    #[error(transparent)]
639    ImportDb(#[from] ImportDbError),
640    #[error("This vfs is only available in dedicated worker")]
641    NotSuported,
642    #[error("An error occurred while getting the directory handle")]
643    GetDirHandle(JsValue),
644    #[error("An error occurred while getting the file handle")]
645    GetFileHandle(JsValue),
646    #[error("An error occurred while creating sync access handle")]
647    CreateSyncAccessHandle(JsValue),
648    #[error("An error occurred while iterating")]
649    IterHandle(JsValue),
650    #[error("An error occurred while getting filename")]
651    GetPath(JsValue),
652    #[error("An error occurred while removing entity")]
653    RemoveEntity(JsValue),
654    #[error("An error occurred while getting size")]
655    GetSize(JsValue),
656    #[error("An error occurred while reading data")]
657    Read(JsValue),
658    #[error("An error occurred while writing data")]
659    Write(JsValue),
660    #[error("An error occurred while flushing data")]
661    Flush(JsValue),
662    #[error("An error occurred while truncating data")]
663    Truncate(JsValue),
664    #[error("An error occurred while getting data using reflect")]
665    Reflect(JsValue),
666    #[error("Generic error: {0}")]
667    Generic(String),
668}
669
670impl OpfsSAHError {
671    fn vfs_err(&self, code: i32) -> VfsError {
672        VfsError::new(code, format!("{self}"))
673    }
674}
675
676/// A OpfsSAHPoolUtil instance is exposed to clients in order to
677/// manipulate an OpfsSAHPool object without directly exposing that
678/// object and allowing for some semantic changes compared to that
679/// class.
680pub struct OpfsSAHPoolUtil {
681    pool: &'static VfsAppData<SyncAccessHandleAppData>,
682}
683
684unsafe impl Send for OpfsSAHPoolUtil {}
685
686unsafe impl Sync for OpfsSAHPoolUtil {}
687
688impl OpfsSAHPoolUtil {
689    /// Returns the number of files currently contained in the SAH pool.
690    pub fn get_capacity(&self) -> u32 {
691        self.pool.get_capacity()
692    }
693
694    /// Adds n entries to the current pool.
695    pub async fn add_capacity(&self, n: u32) -> Result<u32> {
696        self.pool.add_capacity(n).await
697    }
698
699    /// Removes up to n entries from the pool, with the caveat that
700    /// it can only remove currently-unused entries.
701    pub async fn reduce_capacity(&self, n: u32) -> Result<u32> {
702        self.pool.reduce_capacity(n).await
703    }
704
705    /// Removes up to n entries from the pool, with the caveat that it can only
706    /// remove currently-unused entries.
707    pub async fn reserve_minimum_capacity(&self, min: u32) -> Result<()> {
708        self.pool.reserve_minimum_capacity(min).await
709    }
710}
711
712impl OpfsSAHPoolUtil {
713    /// Imports the contents of an SQLite database, provided as a byte array
714    /// under the given name, overwriting any existing content.
715    ///
716    /// If the database is imported with WAL mode enabled,
717    /// it will be forced to write back to legacy mode, see
718    /// <https://sqlite.org/forum/forumpost/67882c5b04>.
719    ///
720    /// If the imported database is encrypted, use `import_db_unchecked` instead.
721    pub fn import_db(&self, filename: &str, bytes: &[u8]) -> Result<()> {
722        self.pool.import_db(filename, bytes)
723    }
724
725    /// `import_db` without checking, can be used to import encrypted database.
726    pub fn import_db_unchecked(&self, filename: &str, bytes: &[u8]) -> Result<()> {
727        self.pool.import_db_unchecked(filename, bytes, false)
728    }
729
730    /// Export the database.
731    pub fn export_db(&self, filename: &str) -> Result<Vec<u8>> {
732        self.pool.export_db(filename)
733    }
734
735    /// Delete the specified database, please make sure that the database is closed.
736    pub fn delete_db(&self, filename: &str) -> Result<bool> {
737        self.pool.delete_file(filename)
738    }
739
740    /// Delete all database, please make sure that all database is closed.
741    pub async fn clear_all(&self) -> Result<()> {
742        self.pool.release_access_handles();
743        self.pool.acquire_access_handles(true).await?;
744        Ok(())
745    }
746
747    /// Does the database exists.
748    pub fn exists(&self, filename: &str) -> Result<bool> {
749        Ok(self.pool.has_filename(filename))
750    }
751
752    /// List all files.
753    pub fn list(&self) -> Vec<String> {
754        self.pool.get_filenames()
755    }
756
757    /// Number of files.
758    pub fn count(&self) -> u32 {
759        self.pool.get_file_count()
760    }
761}
762
763/// Register `opfs-sahpool` vfs and return a utility object which can be used
764/// to perform basic administration of the file pool
765pub async fn install(options: &OpfsSAHPoolCfg, default_vfs: bool) -> Result<OpfsSAHPoolUtil> {
766    #[cfg_attr(target_feature = "atomics", thread_local)]
767    static NAME2VFS: LazyCell<
768        tokio::sync::Mutex<HashMap<String, &'static VfsAppData<SyncAccessHandleAppData>>>,
769    > = LazyCell::new(|| tokio::sync::Mutex::new(HashMap::new()));
770
771    let mut name2vfs = NAME2VFS.lock().await;
772
773    let vfs_name = &options.vfs_name;
774    let pool = if let Some(pool) = name2vfs.get(vfs_name) {
775        pool
776    } else {
777        let pool = OpfsSAHPool::new(options).await?;
778        let vfs = register_vfs::<SyncAccessHandleIoMethods, SyncAccessHandleVfs>(
779            vfs_name,
780            pool,
781            default_vfs,
782        )?;
783        let pool = unsafe { SyncAccessHandleStore::app_data(vfs) };
784        name2vfs.insert(vfs_name.clone(), pool);
785        pool
786    };
787
788    let util = OpfsSAHPoolUtil { pool };
789
790    Ok(util)
791}
792
793#[cfg(test)]
794mod tests {
795    use crate::{
796        sahpool_vfs::{
797            OpfsSAHPool, OpfsSAHPoolCfgBuilder, SyncAccessHandleAppData, SyncAccessHandleStore,
798        },
799        utils::{test_suite::test_vfs_store, VfsAppData},
800    };
801    use wasm_bindgen_test::wasm_bindgen_test;
802    use web_sys::FileSystemSyncAccessHandle;
803
804    #[wasm_bindgen_test]
805    async fn test_opfs_vfs_store() {
806        let data = OpfsSAHPool::new(
807            &OpfsSAHPoolCfgBuilder::new()
808                .directory("test_opfs_suite")
809                .build(),
810        )
811        .await
812        .unwrap();
813
814        test_vfs_store::<SyncAccessHandleAppData, FileSystemSyncAccessHandle, SyncAccessHandleStore>(VfsAppData::new(data))
815            .unwrap();
816    }
817}