haematite 0.7.0

Content-addressed, branchable, actor-native storage engine
Documentation
use js_sys::{Array, Reflect};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
    FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions,
    FileSystemGetFileOptions, FileSystemReadWriteOptions, FileSystemSyncAccessHandle,
};

use super::BrowserSourceError;
use super::js::haem_list_files;

pub(super) async fn get_directory(
    parent: &FileSystemDirectoryHandle,
    name: &str,
    create: bool,
) -> Result<Option<FileSystemDirectoryHandle>, BrowserSourceError> {
    let options = FileSystemGetDirectoryOptions::new();
    options.set_create(create);
    match JsFuture::from(parent.get_directory_handle_with_options(name, &options)).await {
        Ok(value) => value
            .dyn_into()
            .map(Some)
            .map_err(|_| BrowserSourceError::backend("directory cast", "invalid handle")),
        Err(error) if !create && js_name(&error) == "NotFoundError" => Ok(None),
        Err(error) => Err(js_source("get directory handle", &error)),
    }
}

pub(super) async fn get_file(
    parent: &FileSystemDirectoryHandle,
    name: &str,
    create: bool,
) -> Result<FileSystemFileHandle, BrowserSourceError> {
    let options = FileSystemGetFileOptions::new();
    options.set_create(create);
    JsFuture::from(parent.get_file_handle_with_options(name, &options))
        .await
        .map_err(|error| js_source("get file handle", &error))?
        .dyn_into()
        .map_err(|_| BrowserSourceError::backend("file cast", "invalid file handle"))
}

pub(super) async fn sync_handle(
    file: &FileSystemFileHandle,
) -> Result<FileSystemSyncAccessHandle, BrowserSourceError> {
    JsFuture::from(file.create_sync_access_handle())
        .await
        .map_err(|error| js_source("create sync access handle", &error))?
        .dyn_into()
        .map_err(|_| BrowserSourceError::backend("access cast", "invalid sync access handle"))
}

pub(super) async fn write_flushed(
    directory: &FileSystemDirectoryHandle,
    name: &str,
    bytes: &[u8],
) -> Result<(), BrowserSourceError> {
    let file = get_file(directory, name, true).await?;
    let access = sync_handle(&file).await?;
    write_access(&access, bytes)?;
    access
        .flush()
        .map_err(|error| js_source("flush sync access handle", &error))?;
    access.close();
    Ok(())
}

pub(super) fn write_access(
    access: &FileSystemSyncAccessHandle,
    bytes: &[u8],
) -> Result<(), BrowserSourceError> {
    access
        .truncate_with_u32(0)
        .map_err(|error| js_source("truncate before write", &error))?;
    let options = FileSystemReadWriteOptions::new();
    options.set_at(0.0);
    let written = access
        .write_with_u8_array_and_options(bytes, &options)
        .map_err(|error| js_source("write sync access handle", &error))?;
    let written = f64_to_usize(written, "write byte count")?;
    if written != bytes.len() {
        return Err(BrowserSourceError::backend(
            "write sync access handle",
            format!("short write: {written}/{}", bytes.len()),
        ));
    }
    let length = u32::try_from(bytes.len())
        .map_err(|_| BrowserSourceError::backend("truncate after write", "file too large"))?;
    access
        .truncate_with_u32(length)
        .map_err(|error| js_source("truncate after write", &error))
}

pub(super) async fn read_optional(
    directory: &FileSystemDirectoryHandle,
    name: &str,
) -> Result<Option<Vec<u8>>, BrowserSourceError> {
    let file = match get_file(directory, name, false).await {
        Ok(file) => file,
        Err(BrowserSourceError::JavaScript { name, .. }) if name == "NotFoundError" => {
            return Ok(None);
        }
        Err(error) => return Err(error),
    };
    let access = sync_handle(&file).await?;
    let bytes = read_access(&access)?;
    access.close();
    Ok(Some(bytes))
}

pub(super) async fn read_required(
    directory: &FileSystemDirectoryHandle,
    name: &str,
) -> Result<Vec<u8>, BrowserSourceError> {
    read_optional(directory, name).await?.ok_or_else(|| {
        BrowserSourceError::backend("read required file", format!("NotFoundError: {name}"))
    })
}

pub(super) fn read_access(
    access: &FileSystemSyncAccessHandle,
) -> Result<Vec<u8>, BrowserSourceError> {
    let size = access
        .get_size()
        .map_err(|error| js_source("get file size", &error))?;
    if !(size.is_finite() && size >= 0.0 && size.fract() == 0.0 && size <= u32::MAX.into()) {
        return Err(BrowserSourceError::backend("get file size", "invalid size"));
    }
    let size = f64_to_usize(size, "read byte count")?;
    let mut bytes = vec![0; size];
    let options = FileSystemReadWriteOptions::new();
    options.set_at(0.0);
    let read = access
        .read_with_u8_array_and_options(&mut bytes, &options)
        .map_err(|error| js_source("read sync access handle", &error))?;
    let read = f64_to_usize(read, "read byte count")?;
    if read != size {
        return Err(BrowserSourceError::backend(
            "read sync access handle",
            format!("short read: {read}/{size}"),
        ));
    }
    Ok(bytes)
}

fn f64_to_usize(value: f64, operation: &'static str) -> Result<usize, BrowserSourceError> {
    if !(value.is_finite() && value >= 0.0 && value.fract() == 0.0) {
        return Err(BrowserSourceError::backend(
            operation,
            format!("invalid byte count: {value}"),
        ));
    }
    format!("{value:.0}").parse().map_err(|_| {
        BrowserSourceError::backend(operation, format!("byte count exceeds usize: {value}"))
    })
}

pub(super) async fn list_files(
    directory: &FileSystemDirectoryHandle,
) -> Result<Vec<String>, BrowserSourceError> {
    let value = JsFuture::from(haem_list_files(directory))
        .await
        .map_err(|error| js_source("list directory files", &error))?;
    Array::from(&value)
        .iter()
        .map(|value| {
            value.as_string().ok_or_else(|| {
                BrowserSourceError::backend("list directory files", "non-string entry")
            })
        })
        .collect()
}

pub(super) fn js_source(operation: &'static str, value: &JsValue) -> BrowserSourceError {
    BrowserSourceError::JavaScript {
        operation,
        name: js_name(value),
        message: Reflect::get(value, &JsValue::from_str("message"))
            .ok()
            .and_then(|value| value.as_string())
            .or_else(|| value.as_string())
            .unwrap_or_else(|| format!("{value:?}")),
    }
}

fn js_name(value: &JsValue) -> String {
    Reflect::get(value, &JsValue::from_str("name"))
        .ok()
        .and_then(|value| value.as_string())
        .unwrap_or_default()
}