tetanes-core 0.14.0

A NES Emulator written in Rust
//! Web-specific filesystem operations.

use crate::fs::{Error, Result};
use std::{
    io::{self, Read, Write},
    mem,
    path::{Path, PathBuf},
};
use web_sys::js_sys;

#[derive(Debug)]
#[must_use]
pub struct StoreWriter {
    path: PathBuf,
    data: Vec<u8>,
}

pub struct StoreReader {
    cursor: io::Cursor<Vec<u8>>,
}

pub fn local_storage() -> Result<web_sys::Storage> {
    let window = web_sys::window().ok_or_else(|| Error::custom("failed to get js window"))?;
    window
        .local_storage()
        .map_err(|err| {
            tracing::error!("failed to get local storage: {err:?}");
            Error::custom("failed to get storage")
        })?
        .ok_or_else(|| Error::custom("no storage available"))
}

impl Write for StoreWriter {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.data.extend_from_slice(buf);
        Ok(buf.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        let local_storage = local_storage().map_err(io::Error::other)?;

        let key = self.path.to_string_lossy();
        let data = mem::take(&mut self.data);
        let value = match serde_json::to_string(&data) {
            Ok(value) => value,
            Err(err) => {
                self.data = data;
                tracing::error!("failed to serialize data: {err:?}");
                return Err(io::Error::other("failed to serialize data"));
            }
        };

        if let Err(err) = local_storage.set_item(&key, &value) {
            self.data = data;
            tracing::error!("failed to store data in local storage: {err:?}");
            return Err(io::Error::other("failed to write data"));
        }

        Ok(())
    }
}

impl Read for StoreReader {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.cursor.read(buf)
    }
}

pub fn writer_impl(path: impl AsRef<Path>) -> Result<impl Write> {
    let path = path.as_ref();
    Ok(StoreWriter {
        path: path.to_path_buf(),
        data: Vec::new(),
    })
}

pub fn reader_impl(path: impl AsRef<Path>) -> Result<impl Read> {
    let path = path.as_ref();
    let local_storage = local_storage()?;

    let key = path.to_string_lossy().into_owned();
    let data = local_storage
        .get_item(&key)
        .map_err(|_| Error::custom("failed to find data for {key}"))?
        .map(|value| {
            serde_json::from_str(&value).map_err(|err| {
                tracing::error!("failed to deserialize data: {err:?}");
                Error::custom("failed to deserialize data")
            })
        })
        .unwrap_or_else(|| Ok(Vec::new()))?;

    Ok(StoreReader {
        cursor: io::Cursor::new(data),
    })
}

pub fn clear_dir_impl(path: impl AsRef<Path>) -> Result<()> {
    let path = path.as_ref().to_string_lossy();
    let local_storage = local_storage()?;

    for key in js_sys::Object::keys(&local_storage)
        .iter()
        .filter_map(|key| key.as_string())
        .filter(|key| key.starts_with(&*path))
    {
        let _ = local_storage.remove_item(&key);
    }

    Ok(())
}

pub fn exists_impl(path: impl AsRef<Path>) -> bool {
    let path = path.as_ref();
    let Ok(local_storage) = local_storage() else {
        return false;
    };

    let key = path.to_string_lossy();
    matches!(local_storage.get_item(&key), Ok(Some(_)))
}