wasefire-store 0.3.2

Key-value store for SLC flash storage
Documentation
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! File-backed persistent flash storage for virtual authenticator.
//!
//! [`FileStorage`] implements the flash [`Storage`] interface but doesn't interface with an
//! actual flash storage. Instead it uses a host-based file to persist the storage state.

use alloc::borrow::Cow;
use core::cell::RefCell;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;

use wasefire_error::Error;

use crate::{Storage, StorageIndex};

/// Simulates a flash storage using a host-based file.
///
/// This is usable for emulating authenticator hardware on VM hypervisor's host OS.
pub struct FileStorage {
    // Options of the storage.
    options: FileOptions,

    /// File for persisting contents of the storage.
    ///
    /// Reading data from File requires mutable reference, as seeking and reading data
    /// changes file's current position.
    ///
    /// All operations on backing file internally always first seek to needed position,
    /// so it's safe to borrow mutable reference to backing file for the time of operation.
    file: RefCell<File>,
}

/// Options for file-backed storage.
pub struct FileOptions {
    /// Size of a word in bytes.
    pub word_size: usize,

    /// Size of a page in bytes.
    pub page_size: usize,

    /// Number of pages in storage.
    pub num_pages: usize,
}

impl FileStorage {
    pub fn new(path: &Path, options: FileOptions) -> Result<FileStorage, Error> {
        let mut file_ref = RefCell::new(
            OpenOptions::new().read(true).write(true).create(true).truncate(false).open(path)?,
        );
        let file = file_ref.get_mut();
        let file_len = file.metadata()?.len();
        let store_len: u64 = (options.page_size * options.num_pages) as u64;

        if file_len == 0 {
            file.seek(SeekFrom::Start(0))?;
            let buf = vec![0xff; options.page_size];
            for _ in 0 .. options.num_pages {
                file.write_all(&buf)?;
            }
        } else if file_len != store_len {
            // FileStorage buffer should be of fixed size, opening previously saved file
            // from storage of different size is not supported
            panic!("Invalid file size {file_len}, should be {store_len}");
        }
        Ok(FileStorage { options, file: file_ref })
    }
}

impl Storage for FileStorage {
    fn word_size(&self) -> usize {
        self.options.word_size
    }

    fn page_size(&self) -> usize {
        self.options.page_size
    }

    fn num_pages(&self) -> usize {
        self.options.num_pages
    }

    fn max_word_writes(&self) -> usize {
        // We can write an unlimited amount of times in a file, but the store arithmetic
        // uses `Nat` so the value should fit in a `Nat`.
        u32::MAX as usize
    }

    fn max_page_erases(&self) -> usize {
        // We can "erase" an unlimited amount of times in a file, but the store format
        // encodes the number of erase cycles on 16 bits.
        u16::MAX as usize
    }

    fn read_slice(&self, index: StorageIndex, length: usize) -> Result<Cow<'_, [u8]>, Error> {
        let mut file = self.file.borrow_mut();
        file.seek(SeekFrom::Start(index.range(length, self)?.start as u64))?;
        let mut buf = vec![0u8; length];
        file.read_exact(&mut buf)?;
        Ok(Cow::Owned(buf))
    }

    fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> Result<(), Error> {
        let mut file = self.file.borrow_mut();
        file.seek(SeekFrom::Start(index.range(value.len(), self)?.start as u64))?;
        file.write_all(value)?;
        Ok(())
    }

    fn erase_page(&mut self, page: usize) -> Result<(), Error> {
        let mut file = self.file.borrow_mut();
        let index = StorageIndex { page, byte: 0 };
        file.seek(SeekFrom::Start(index.range(self.page_size(), self)?.start as u64))?;
        file.write_all(&vec![0xff; self.page_size()][..])?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use tempfile::TempDir;

    use super::*;

    const BLANK_WORD: &[u8] = &[0xff, 0xff, 0xff, 0xff];
    const DATA_WORD: &[u8] = &[0xee, 0xdd, 0xbb, 0x77];

    const FILE_NAME: &str = "storage.bin";

    const OPTIONS: FileOptions = FileOptions { word_size: 4, page_size: 0x1000, num_pages: 20 };

    fn make_tmp_dir() -> PathBuf {
        TempDir::new().unwrap().keep()
    }

    fn remove_tmp_dir(tmp_dir: &Path) {
        std::fs::remove_dir_all(tmp_dir).unwrap();
    }

    fn temp_storage(tmp_dir: &Path) -> FileStorage {
        FileStorage::new(&tmp_dir.join(FILE_NAME), OPTIONS).unwrap()
    }

    #[test]
    fn read_write_persist_ok() {
        let index = StorageIndex { page: 0, byte: 0 };
        let next_index = StorageIndex { page: 0, byte: 4 };

        let tmp_dir = make_tmp_dir();
        {
            let mut file_storage = temp_storage(&tmp_dir);
            assert_eq!(file_storage.read_slice(index, 4).unwrap(), BLANK_WORD);
            file_storage.write_slice(index, DATA_WORD).unwrap();
            assert_eq!(file_storage.read_slice(index, 4).unwrap(), DATA_WORD);
            assert_eq!(file_storage.read_slice(next_index, 4).unwrap(), BLANK_WORD);
        }
        // Reload and check the data from previously persisted storage
        {
            let file_storage = temp_storage(&tmp_dir);
            assert_eq!(file_storage.read_slice(index, 4).unwrap(), DATA_WORD);
            assert_eq!(file_storage.read_slice(next_index, 4).unwrap(), BLANK_WORD);
        }
        remove_tmp_dir(&tmp_dir);
    }
}