mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-19 05:47:43 -04:00
Update Rust core README (#1404)
This commit is contained in:
@@ -1,210 +1,371 @@
|
||||
# AliasVault Rust Core
|
||||
|
||||
Cross-platform core library for AliasVault, providing shared business logic for **native platforms**:
|
||||
- **Mobile Apps** (iOS via Swift bindings, Android via Kotlin bindings)
|
||||
- **Server** (.NET via P/Invoke)
|
||||
- **Desktop Apps** (future)
|
||||
Cross-platform core library providing shared business logic for all AliasVault clients:
|
||||
|
||||
> **Note:** Browser extensions use the TypeScript [VaultMergeService](../../apps/browser-extension/src/utils/VaultMergeService.ts) with sql.js (pre-compiled SQLite WASM). This is more efficient than re-compiling SQLite via Rust to WASM.
|
||||
- **Browser Extensions** (Chrome, Firefox, Edge, Safari via WASM)
|
||||
- **Mobile Apps** (iOS via Swift bindings, Android via Kotlin bindings)
|
||||
- **Server** (.NET via P/Invoke - currently only scaffolding, not actively used)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Rust Core Library │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐│
|
||||
│ │ src/lib.rs - Main library entry point ││
|
||||
│ │ src/merge.rs - Vault merge service (LWW strategy) ││
|
||||
│ │ src/types.rs - Common types and table configurations ││
|
||||
│ │ src/error.rs - Error types ││
|
||||
│ └─────────────────────────────────────────────────────────────────────┘│
|
||||
│ Rust Core Library │
|
||||
│ │
|
||||
│ src/ │
|
||||
│ ├── lib.rs Entry point, module exports │
|
||||
│ ├── error.rs VaultError type │
|
||||
│ ├── vault_merge/ LWW merge algorithm │
|
||||
│ ├── vault_pruner/ Trash retention cleanup │
|
||||
│ └── credential_matcher/ Autofill filtering │
|
||||
│ │
|
||||
│ Platform Interfaces: │
|
||||
│ ├── wasm.rs WASM bindings (#[wasm_bindgen]) │
|
||||
│ ├── uniffi_api.rs UniFFI bindings (#[uniffi::export]) │
|
||||
│ └── ffi.rs C FFI for .NET (extern "C") │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────┼──────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ UniFFI (FFI) │ │ UniFFI (FFI) │ │ C FFI (.NET) │
|
||||
│ Swift bindings │ │ Kotlin bindings │ │ P/Invoke │
|
||||
│ WASM Module │ │ UniFFI (FFI) │ │ C FFI (.NET) │
|
||||
│ wasm-bindgen │ │ Swift + Kotlin │ │ P/Invoke │
|
||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ iOS Mobile │ │ Android Mobile │ │ Server │
|
||||
│ App │ │ App │ │ (.NET) │
|
||||
│ Browser │ │ iOS & Android │ │ Server │
|
||||
│ Extensions │ │ Mobile Apps │ │ (.NET) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
## Core Modules
|
||||
|
||||
### Required
|
||||
- **Rust** (1.70+): Install via [rustup](https://rustup.rs/)
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
### vault_merge
|
||||
Last-Write-Wins (LWW) merge algorithm for syncing local and server vaults.
|
||||
|
||||
### For iOS builds (macOS only)
|
||||
- **Xcode** with command line tools
|
||||
### vault_pruner
|
||||
Permanently deletes items in trash older than retention period (default: 30 days).
|
||||
|
||||
### For Android builds
|
||||
- **Android NDK**: Set `ANDROID_NDK_HOME` environment variable
|
||||
- **cargo-ndk**: For Android cross-compilation (installed automatically)
|
||||
```bash
|
||||
cargo install cargo-ndk
|
||||
```
|
||||
### credential_matcher
|
||||
Priority-based credential filtering for autofill with anti-phishing protection.
|
||||
|
||||
## Building
|
||||
|
||||
### Build all targets
|
||||
```bash
|
||||
./build.sh
|
||||
./build.sh --browser # WASM for browser extension
|
||||
./build.sh --ios # iOS device + simulator with Swift bindings
|
||||
./build.sh --android # Android ABIs with Kotlin bindings
|
||||
./build.sh --dotnet # Native library for .NET
|
||||
./build.sh --mobile # iOS + Android
|
||||
./build.sh --all # All targets
|
||||
```
|
||||
|
||||
### Build specific targets
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
./build.sh --ios # iOS only (macOS required)
|
||||
./build.sh --android # Android only (NDK required)
|
||||
./build.sh --csharp # C#/.NET only
|
||||
cargo test # All tests
|
||||
cargo test vault_merge # Specific module
|
||||
cargo test --features uniffi # With UniFFI enabled
|
||||
```
|
||||
|
||||
### Manual builds (for development)
|
||||
---
|
||||
|
||||
# Developer Guide
|
||||
|
||||
This section documents the interface structure and how to add new functionality.
|
||||
|
||||
## Interface Overview
|
||||
|
||||
The Rust core exposes functions through three interface files, each targeting different platforms:
|
||||
|
||||
| Interface | File | Platforms | Attribute |
|
||||
|-----------|------|-----------|-----------|
|
||||
| WASM | `src/wasm.rs` | Browser extensions | `#[wasm_bindgen]` |
|
||||
| UniFFI | `src/uniffi_api.rs` | iOS, Android | `#[uniffi::export]` |
|
||||
| C FFI | `src/ffi.rs` | .NET Server | `#[no_mangle] extern "C"` |
|
||||
|
||||
All interfaces follow a JSON-in/JSON-out pattern for simplicity. Each platform handles its own database I/O and passes data as JSON to Rust.
|
||||
|
||||
## Current Exported Functions
|
||||
|
||||
| Function | WASM | UniFFI | C FFI | Description |
|
||||
|----------|------|--------|-------|-------------|
|
||||
| `getSyncableTableNames` | ✓ | ✓ | ✓ | Returns list of syncable table names |
|
||||
| `mergeVaults` / `mergeVaultsJson` | ✓ | ✓ (JSON only) | ✓ | LWW merge of local + server vaults |
|
||||
| `pruneVault` / `pruneVaultJson` | ✓ | ✓ (JSON only) | ✓ | Remove expired trash items |
|
||||
| `filterCredentials` / `filterCredentialsJson` | ✓ | ✓ (JSON only) | ✓ | Credential matching for autofill |
|
||||
| `extractDomain` | ✓ | ✓ | - | Extract domain from URL |
|
||||
| `extractRootDomain` | ✓ | ✓ | - | Extract root domain (handles .co.uk etc) |
|
||||
|
||||
## Adding a New Function
|
||||
|
||||
When adding a new module or function to the Rust core, you need to update multiple files. Follow this checklist:
|
||||
|
||||
### 1. Implement the Core Logic
|
||||
|
||||
Create or update a module in `src/`:
|
||||
|
||||
```rust
|
||||
// src/my_module/mod.rs
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::error::VaultResult;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MyInput {
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MyOutput {
|
||||
pub result: String,
|
||||
}
|
||||
|
||||
/// Core function with typed input/output.
|
||||
pub fn my_function(input: MyInput) -> VaultResult<MyOutput> {
|
||||
// Implementation
|
||||
Ok(MyOutput { result: input.data })
|
||||
}
|
||||
|
||||
/// JSON wrapper for cross-platform convenience.
|
||||
pub fn my_function_json(input_json: &str) -> VaultResult<String> {
|
||||
let input: MyInput = serde_json::from_str(input_json)?;
|
||||
let output = my_function(input)?;
|
||||
Ok(serde_json::to_string(&output)?)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Export from lib.rs
|
||||
|
||||
```rust
|
||||
// src/lib.rs
|
||||
|
||||
pub mod my_module;
|
||||
pub use my_module::{my_function, MyInput, MyOutput};
|
||||
```
|
||||
|
||||
### 3. Add WASM Bindings (src/wasm.rs)
|
||||
|
||||
```rust
|
||||
use crate::my_module::{my_function, MyInput, MyOutput};
|
||||
|
||||
/// Typed API - accepts/returns JsValue.
|
||||
#[wasm_bindgen(js_name = myFunction)]
|
||||
pub fn my_function_js(input: JsValue) -> Result<JsValue, JsValue> {
|
||||
let input: MyInput = serde_wasm_bindgen::from_value(input)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to parse input: {}", e)))?;
|
||||
|
||||
let output: MyOutput = my_function(input)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed: {}", e)))?;
|
||||
|
||||
serde_wasm_bindgen::to_value(&output)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to serialize: {}", e)))
|
||||
}
|
||||
|
||||
/// JSON API - accepts/returns strings.
|
||||
#[wasm_bindgen(js_name = myFunctionJson)]
|
||||
pub fn my_function_json_js(input_json: &str) -> Result<String, JsValue> {
|
||||
crate::my_module::my_function_json(input_json)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed: {}", e)))
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add UniFFI Bindings (src/uniffi_api.rs)
|
||||
|
||||
```rust
|
||||
use crate::error::VaultError;
|
||||
|
||||
/// JSON API only - UniFFI handles type conversion automatically.
|
||||
#[uniffi::export]
|
||||
pub fn my_function_json(input_json: String) -> Result<String, VaultError> {
|
||||
crate::my_module::my_function_json(&input_json)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add C FFI Bindings (src/ffi.rs) - If Needed for .NET
|
||||
|
||||
```rust
|
||||
/// C-compatible function for .NET P/Invoke.
|
||||
///
|
||||
/// # Safety
|
||||
/// - `input_json` must be a valid null-terminated C string
|
||||
/// - Returned pointer must be freed with `free_string()`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn my_function_ffi(input_json: *const c_char) -> *mut c_char {
|
||||
if input_json.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let c_str = match CStr::from_ptr(input_json).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return ptr::null_mut(),
|
||||
};
|
||||
|
||||
let input: MyInput = match serde_json::from_str(c_str) {
|
||||
Ok(i) => i,
|
||||
Err(e) => return create_error_response(&format!("Parse failed: {}", e)),
|
||||
};
|
||||
|
||||
let output = match my_function(input) {
|
||||
Ok(o) => o,
|
||||
Err(e) => return create_error_response(&format!("Failed: {}", e)),
|
||||
};
|
||||
|
||||
match serde_json::to_string(&output) {
|
||||
Ok(json) => string_to_c_char(json),
|
||||
Err(e) => create_error_response(&format!("Serialize failed: {}", e)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Update Cargo.toml Features (If Needed)
|
||||
|
||||
If your module requires additional dependencies, ensure they're properly feature-gated:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
wasm = ["dep:wasm-bindgen", "dep:serde-wasm-bindgen"]
|
||||
uniffi = ["dep:uniffi"]
|
||||
ffi = []
|
||||
|
||||
[dependencies]
|
||||
my-new-dep = { version = "1.0", optional = true }
|
||||
```
|
||||
|
||||
### 7. Add Tests
|
||||
|
||||
```rust
|
||||
// In src/my_module/mod.rs or src/my_module/tests.rs
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_my_function() {
|
||||
let input = MyInput { data: "test".to_string() };
|
||||
let output = my_function(input).unwrap();
|
||||
assert_eq!(output.result, "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_my_function_json() {
|
||||
let input = r#"{"data": "test"}"#;
|
||||
let output = my_function_json(input).unwrap();
|
||||
assert!(output.contains("test"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Build and Distribute
|
||||
|
||||
```bash
|
||||
# iOS (device)
|
||||
cargo build --release --target aarch64-apple-ios --features uniffi
|
||||
# Build for all targets you support
|
||||
./build.sh --browser --ios --android
|
||||
|
||||
# Android (arm64)
|
||||
cargo ndk --target aarch64-linux-android --platform 21 -- build --release --features uniffi
|
||||
|
||||
# Native (current platform)
|
||||
cargo build --release
|
||||
# Or just the ones you need during development
|
||||
./build.sh --browser
|
||||
```
|
||||
|
||||
## Output Structure
|
||||
## Platform Consumption
|
||||
|
||||
After building, artifacts are placed in `dist/` and distributed to consumer apps:
|
||||
After building, the outputs are distributed to:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── ios/ # iOS libraries
|
||||
│ ├── device/
|
||||
│ │ └── libaliasvault_core.a
|
||||
│ ├── simulator/
|
||||
│ │ └── libaliasvault_core.a (universal)
|
||||
│ └── aliasvault_core.swift
|
||||
├── android/ # Android libraries
|
||||
│ ├── arm64-v8a/
|
||||
│ │ └── libaliasvault_core.so
|
||||
│ ├── armeabi-v7a/
|
||||
│ │ └── libaliasvault_core.so
|
||||
│ ├── x86_64/
|
||||
│ │ └── libaliasvault_core.so
|
||||
│ ├── x86/
|
||||
│ │ └── libaliasvault_core.so
|
||||
│ └── aliasvault_core.kt
|
||||
└── csharp/ # C# libraries
|
||||
├── osx-arm64/
|
||||
│ └── libaliasvault_core.dylib
|
||||
├── osx-x64/
|
||||
│ └── libaliasvault_core.dylib
|
||||
├── linux-x64/
|
||||
│ └── libaliasvault_core.so
|
||||
└── VaultMergeService.cs
|
||||
| Platform | Output Location | Files |
|
||||
|----------|----------------|-------|
|
||||
| Browser Extension | `apps/browser-extension/src/utils/dist/core/rust/` | `aliasvault_core.js`, `.wasm`, `.d.ts` |
|
||||
| Blazor WASM | `apps/server/AliasVault.Client/wwwroot/wasm/` | `aliasvault_core.js`, `.wasm` |
|
||||
| iOS | `apps/mobile-app/ios/VaultStoreKit/RustCore/` | `.a`, `.h`, `.swift` |
|
||||
| Android | `apps/mobile-app/android/app/src/main/jniLibs/` | `.so` per ABI + `.kt` |
|
||||
|
||||
### Browser Extension Usage
|
||||
|
||||
```typescript
|
||||
import init, { myFunction, myFunctionJson } from '@/utils/dist/core/rust/aliasvault_core.js';
|
||||
|
||||
// Initialize once
|
||||
await init(wasmBytes);
|
||||
|
||||
// Typed API
|
||||
const result = myFunction({ data: "test" });
|
||||
|
||||
// JSON API
|
||||
const jsonResult = myFunctionJson('{"data": "test"}');
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Swift (iOS)
|
||||
### iOS Usage (Swift)
|
||||
|
||||
```swift
|
||||
import AliasVaultCore
|
||||
|
||||
let mergeService = VaultMergeService()
|
||||
|
||||
do {
|
||||
let result = try mergeService.merge(
|
||||
localVaultBase64: localVault,
|
||||
serverVaultBase64: serverVault
|
||||
)
|
||||
|
||||
print("Merged \(result.stats.tablesProcessed) tables")
|
||||
print("New vault: \(result.mergedVaultBase64.prefix(50))...")
|
||||
} catch {
|
||||
print("Merge failed: \(error)")
|
||||
}
|
||||
// Generated UniFFI bindings in aliasvault_core.swift
|
||||
let result = try myFunctionJson(inputJson: jsonString)
|
||||
```
|
||||
|
||||
### Kotlin (Android)
|
||||
### Android Usage (Kotlin)
|
||||
|
||||
```kotlin
|
||||
import net.aliasvault.core.VaultMergeService
|
||||
// Generated UniFFI bindings
|
||||
val result = uniffi.aliasvault_core.myFunctionJson(jsonString)
|
||||
```
|
||||
|
||||
val mergeService = VaultMergeService()
|
||||
## Design Decisions
|
||||
|
||||
try {
|
||||
val result = mergeService.merge(
|
||||
localVaultBase64 = localVault,
|
||||
serverVaultBase64 = serverVault
|
||||
)
|
||||
### JSON-First Communication
|
||||
|
||||
println("Merged ${result.stats.tablesProcessed} tables")
|
||||
println("Conflicts: ${result.stats.conflicts}")
|
||||
} catch (e: Exception) {
|
||||
println("Merge failed: ${e.message}")
|
||||
All interfaces use JSON strings for input/output. This simplifies:
|
||||
- Cross-language type marshalling
|
||||
- Debugging (human-readable)
|
||||
- Consistency across platforms
|
||||
|
||||
For WASM, we also provide typed APIs using `serde-wasm-bindgen` for better TypeScript integration.
|
||||
|
||||
### Feature Flags
|
||||
|
||||
| Feature | Purpose | Used By |
|
||||
|---------|---------|---------|
|
||||
| `uniffi` | UniFFI runtime support | iOS/Android builds |
|
||||
| `uniffi-cli` | UniFFI binding generator | Build script only |
|
||||
| `wasm` | WASM + JS interop | Browser extension |
|
||||
| `ffi` | C-compatible exports | .NET server |
|
||||
|
||||
Use `uniffi` (not `uniffi-cli`) for library builds to avoid pulling in heavy bindgen dependencies.
|
||||
|
||||
### Error Handling
|
||||
|
||||
Use `VaultError` from `src/error.rs` for all errors:
|
||||
|
||||
```rust
|
||||
use crate::error::{VaultError, VaultResult};
|
||||
|
||||
pub fn my_function() -> VaultResult<String> {
|
||||
// serde errors auto-convert via From impl
|
||||
let data: MyType = serde_json::from_str(json)?;
|
||||
|
||||
// Manual error creation
|
||||
if data.invalid {
|
||||
return Err(VaultError::General("Invalid data".to_string()));
|
||||
}
|
||||
|
||||
Ok("success".to_string())
|
||||
}
|
||||
```
|
||||
|
||||
### C# (.NET)
|
||||
## File Reference
|
||||
|
||||
```csharp
|
||||
using AliasVault.Shared.RustCore;
|
||||
|
||||
var result = VaultMergeService.Merge(localVaultBase64, serverVaultBase64);
|
||||
|
||||
Console.WriteLine($"Merged {result.Stats.TablesProcessed} tables");
|
||||
Console.WriteLine($"Conflicts: {result.Stats.Conflicts}");
|
||||
Console.WriteLine($"Records from server: {result.Stats.RecordsFromServer}");
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running tests
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Checking code
|
||||
```bash
|
||||
cargo clippy
|
||||
cargo fmt --check
|
||||
```
|
||||
|
||||
### Adding new functionality
|
||||
|
||||
1. Add Rust implementation in `src/`
|
||||
2. Update `src/aliasvault_core.udl` for UniFFI bindings
|
||||
3. Run `./build.sh` to generate all bindings
|
||||
|
||||
## Binary Size Considerations
|
||||
|
||||
The compiled native libraries include SQLite bundled statically. Approximate sizes:
|
||||
|
||||
| Target | Approximate Size |
|
||||
|--------|-----------------|
|
||||
| iOS (arm64 static) | ~20MB |
|
||||
| Android (arm64 shared) | ~2-3MB |
|
||||
| macOS (.dylib) | ~16KB (dynamic) |
|
||||
|
||||
> **Note:** Binaries are NOT committed to the repository. They are built:
|
||||
> 1. Locally via `./build.sh` when needed
|
||||
> 2. In CI pipelines for deployment
|
||||
> 3. In Docker builds for containerized deployments
|
||||
|
||||
## Why Rust for Native, TypeScript for Browser?
|
||||
|
||||
- **Browser Extensions**: Already use sql.js (SQLite compiled to WASM). Re-compiling SQLite via Rust to WASM adds complexity without benefit since sql.js already handles this well.
|
||||
- **Native Platforms**: Rust compiles to native code with excellent SQLite support. UniFFI generates idiomatic Swift/Kotlin bindings automatically.
|
||||
- **Consistency**: Both implementations follow the same LWW merge algorithm, ensuring identical behavior across all platforms.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the main AliasVault repository for details.
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/lib.rs` | Entry point, exports all modules |
|
||||
| `src/error.rs` | `VaultError` and `VaultResult` types |
|
||||
| `src/vault_merge/mod.rs` | LWW merge implementation |
|
||||
| `src/vault_merge/types.rs` | Table configurations, composite keys |
|
||||
| `src/vault_pruner/mod.rs` | Trash cleanup implementation |
|
||||
| `src/credential_matcher/mod.rs` | Autofill filtering algorithm |
|
||||
| `src/credential_matcher/domain.rs` | URL/domain extraction utilities |
|
||||
| `src/credential_matcher/stop_words.rs` | Text filtering for service name matching |
|
||||
| `src/wasm.rs` | WASM bindings (browser) |
|
||||
| `src/uniffi_api.rs` | UniFFI bindings (iOS/Android) |
|
||||
| `src/ffi.rs` | C FFI bindings (.NET) |
|
||||
| `uniffi-bindgen.rs` | UniFFI CLI binary entry point |
|
||||
| `build.sh` | Multi-platform build script |
|
||||
| `Cargo.toml` | Dependencies and feature flags |
|
||||
Reference in New Issue
Block a user