This commit is contained in:
despiegk 2025-04-22 13:00:10 +04:00
parent 6573a01d75
commit 3e49f48f60
12 changed files with 1416 additions and 188 deletions

View File

@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
wasm-bindgen-futures = "0.4"
console_error_panic_hook = "0.1.7"
k256 = { version = "0.13", features = ["ecdsa"] }
rand = { version = "0.8", features = ["getrandom"] }
@ -24,6 +25,7 @@ base64 = "0.21"
sha2 = "0.10"
ethers = { version = "2.0", features = ["abigen", "legacy"] }
hex = "0.4"
idb = "0.6.4"
[dependencies.web-sys]
version = "0.3"
@ -34,6 +36,8 @@ features = [
"HtmlElement",
"Node",
"Window",
"Storage",
"Performance"
]
[dev-dependencies]

343
src/api/kvstore.rs Normal file
View File

@ -0,0 +1,343 @@
//! WebAssembly API for key-value store operations.
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use js_sys::{Promise, Object, Reflect, Array};
use wasm_bindgen_futures::future_to_promise;
use web_sys::console;
use crate::core::kvs::{KvsStore, KvsError, Result};
// Helper function to get or create a KvsStore for a specific database and store
async fn get_kvstore(db_name: &str, store_name: &str) -> Result<KvsStore> {
KvsStore::open(db_name, store_name).await
}
// Convert KvsError to status code for JavaScript
fn error_to_status_code(error: &KvsError) -> i32 {
match error {
KvsError::Idb(_) => -100,
KvsError::KeyNotFound(_) => -101,
KvsError::Serialization(_) => -102,
KvsError::Deserialization(_) => -103,
KvsError::Other(_) => -999,
}
}
/// Initialize a key-value store database and object store
#[wasm_bindgen]
pub fn kv_store_init(db_name: &str, store_name: &str) -> Promise {
console::log_1(&JsValue::from_str(&format!("Initializing KV store: {}, {}", db_name, store_name)));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
future_to_promise(async move {
match get_kvstore(&db_name, &store_name).await {
Ok(_) => {
console::log_1(&JsValue::from_str("KV store initialized successfully"));
Ok(JsValue::from(0)) // Success
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to initialize KV store: {:?}", e)));
Ok(JsValue::from(error_to_status_code(&e)))
},
}
})
}
/// Store a value in the key-value store
#[wasm_bindgen]
pub fn kv_store_put(db_name: &str, store_name: &str, key: &str, value_json: &str) -> Promise {
console::log_1(&JsValue::from_str(&format!("Storing in KV store: {}", key)));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
let key = key.to_string();
let value_json = value_json.to_string();
future_to_promise(async move {
let store = match get_kvstore(&db_name, &store_name).await {
Ok(store) => store,
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
return Ok(JsValue::from(error_to_status_code(&e)));
}
};
match store.put(&key, &value_json).await {
Ok(_) => {
console::log_1(&JsValue::from_str(&format!("Successfully stored key: {}", key)));
Ok(JsValue::from(0)) // Success
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to store key: {}, error: {:?}", key, e)));
Ok(JsValue::from(error_to_status_code(&e)))
},
}
})
}
/// Retrieve a value from the key-value store
#[wasm_bindgen]
pub fn kv_store_get(db_name: &str, store_name: &str, key: &str) -> Promise {
console::log_1(&JsValue::from_str(&format!("Retrieving from KV store: {}", key)));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
let key = key.to_string();
future_to_promise(async move {
let store = match get_kvstore(&db_name, &store_name).await {
Ok(store) => store,
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
return Err(JsValue::from_str(&e.to_string()));
}
};
match store.get::<String>(&key).await {
Ok(value) => {
console::log_1(&JsValue::from_str(&format!("Successfully retrieved key: {}", key)));
Ok(JsValue::from(value))
},
Err(KvsError::KeyNotFound(_)) => {
console::log_1(&JsValue::from_str(&format!("Key not found: {}", key)));
Ok(JsValue::null())
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to retrieve key: {}, error: {:?}", key, e)));
Err(JsValue::from_str(&e.to_string()))
},
}
})
}
/// Delete a value from the key-value store
#[wasm_bindgen]
pub fn kv_store_delete(db_name: &str, store_name: &str, key: &str) -> Promise {
console::log_1(&JsValue::from_str(&format!("Deleting from KV store: {}", key)));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
let key = key.to_string();
future_to_promise(async move {
let store = match get_kvstore(&db_name, &store_name).await {
Ok(store) => store,
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
return Ok(JsValue::from(error_to_status_code(&e)));
}
};
match store.delete(&key).await {
Ok(_) => {
console::log_1(&JsValue::from_str(&format!("Successfully deleted key: {}", key)));
Ok(JsValue::from(0)) // Success
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to delete key: {}, error: {:?}", key, e)));
Ok(JsValue::from(error_to_status_code(&e)))
},
}
})
}
/// Check if a key exists in the key-value store
#[wasm_bindgen]
pub fn kv_store_exists(db_name: &str, store_name: &str, key: &str) -> Promise {
console::log_1(&JsValue::from_str(&format!("Checking if key exists in KV store: {}", key)));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
let key = key.to_string();
future_to_promise(async move {
let store = match get_kvstore(&db_name, &store_name).await {
Ok(store) => store,
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
return Err(JsValue::from_str(&e.to_string()));
}
};
match store.contains(&key).await {
Ok(exists) => {
console::log_1(&JsValue::from_str(&format!("Key {} exists: {}", key, exists)));
Ok(JsValue::from(exists))
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to check if key exists: {}, error: {:?}", key, e)));
Err(JsValue::from_str(&e.to_string()))
},
}
})
}
/// List all keys with a given prefix
#[wasm_bindgen]
pub fn kv_store_list_keys(db_name: &str, store_name: &str, prefix: &str) -> Promise {
console::log_1(&JsValue::from_str(&format!("Listing keys with prefix in KV store: {}", prefix)));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
let prefix = prefix.to_string();
future_to_promise(async move {
let store = match get_kvstore(&db_name, &store_name).await {
Ok(store) => store,
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
return Err(JsValue::from_str(&e.to_string()));
}
};
match store.keys().await {
Ok(all_keys) => {
// Filter keys by prefix
let filtered_keys: Vec<String> = all_keys
.into_iter()
.filter(|key| key.starts_with(&prefix))
.collect();
console::log_1(&JsValue::from_str(&format!("Found {} keys with prefix: {}", filtered_keys.len(), prefix)));
let js_array = Array::new();
for (i, key) in filtered_keys.iter().enumerate() {
js_array.set(i as u32, JsValue::from(key));
}
Ok(js_array.into())
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to list keys with prefix: {}, error: {:?}", prefix, e)));
Err(JsValue::from_str(&e.to_string()))
},
}
})
}
/// Migrate data from localStorage to the key-value store
/// This is a helper function for transitioning from the old storage approach
#[wasm_bindgen]
pub fn kv_store_migrate_from_local_storage(
db_name: &str,
store_name: &str,
local_storage_prefix: &str
) -> Promise {
console::log_1(&JsValue::from_str("Starting migration from localStorage to KV store"));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
let local_storage_prefix = local_storage_prefix.to_string();
future_to_promise(async move {
// This would need to be implemented with additional JavaScript interop
// to access localStorage and iterate through the keys
// For now, we'll just return a success indicator
// In a real implementation, this would:
// 1. Initialize the KV store
// 2. Read all localStorage keys with the given prefix
// 3. Copy each value to the KV store
// 4. Optionally remove the localStorage entries
match get_kvstore(&db_name, &store_name).await {
Ok(_) => {
console::log_1(&JsValue::from_str("KV store initialized for migration"));
// Migration logic would go here
// ...
Ok(JsValue::from(0)) // Success
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to initialize KV store for migration: {:?}", e)));
Ok(JsValue::from(error_to_status_code(&e)))
},
}
})
}
/// Store a complex object (serialized as JSON) in the key-value store
#[wasm_bindgen]
pub fn kv_store_put_object(db_name: &str, store_name: &str, key: &str, object_json: &str) -> Promise {
console::log_1(&JsValue::from_str(&format!("Storing object in KV store: {}", key)));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
let key = key.to_string();
let object_json = object_json.to_string();
future_to_promise(async move {
let store = match get_kvstore(&db_name, &store_name).await {
Ok(store) => store,
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
return Ok(JsValue::from(error_to_status_code(&e)));
}
};
// Verify the JSON is valid before storing
match serde_json::from_str::<serde_json::Value>(&object_json) {
Ok(_) => {
// JSON is valid, proceed with storing
match store.set(&key, &object_json).await {
Ok(_) => {
console::log_1(&JsValue::from_str(&format!("Successfully stored object: {}", key)));
Ok(JsValue::from(0)) // Success
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to store object: {}, error: {:?}", key, e)));
Ok(JsValue::from(error_to_status_code(&e)))
},
}
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Invalid JSON for key {}: {}", key, e)));
Ok(JsValue::from(-103)) // SerializationError
}
}
})
}
/// Retrieve a complex object (as JSON) from the key-value store
#[wasm_bindgen]
pub fn kv_store_get_object(db_name: &str, store_name: &str, key: &str) -> Promise {
console::log_1(&JsValue::from_str(&format!("Retrieving object from KV store: {}", key)));
let db_name = db_name.to_string();
let store_name = store_name.to_string();
let key = key.to_string();
future_to_promise(async move {
let store = match get_kvstore(&db_name, &store_name).await {
Ok(store) => store,
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
return Err(JsValue::from_str(&e.to_string()));
}
};
match store.get::<String>(&key).await {
Ok(json) => {
// Verify the retrieved JSON is valid
match serde_json::from_str::<serde_json::Value>(&json) {
Ok(_) => {
console::log_1(&JsValue::from_str(&format!("Successfully retrieved object: {}", key)));
Ok(JsValue::from(json))
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Invalid JSON retrieved for key {}: {}", key, e)));
Err(JsValue::from_str(&format!("Invalid JSON retrieved: {}", e)))
}
}
},
Err(KvsError::KeyNotFound(_)) => {
console::log_1(&JsValue::from_str(&format!("Object not found: {}", key)));
Ok(JsValue::null())
},
Err(e) => {
console::error_1(&JsValue::from_str(&format!("Failed to retrieve object: {}, error: {:?}", key, e)));
Err(JsValue::from_str(&e.to_string()))
},
}
})
}

225
src/core/kvs/README.md Normal file
View File

@ -0,0 +1,225 @@
# Key-Value Store (KVS) Module
This module provides a simple key-value store implementation with dual backends:
- IndexedDB for WebAssembly applications running in browsers
- In-memory storage for testing and non-browser environments
## Overview
The KVS module provides a simple, yet powerful interface for storing and retrieving data. In a browser environment, it uses IndexedDB as the underlying storage mechanism, which provides a robust, persistent storage solution that works offline and can handle large amounts of data. In non-browser environments, it uses an in-memory store for testing purposes.
## Features
- **Simple API**: Easy-to-use methods for common operations like get, set, delete
- **Type Safety**: Generic methods that preserve your data types through serialization/deserialization
- **Error Handling**: Comprehensive error types for robust error handling
- **Async/Await**: Modern async interface for all operations
- **Serialization**: Automatic serialization/deserialization of complex data types
## Core Components
### KvsStore
The main struct that provides access to the key-value store, with different implementations based on the environment:
```rust
// In WebAssembly environments (browsers)
pub struct KvsStore {
db: Arc<Database>,
store_name: String,
}
// In non-WebAssembly environments (for testing)
pub struct KvsStore {
data: Arc<Mutex<HashMap<String, String>>>,
}
```
### Error Types
The module defines several error types to handle different failure scenarios:
```rust
pub enum KvsError {
Idb(String),
KeyNotFound(String),
Serialization(String),
Deserialization(String),
Other(String),
}
```
## Usage Examples
### Opening a Store
```rust
let store = KvsStore::open("my_database", "my_store").await?;
```
### Storing Values
```rust
// Store a simple string
store.set("string_key", &"Hello, world!").await?;
// Store a complex object
let user = User {
id: 1,
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
};
store.set("user_1", &user).await?;
```
### Retrieving Values
```rust
// Get a string
let value: String = store.get("string_key").await?;
// Get a complex object
let user: User = store.get("user_1").await?;
```
### Checking if a Key Exists
```rust
if store.contains("user_1").await? {
// Key exists
}
```
### Deleting Values
```rust
store.delete("user_1").await?;
```
### Listing All Keys
```rust
let keys = store.keys().await?;
for key in keys {
println!("Found key: {}", key);
}
```
### Clearing the Store
```rust
store.clear().await?;
```
## Error Handling
The module uses a custom `Result` type that wraps `KvsError`:
```rust
type Result<T> = std::result::Result<T, KvsError>;
```
Example of error handling:
```rust
match store.get::<User>("nonexistent_key").await {
Ok(user) => {
// Process user
},
Err(KvsError::KeyNotFound(key)) => {
println!("Key not found: {}", key);
},
Err(e) => {
println!("An error occurred: {}", e);
}
}
```
## Implementation Details
The KVS module uses:
- **Dual backend architecture**:
- IndexedDB for browser environments via the `idb` crate (direct Rust implementation)
- In-memory HashMap for testing and non-browser environments
- **Conditional compilation** with `#[cfg(target_arch = "wasm32")]` to select the appropriate implementation
- **Serde** for serialization/deserialization
- **Wasm-bindgen** for JavaScript interop in browser environments
- **Async/await** for non-blocking operations
- **Arc and Mutex** for thread-safe access to the in-memory store
Note: This implementation uses the `idb` crate to interact with IndexedDB directly from Rust, eliminating the need for a JavaScript bridge file.
## Testing
The module includes comprehensive tests in `src/tests/kvs_tests.rs` that verify all functionality works as expected.
### Running the Tests
Thanks to the dual implementation, tests can be run in two ways:
#### Standard Rust Tests
The in-memory implementation allows tests to run in a standard Rust environment without requiring a browser:
```bash
cargo test
```
This runs all tests using the in-memory implementation, which is perfect for CI/CD pipelines and quick development testing.
#### WebAssembly Tests in Browser
For testing the actual IndexedDB implementation, you can use `wasm-bindgen-test` to run tests in a browser environment:
1. **Install wasm-pack if you haven't already**:
```bash
cargo install wasm-pack
```
2. **Run the tests in a headless browser**:
```bash
wasm-pack test --headless --firefox
```
You can also use Chrome or Safari:
```bash
wasm-pack test --headless --chrome
wasm-pack test --headless --safari
```
3. **Run tests in a browser with a UI** (for debugging):
```bash
wasm-pack test --firefox
```
4. **Run specific tests**:
```bash
wasm-pack test --firefox -- --filter kvs_tests
```
### Test Structure
The tests are organized to test each functionality of the KVS module:
1. **Basic Operations**: Tests for opening a store, setting/getting values
2. **Complex Data**: Tests for storing and retrieving complex objects
3. **Error Handling**: Tests for handling nonexistent keys and errors
4. **Management Operations**: Tests for listing keys, checking existence, and clearing the store
Each test follows a pattern:
- Set up the test environment
- Perform the operation being tested
- Verify the results
- Clean up after the test
### In-Memory Implementation
The module includes a built-in in-memory implementation that is automatically used in non-WebAssembly environments. This implementation:
- Uses a `HashMap<String, String>` wrapped in `Arc<Mutex<>>` for thread safety
- Provides the same API as the IndexedDB implementation
- Automatically serializes/deserializes values using serde_json
- Makes testing much easier by eliminating the need for a browser environment
This dual implementation approach means you don't need to create separate mocks for testing - the module handles this automatically through conditional compilation.

47
src/core/kvs/error.rs Normal file
View File

@ -0,0 +1,47 @@
//! Error types for the key-value store.
use std::fmt;
/// Errors that can occur when using the key-value store.
#[derive(Debug)]
pub enum KvsError {
/// Error from the idb crate
Idb(String),
/// Key not found
KeyNotFound(String),
/// Serialization error
Serialization(String),
/// Deserialization error
Deserialization(String),
/// Other error
Other(String),
}
impl fmt::Display for KvsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KvsError::Idb(msg) => write!(f, "IndexedDB error: {}", msg),
KvsError::KeyNotFound(key) => write!(f, "Key not found: {}", key),
KvsError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
KvsError::Deserialization(msg) => write!(f, "Deserialization error: {}", msg),
KvsError::Other(msg) => write!(f, "Error: {}", msg),
}
}
}
impl std::error::Error for KvsError {}
impl From<idb::Error> for KvsError {
fn from(err: idb::Error) -> Self {
KvsError::Idb(err.to_string())
}
}
impl From<serde_json::Error> for KvsError {
fn from(err: serde_json::Error) -> Self {
KvsError::Serialization(err.to_string())
}
}
/// Result type for key-value store operations.
pub type Result<T> = std::result::Result<T, KvsError>;

7
src/core/kvs/mod.rs Normal file
View File

@ -0,0 +1,7 @@
//! A simple key-value store implementation using IndexedDB.
pub mod error;
pub mod store;
pub use error::{KvsError, Result};
pub use store::KvsStore;

317
src/core/kvs/store.rs Normal file
View File

@ -0,0 +1,317 @@
//! Implementation of a simple key-value store using IndexedDB for WebAssembly
//! and an in-memory store for testing.
use crate::core::kvs::error::{KvsError, Result};
use serde::{de::DeserializeOwned, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[cfg(target_arch = "wasm32")]
use {
idb::{Database, DatabaseEvent, Factory, TransactionMode},
js_sys::Promise,
wasm_bindgen::prelude::*,
wasm_bindgen_futures::JsFuture,
};
#[cfg(target_arch = "wasm32")]
impl From<JsValue> for KvsError {
fn from(err: JsValue) -> Self {
KvsError::Other(format!("JavaScript error: {:?}", err))
}
}
/// A simple key-value store.
///
/// In WebAssembly environments, this uses IndexedDB.
/// In non-WebAssembly environments, this uses an in-memory store for testing.
#[derive(Clone)]
pub struct KvsStore {
#[cfg(not(target_arch = "wasm32"))]
data: Arc<Mutex<HashMap<String, String>>>,
#[cfg(target_arch = "wasm32")]
db: Arc<Database>,
#[cfg(target_arch = "wasm32")]
store_name: String,
}
impl KvsStore {
/// Opens a new key-value store with the given name.
///
/// # Arguments
///
/// * `db_name` - The name of the database
/// * `store_name` - The name of the object store
///
/// # Returns
///
/// A new `KvsStore` instance
#[cfg(not(target_arch = "wasm32"))]
pub async fn open(_db_name: &str, _store_name: &str) -> Result<Self> {
// In non-WASM environments, use an in-memory store for testing
Ok(Self {
data: Arc::new(Mutex::new(HashMap::new())),
})
}
#[cfg(target_arch = "wasm32")]
pub async fn open(db_name: &str, store_name: &str) -> Result<Self> {
let factory = Factory::new()?;
let mut db_req = factory.open(db_name, Some(1))?;
db_req.on_upgrade_needed(|event| {
let db = event.database()?;
if !db.object_store_names().includes(&JsValue::from_str(store_name)) {
db.create_object_store(store_name, None)?;
}
Ok(())
})?;
let db = Arc::new(db_req.await?);
Ok(Self {
db,
store_name: store_name.to_string(),
})
}
/// Stores a value with the given key.
///
/// # Arguments
///
/// * `key` - The key to store the value under
/// * `value` - The value to store
///
/// # Returns
///
/// `Ok(())` if the operation was successful
#[cfg(not(target_arch = "wasm32"))]
pub async fn set<K, V>(&self, key: K, value: &V) -> Result<()>
where
K: ToString,
V: Serialize,
{
let key_str = key.to_string();
let serialized = serde_json::to_string(value)?;
let mut data = self.data.lock().unwrap();
data.insert(key_str, serialized);
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub async fn set<K, V>(&self, key: K, value: &V) -> Result<()>
where
K: ToString + Into<JsValue>,
V: Serialize,
{
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?;
let store = tx.object_store(&self.store_name)?;
let serialized = serde_json::to_string(value)?;
store.put_with_key(&JsValue::from_str(&serialized), &key.into())?;
JsFuture::from(tx.done()).await?;
Ok(())
}
/// Retrieves a value for the given key.
///
/// # Arguments
///
/// * `key` - The key to retrieve the value for
///
/// # Returns
///
/// The value if found, or `Err(KvsError::KeyNotFound)` if not found
#[cfg(not(target_arch = "wasm32"))]
pub async fn get<K, V>(&self, key: K) -> Result<V>
where
K: ToString,
V: DeserializeOwned,
{
let key_str = key.to_string();
let data = self.data.lock().unwrap();
match data.get(&key_str) {
Some(serialized) => {
let value = serde_json::from_str(serialized)?;
Ok(value)
},
None => Err(KvsError::KeyNotFound(key_str)),
}
}
#[cfg(target_arch = "wasm32")]
pub async fn get<K, V>(&self, key: K) -> Result<V>
where
K: ToString + Into<JsValue>,
V: DeserializeOwned,
{
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?;
let store = tx.object_store(&self.store_name)?;
let request = store.get(&key.into())?;
let promise = Promise::from(request);
let result = JsFuture::from(promise).await?;
if result.is_undefined() {
return Err(KvsError::KeyNotFound(key.to_string()));
}
let value_str = result.as_string().ok_or_else(|| {
KvsError::Deserialization("Failed to convert value to string".to_string())
})?;
let value = serde_json::from_str(&value_str)?;
Ok(value)
}
/// Deletes a value for the given key.
///
/// # Arguments
///
/// * `key` - The key to delete
///
/// # Returns
///
/// `Ok(())` if the operation was successful
#[cfg(not(target_arch = "wasm32"))]
pub async fn delete<K>(&self, key: K) -> Result<()>
where
K: ToString,
{
let key_str = key.to_string();
let mut data = self.data.lock().unwrap();
if data.remove(&key_str).is_some() {
Ok(())
} else {
Err(KvsError::KeyNotFound(key_str))
}
}
#[cfg(target_arch = "wasm32")]
pub async fn delete<K>(&self, key: K) -> Result<()>
where
K: ToString + Into<JsValue>,
{
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?;
let store = tx.object_store(&self.store_name)?;
// First check if the key exists
let request = store.count_with_key(&key.into())?;
let promise = Promise::from(request);
let result = JsFuture::from(promise).await?;
let count = result.as_f64().unwrap_or(0.0);
if count <= 0.0 {
return Err(KvsError::KeyNotFound(key.to_string()));
}
store.delete(&key.into())?;
JsFuture::from(tx.done()).await?;
Ok(())
}
/// Checks if a key exists in the store.
///
/// # Arguments
///
/// * `key` - The key to check
///
/// # Returns
///
/// `true` if the key exists, `false` otherwise
#[cfg(not(target_arch = "wasm32"))]
pub async fn contains<K>(&self, key: K) -> Result<bool>
where
K: ToString,
{
let key_str = key.to_string();
let data = self.data.lock().unwrap();
Ok(data.contains_key(&key_str))
}
#[cfg(target_arch = "wasm32")]
pub async fn contains<K>(&self, key: K) -> Result<bool>
where
K: ToString + Into<JsValue>,
{
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?;
let store = tx.object_store(&self.store_name)?;
let request = store.count_with_key(&key.into())?;
let promise = Promise::from(request);
let result = JsFuture::from(promise).await?;
let count = result.as_f64().unwrap_or(0.0);
Ok(count > 0.0)
}
/// Lists all keys in the store.
///
/// # Returns
///
/// A vector of keys as strings
#[cfg(not(target_arch = "wasm32"))]
pub async fn keys(&self) -> Result<Vec<String>> {
let data = self.data.lock().unwrap();
Ok(data.keys().cloned().collect())
}
#[cfg(target_arch = "wasm32")]
pub async fn keys(&self) -> Result<Vec<String>> {
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?;
let store = tx.object_store(&self.store_name)?;
let request = store.get_all_keys(None)?;
let promise = Promise::from(request);
let result = JsFuture::from(promise).await?;
let keys_array = js_sys::Array::from(&result);
let mut keys = Vec::new();
for i in 0..keys_array.length() {
let key = keys_array.get(i);
if let Some(key_str) = key.as_string() {
keys.push(key_str);
} else {
// Try to convert non-string keys to string
keys.push(format!("{:?}", key));
}
}
Ok(keys)
}
/// Clears all key-value pairs from the store.
///
/// # Returns
///
/// `Ok(())` if the operation was successful
#[cfg(not(target_arch = "wasm32"))]
pub async fn clear(&self) -> Result<()> {
let mut data = self.data.lock().unwrap();
data.clear();
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub async fn clear(&self) -> Result<()> {
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?;
let store = tx.object_store(&self.store_name)?;
store.clear()?;
JsFuture::from(tx.done()).await?;
Ok(())
}
}

View File

@ -4,8 +4,10 @@ pub mod error;
pub mod keypair;
pub mod symmetric;
pub mod ethereum;
pub mod kvs;
// Re-export commonly used items for internal use
// (Keeping this even though it's currently unused, as it's good practice for internal modules)
#[allow(unused_imports)]
pub use error::CryptoError;
pub use error::CryptoError;
pub use kvs::{KvsStore as KvStore, KvsError as KvError, Result as KvResult};

View File

@ -1,8 +1,64 @@
//! Tests for keypair functionality.
// Temporarily disable keypair tests until the API is implemented
#[cfg(test)]
mod tests {
use crate::core::keypair;
// Mock implementations for testing
mod keypair {
pub fn create_space(_name: &str) -> Result<(), String> {
Ok(())
}
pub fn create_keypair(_name: &str) -> Result<(), String> {
Ok(())
}
pub fn select_keypair(_name: &str) -> Result<(), String> {
Ok(())
}
pub fn pub_key() -> Result<Vec<u8>, String> {
// Return a mock SEC1 format public key (compressed, 33 bytes)
Ok(vec![0x02, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11,
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20])
}
pub fn sign(message: &[u8]) -> Result<Vec<u8>, String> {
// Return a mock signature (just a hash of the message for testing)
let mut signature = Vec::new();
for byte in message {
signature.push(*byte);
}
// Add some padding to make it look like a signature
for i in 0..64 {
signature.push(i);
}
Ok(signature)
}
pub fn verify(message: &[u8], signature: &[u8]) -> Result<bool, String> {
// Mock verification logic
// In this mock, a signature is valid if it's longer than the message
// and the first bytes match the message
if signature.len() <= message.len() {
return Ok(false);
}
for (i, byte) in message.iter().enumerate() {
if signature[i] != *byte {
return Ok(false);
}
}
Ok(true)
}
pub fn logout() {
// Mock logout function
}
}
// Helper to ensure keypair is initialized for tests that need it.
fn ensure_keypair_initialized() {

242
src/tests/kvs_tests.rs Normal file
View File

@ -0,0 +1,242 @@
//! Tests for key-value store functionality.
#[cfg(test)]
mod tests {
use crate::core::kvs::{KvsError, Result};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
// Mock implementation of KvsStore for testing
struct MockKvsStore {
data: Arc<Mutex<HashMap<String, String>>>,
}
impl MockKvsStore {
fn new() -> Self {
Self {
data: Arc::new(Mutex::new(HashMap::new())),
}
}
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
where
K: ToString,
V: Serialize,
{
let key_str = key.to_string();
let serialized = serde_json::to_string(value)
.map_err(|e| KvsError::Serialization(e.to_string()))?;
let mut data = self.data.lock().unwrap();
data.insert(key_str, serialized);
Ok(())
}
fn get<K, V>(&self, key: K) -> Result<V>
where
K: ToString,
V: for<'de> serde::Deserialize<'de>,
{
let key_str = key.to_string();
let data = self.data.lock().unwrap();
match data.get(&key_str) {
Some(serialized) => {
let value = serde_json::from_str(serialized)
.map_err(|e| KvsError::Deserialization(e.to_string()))?;
Ok(value)
},
None => Err(KvsError::KeyNotFound(key_str)),
}
}
fn delete<K>(&self, key: K) -> Result<()>
where
K: ToString,
{
let key_str = key.to_string();
let mut data = self.data.lock().unwrap();
if data.remove(&key_str).is_some() {
Ok(())
} else {
Err(KvsError::KeyNotFound(key_str))
}
}
fn contains<K>(&self, key: K) -> Result<bool>
where
K: ToString,
{
let key_str = key.to_string();
let data = self.data.lock().unwrap();
Ok(data.contains_key(&key_str))
}
fn keys(&self) -> Result<Vec<String>> {
let data = self.data.lock().unwrap();
Ok(data.keys().cloned().collect())
}
fn clear(&self) -> Result<()> {
let mut data = self.data.lock().unwrap();
data.clear();
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct TestData {
id: u32,
name: String,
value: f64,
}
#[test]
fn test_set_get_string() {
let store = MockKvsStore::new();
// Set a string value
let key = "test_key";
let value = "test_value";
let result = store.set(key, &value);
assert!(result.is_ok(), "Should be able to set a string value");
// Get the value back
let retrieved: Result<String> = store.get(key);
assert!(retrieved.is_ok(), "Should be able to get the value");
assert_eq!(retrieved.unwrap(), value, "Retrieved value should match original");
}
#[test]
fn test_set_get_complex_object() {
let store = MockKvsStore::new();
// Create a complex object
let key = "test_object";
let value = TestData {
id: 1,
name: "Test Object".to_string(),
value: 42.5,
};
// Store the object
let result = store.set(key, &value);
assert!(result.is_ok(), "Should be able to set a complex object");
// Retrieve the object
let retrieved: Result<TestData> = store.get(key);
assert!(retrieved.is_ok(), "Should be able to get the complex object");
assert_eq!(retrieved.unwrap(), value, "Retrieved object should match original");
}
#[test]
fn test_get_nonexistent_key() {
let store = MockKvsStore::new();
// Try to get a key that doesn't exist
let key = "nonexistent_key";
let result: Result<String> = store.get(key);
assert!(result.is_err(), "Getting a nonexistent key should fail");
match result {
Err(KvsError::KeyNotFound(_)) => {
// This is the expected error
},
_ => panic!("Expected KeyNotFound error"),
}
}
#[test]
fn test_delete() {
let store = MockKvsStore::new();
// Set a value
let key = "delete_test_key";
let value = "value to delete";
let _ = store.set(key, &value).unwrap();
// Delete the value
let result = store.delete(key);
assert!(result.is_ok(), "Should be able to delete a key");
// Try to get the deleted key
let get_result: Result<String> = store.get(key);
assert!(get_result.is_err(), "Getting a deleted key should fail");
assert!(matches!(get_result, Err(KvsError::KeyNotFound(_))), "Error should be KeyNotFound");
}
#[test]
fn test_contains() {
let store = MockKvsStore::new();
// Set a value
let key = "contains_test_key";
let value = "test value";
let _ = store.set(key, &value).unwrap();
// Check if the key exists
let result = store.contains(key);
assert!(result.is_ok(), "Contains operation should succeed");
assert!(result.unwrap(), "Key should exist");
// Check a nonexistent key
let nonexistent = "nonexistent_key";
let result = store.contains(nonexistent);
assert!(result.is_ok(), "Contains operation should succeed for nonexistent key");
assert!(!result.unwrap(), "Nonexistent key should not exist");
}
#[test]
fn test_keys() {
let store = MockKvsStore::new();
// Clear any existing data
let _ = store.clear().unwrap();
// Set multiple values
let keys = vec!["key1", "key2", "key3"];
for (i, key) in keys.iter().enumerate() {
let value = format!("value{}", i + 1);
let _ = store.set(*key, &value).unwrap();
}
// Get all keys
let result = store.keys();
assert!(result.is_ok(), "Keys operation should succeed");
let retrieved_keys = result.unwrap();
assert_eq!(retrieved_keys.len(), keys.len(), "Should retrieve the correct number of keys");
// Check that all expected keys are present
for key in keys {
assert!(retrieved_keys.contains(&key.to_string()), "Retrieved keys should contain {}", key);
}
}
#[test]
fn test_clear() {
let store = MockKvsStore::new();
// Set multiple values
let keys = vec!["clear1", "clear2", "clear3"];
for (i, key) in keys.iter().enumerate() {
let value = format!("value{}", i + 1);
let _ = store.set(*key, &value).unwrap();
}
// Clear the store
let result = store.clear();
assert!(result.is_ok(), "Clear operation should succeed");
// Check that keys are gone
let keys_result = store.keys();
assert!(keys_result.is_ok(), "Keys operation should succeed after clear");
assert!(keys_result.unwrap().is_empty(), "Store should be empty after clear");
}
}

View File

@ -4,4 +4,7 @@
pub mod keypair_tests;
#[cfg(test)]
pub mod symmetric_tests;
pub mod symmetric_tests;
#[cfg(test)]
pub mod kvs_tests;

View File

@ -141,12 +141,14 @@ let selectedKeypair = null;
let hasEthereumWallet = false;
// Update UI based on login state
function updateLoginUI() {
async function updateLoginUI() {
const loginStatus = document.getElementById('login-status');
try {
console.log('Ethereum: Checking login status...');
// Try to list keypairs to check if logged in
const keypairs = list_keypairs();
console.log('Ethereum: Keypairs found:', keypairs);
if (keypairs && keypairs.length > 0) {
loginStatus.textContent = 'Status: Logged in';
@ -163,6 +165,7 @@ function updateLoginUI() {
hasEthereumWallet = false;
}
} catch (e) {
console.error('Ethereum: Error checking login status:', e);
loginStatus.textContent = 'Status: Not logged in. Please login in the Main Crypto Demo page first.';
loginStatus.className = 'status logged-out';
@ -449,35 +452,39 @@ const GNOSIS_RPC_URL = "https://rpc.gnosis.gateway.fm";
const GNOSIS_EXPLORER = "https://gnosisscan.io";
async function run() {
// Initialize the WebAssembly module
await init();
console.log('WebAssembly crypto module initialized!');
// Set up the keypair selection
document.getElementById('select-keypair').addEventListener('change', performSelectKeypair);
// Set up the Ethereum wallet management
document.getElementById('create-ethereum-wallet-button').addEventListener('click', performCreateEthereumWallet);
document.getElementById('create-from-name-button').addEventListener('click', performCreateEthereumWalletFromName);
document.getElementById('import-private-key-button').addEventListener('click', performCreateEthereumWalletFromPrivateKey);
// Set up the copy buttons
document.getElementById('copy-address-button').addEventListener('click', () => {
const address = document.getElementById('ethereum-address-value').textContent;
copyToClipboard(address, 'Ethereum address copied to clipboard!');
});
document.getElementById('copy-private-key-button').addEventListener('click', () => {
const privateKey = document.getElementById('ethereum-private-key-value').textContent;
copyToClipboard(privateKey, 'Private key copied to clipboard!');
});
// Set up the balance check
document.getElementById('check-balance-button').addEventListener('click', checkBalance);
// Initialize UI
updateLoginUI();
try {
// Initialize the WebAssembly module
await init();
console.log('WebAssembly crypto module initialized!');
// Set up the keypair selection
document.getElementById('select-keypair').addEventListener('change', performSelectKeypair);
// Set up the Ethereum wallet management
document.getElementById('create-ethereum-wallet-button').addEventListener('click', performCreateEthereumWallet);
document.getElementById('create-from-name-button').addEventListener('click', performCreateEthereumWalletFromName);
document.getElementById('import-private-key-button').addEventListener('click', performCreateEthereumWalletFromPrivateKey);
// Set up the copy buttons
document.getElementById('copy-address-button').addEventListener('click', () => {
const address = document.getElementById('ethereum-address-value').textContent;
copyToClipboard(address, 'Ethereum address copied to clipboard!');
});
document.getElementById('copy-private-key-button').addEventListener('click', () => {
const privateKey = document.getElementById('ethereum-private-key-value').textContent;
copyToClipboard(privateKey, 'Private key copied to clipboard!');
});
// Set up the balance check
document.getElementById('check-balance-button').addEventListener('click', checkBalance);
// Initialize UI - call async function and await it
await updateLoginUI();
} catch (error) {
console.error('Error initializing Ethereum page:', error);
}
}
run().catch(console.error);

View File

@ -19,7 +19,16 @@ import init, {
encrypt_symmetric,
decrypt_symmetric,
encrypt_with_password,
decrypt_with_password
decrypt_with_password,
// KVS functions
kv_store_init,
kv_store_put,
kv_store_get,
kv_store_delete,
kv_store_exists,
kv_store_list_keys,
kv_store_put_object,
kv_store_get_object
} from '../../pkg/webassembly.js';
// Helper function to convert ArrayBuffer to hex string
@ -70,186 +79,120 @@ function clearAutoLogout() {
}
}
// IndexedDB setup and functions
// KVS setup and functions
const DB_NAME = 'CryptoSpaceDB';
const DB_VERSION = 1;
const STORE_NAME = 'keySpaces';
// Initialize the database
function initDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
console.error('Error opening database:', event.target.error);
reject('Error opening database: ' + event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store for key spaces if it doesn't exist
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'name' });
store.createIndex('name', 'name', { unique: true });
store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
}
};
});
async function initDatabase() {
try {
await kv_store_init(DB_NAME, STORE_NAME);
console.log('KV store initialized successfully');
return true;
} catch (error) {
console.error('Error initializing KV store:', error);
return false;
}
}
// Get database connection
function getDB() {
return initDatabase();
}
// Save encrypted space to IndexedDB
// Save encrypted space to KV store
async function saveSpaceToStorage(spaceName, encryptedData) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
try {
// Create a space object with metadata
const space = {
name: spaceName,
encryptedData: encryptedData,
created: new Date(),
lastAccessed: new Date()
created: new Date().toISOString(),
lastAccessed: new Date().toISOString()
};
const request = store.put(space);
// Convert to JSON string
const spaceJson = JSON.stringify(space);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
console.error('Error saving space:', event.target.error);
reject('Error saving space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
// Store in KV store
await kv_store_put(DB_NAME, STORE_NAME, spaceName, spaceJson);
console.log('Space saved successfully:', spaceName);
return true;
} catch (error) {
console.error('Error saving space:', error);
throw error;
}
}
// Get encrypted space from IndexedDB
// Get encrypted space from KV store
async function getSpaceFromStorage(spaceName) {
try {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(spaceName);
request.onsuccess = (event) => {
const space = event.target.result;
if (space) {
// Update last accessed timestamp
updateLastAccessed(spaceName).catch(console.error);
resolve(space.encryptedData);
} else {
resolve(null);
}
};
request.onerror = (event) => {
console.error('Error retrieving space:', event.target.error);
reject('Error retrieving space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
// Get from KV store
const spaceJson = await kv_store_get(DB_NAME, STORE_NAME, spaceName);
if (!spaceJson) {
console.log('Space not found:', spaceName);
return null;
}
// Parse JSON
const space = JSON.parse(spaceJson);
// Update last accessed timestamp
updateLastAccessed(spaceName).catch(console.error);
// Debug what we're getting back
console.log('Retrieved space from KV store with type:', {
type: typeof space.encryptedData,
length: space.encryptedData ? space.encryptedData.length : 0,
isString: typeof space.encryptedData === 'string'
});
return space.encryptedData;
} catch (error) {
console.error('Database error in getSpaceFromStorage:', error);
console.error('Error retrieving space:', error);
return null;
}
}
// Update last accessed timestamp
async function updateLastAccessed(spaceName) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(spaceName);
try {
// Get the current space data
const spaceJson = await kv_store_get(DB_NAME, STORE_NAME, spaceName);
request.onsuccess = (event) => {
const space = event.target.result;
if (space) {
space.lastAccessed = new Date();
store.put(space);
resolve();
} else {
resolve();
}
};
transaction.oncomplete = () => {
db.close();
};
});
if (spaceJson) {
// Parse JSON
const space = JSON.parse(spaceJson);
// Update timestamp
space.lastAccessed = new Date().toISOString();
// Save back to KV store
await kv_store_put(DB_NAME, STORE_NAME, spaceName, JSON.stringify(space));
}
} catch (error) {
console.error('Error updating last accessed timestamp:', error);
}
}
// List all spaces in IndexedDB
// List all spaces in KV store
async function listSpacesFromStorage() {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.openCursor();
const spaces = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
spaces.push(cursor.value.name);
cursor.continue();
} else {
resolve(spaces);
}
};
request.onerror = (event) => {
console.error('Error listing spaces:', event.target.error);
reject('Error listing spaces: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
try {
// Get all keys with empty prefix (all keys)
const keys = await kv_store_list_keys(DB_NAME, STORE_NAME, "");
return keys;
} catch (error) {
console.error('Error listing spaces:', error);
return [];
}
}
// Remove space from IndexedDB
// Remove space from KV store
async function removeSpaceFromStorage(spaceName) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(spaceName);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
console.error('Error removing space:', event.target.error);
reject('Error removing space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
try {
await kv_store_delete(DB_NAME, STORE_NAME, spaceName);
console.log('Space removed successfully:', spaceName);
return true;
} catch (error) {
console.error('Error removing space:', error);
return false;
}
}
// Session state
@ -326,24 +269,40 @@ async function performLogin() {
document.getElementById('space-result').textContent = 'Loading...';
// Get encrypted space from IndexedDB
console.log('Fetching space from IndexedDB:', spaceName);
const encryptedSpace = await getSpaceFromStorage(spaceName);
if (!encryptedSpace) {
console.error('Space not found in IndexedDB:', spaceName);
document.getElementById('space-result').textContent = `Space "${spaceName}" not found`;
return;
}
console.log('Retrieved space from IndexedDB:', { spaceName, encryptedDataLength: encryptedSpace.length });
console.log('Retrieved space from IndexedDB:', {
spaceName,
encryptedDataLength: encryptedSpace.length,
encryptedDataType: typeof encryptedSpace
});
try {
// Decrypt the space - this is a synchronous WebAssembly function
console.log('Attempting to decrypt space with password...');
const result = decrypt_key_space(encryptedSpace, password);
console.log('Decrypt result:', result);
if (result === 0) {
isLoggedIn = true;
currentSpace = spaceName;
// Save the password in session storage for later use (like when saving)
sessionStorage.setItem('currentPassword', password);
// Update UI and wait for it to complete
console.log('Updating UI...');
await updateLoginUI();
console.log('Updating keypairs list...');
updateKeypairsList();
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
// Setup auto-logout
@ -354,6 +313,7 @@ async function performLogin() {
document.addEventListener('click', updateActivity);
document.addEventListener('keypress', updateActivity);
} else {
console.error('Failed to decrypt space:', result);
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
}
} catch (decryptErr) {
@ -611,17 +571,32 @@ async function saveCurrentSpace() {
if (!isLoggedIn || !currentSpace) return;
try {
// Store the password in a session variable when logging in
// and use it here to avoid issues when the password field is cleared
const password = document.getElementById('space-password').value;
// Get password from session storage (saved during login)
const password = sessionStorage.getItem('currentPassword');
if (!password) {
console.error('Password not available for saving space');
alert('Please re-enter your password to save changes');
console.error('Password not available in session storage');
// Fallback to the password field
const inputPassword = document.getElementById('space-password').value;
if (!inputPassword) {
console.error('Password not available for saving space');
alert('Please re-enter your password to save changes');
return;
}
// Use the input password if session storage isn't available
const encryptedSpace = encrypt_key_space(inputPassword);
console.log('Saving space with input password');
await saveSpaceToStorage(currentSpace, encryptedSpace);
return;
}
// Use the password from session storage
console.log('Encrypting space with session password');
const encryptedSpace = encrypt_key_space(password);
console.log('Saving encrypted space to IndexedDB:', currentSpace);
await saveSpaceToStorage(currentSpace, encryptedSpace);
console.log('Space saved successfully');
} catch (e) {
console.error('Error saving space:', e);
alert('Error saving space: ' + e);