generate table structs from ts in rust

This commit is contained in:
2025-10-02 14:31:47 +02:00
parent fb577a8699
commit fc841f238b
49 changed files with 1609 additions and 8610 deletions

View File

@ -0,0 +1,3 @@
// src-tauri/src/build/mod.rs
pub mod rust_types;
pub mod table_names;

View File

@ -0,0 +1,24 @@
// src-tauri/src/build/rust_types.rs
use std::fs;
use std::path::Path;
pub fn generate_rust_types() {
// Prüfe ob die generierte Datei vom TypeScript-Script existiert
let generated_path = Path::new("src/database/generated.rs");
if !generated_path.exists() {
eprintln!("⚠️ Warning: src/database/generated.rs not found!");
eprintln!(" Run 'pnpm generate:rust-types' first.");
// Erstelle eine leere Datei als Fallback
fs::write(
generated_path,
"// Run 'pnpm generate:rust-types' to generate this file\n",
)
.ok();
}
println!("cargo:rerun-if-changed=src/database/generated.rs");
println!("cargo:rerun-if-changed=src/database/schemas/crdt.ts");
println!("cargo:rerun-if-changed=src/database/schemas/haex.ts");
}

View File

@ -0,0 +1,64 @@
// build/table_names.rs
use serde::Deserialize;
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::Path;
#[derive(Debug, Deserialize)]
pub struct Schema {
pub haex: Haex,
}
#[derive(Debug, Deserialize)]
pub struct Haex {
pub settings: String,
pub extensions: String,
pub extension_permissions: String,
pub crdt: Crdt,
}
#[derive(Debug, Deserialize)]
pub struct Crdt {
pub logs: String,
pub snapshots: String,
pub configs: String,
}
pub fn generate_table_names(out_dir: &str) {
let schema_path = Path::new("database/tableNames.json");
let dest_path = Path::new(out_dir).join("tableNames.rs");
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
let reader = BufReader::new(file);
let schema: Schema =
serde_json::from_reader(reader).expect("Konnte tableNames.json nicht parsen");
let haex = schema.haex;
let code = format!(
r#"
// Auto-generated - DO NOT EDIT
// Core Tables
pub const TABLE_SETTINGS: &str = "{settings}";
pub const TABLE_EXTENSIONS: &str = "{extensions}";
pub const TABLE_EXTENSION_PERMISSIONS: &str = "{extension_permissions}";
// CRDT Tables
pub const TABLE_CRDT_LOGS: &str = "{crdt_logs}";
pub const TABLE_CRDT_SNAPSHOTS: &str = "{crdt_snapshots}";
pub const TABLE_CRDT_CONFIGS: &str = "{crdt_configs}";
"#,
settings = haex.settings,
extensions = haex.extensions,
extension_permissions = haex.extension_permissions,
crdt_logs = haex.crdt.logs,
crdt_snapshots = haex.crdt.snapshots,
crdt_configs = haex.crdt.configs
);
let mut f = File::create(&dest_path).expect("Konnte Zieldatei nicht erstellen");
f.write_all(code.as_bytes())
.expect("Konnte nicht in Zieldatei schreiben");
println!("cargo:rerun-if-changed=database/tableNames.json");
}

View File

@ -15,6 +15,7 @@ use sqlparser::parser::Parser;
use std::collections::HashMap;
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
///
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, DatabaseError> {
let flags = if create {
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE

View File

@ -0,0 +1,210 @@
// Auto-generated from Drizzle schema
// DO NOT EDIT MANUALLY
// Run 'pnpm generate:rust-types' to regenerate
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HaexSettings {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub r#type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_tombstone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_timestamp: Option<String>,
}
impl HaexSettings {
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
key: row.get(1)?,
r#type: row.get(2)?,
value: row.get(3)?,
haex_tombstone: row.get(4)?,
haex_timestamp: row.get(5)?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HaexExtensions {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_tombstone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_timestamp: Option<String>,
}
impl HaexExtensions {
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
author: row.get(1)?,
description: row.get(2)?,
entry: row.get(3)?,
homepage: row.get(4)?,
enabled: row.get(5)?,
icon: row.get(6)?,
name: row.get(7)?,
public_key: row.get(8)?,
signature: row.get(9)?,
url: row.get(10)?,
version: row.get(11)?,
haex_tombstone: row.get(12)?,
haex_timestamp: row.get(13)?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HaexExtensionPermissions {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub extension_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<String>,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_tombstone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_timestamp: Option<String>,
}
impl HaexExtensionPermissions {
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
extension_id: row.get(1)?,
resource_type: row.get(2)?,
action: row.get(3)?,
target: row.get(4)?,
constraints: row.get(5)?,
status: row.get(6)?,
created_at: row.get(7)?,
updated_at: row.get(8)?,
haex_tombstone: row.get(9)?,
haex_timestamp: row.get(10)?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HaexCrdtLogs {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub table_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub row_pks: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub op_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_value: Option<String>,
}
impl HaexCrdtLogs {
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self {
id: row.get(0)?,
haex_timestamp: row.get(1)?,
table_name: row.get(2)?,
row_pks: row.get(3)?,
op_type: row.get(4)?,
column_name: row.get(5)?,
new_value: row.get(6)?,
old_value: row.get(7)?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HaexCrdtSnapshots {
pub snapshot_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub epoch_hlc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_size_bytes: Option<i64>,
}
impl HaexCrdtSnapshots {
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self {
snapshot_id: row.get(0)?,
created: row.get(1)?,
epoch_hlc: row.get(2)?,
location_url: row.get(3)?,
file_size_bytes: row.get(4)?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HaexCrdtConfigs {
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
}
impl HaexCrdtConfigs {
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
Ok(Self {
key: row.get(0)?,
value: row.get(1)?,
})
}
}

View File

@ -1,973 +0,0 @@
/// src-tauri/src/extension/core.rs
use crate::extension::crypto::ExtensionCrypto;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::manager::PermissionManager;
use crate::extension::permissions::types::{
Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints,
PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints,
};
use crate::AppState;
use mime;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use sha2::Sha256;
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::fs::File;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, SystemTime};
use tauri::State;
use tauri::{
http::{Request, Response},
AppHandle, Manager, Runtime, UriSchemeContext,
};
use zip::ZipArchive;
#[derive(Serialize, Deserialize)]
pub struct ExtensionPreview {
pub manifest: ExtensionManifest,
pub is_valid_signature: bool,
pub key_hash: String,
pub editable_permissions: EditablePermissions,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermissions {
pub permissions: Vec<EditablePermission>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermission {
pub resource_type: String,
pub action: String,
pub target: String,
pub constraints: Option<serde_json::Value>,
pub status: String,
}
impl EditablePermissions {
/// Konvertiert EditablePermissions zu internen ExtensionPermissions
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
self.permissions
.iter()
.map(|p| ExtensionPermission {
id: uuid::Uuid::new_v4().to_string(),
extension_id: extension_id.to_string(),
resource_type: match p.resource_type.as_str() {
"fs" => ResourceType::Fs,
"http" => ResourceType::Http,
"db" => ResourceType::Db,
"shell" => ResourceType::Shell,
_ => ResourceType::Fs, // Fallback
},
action: match p.action.as_str() {
"read" => Action::Read,
"write" => Action::Write,
_ => Action::Read, // Fallback
},
target: p.target.clone(),
constraints: p
.constraints
.as_ref()
.and_then(|c| Self::parse_constraints(&p.resource_type, c)),
status: match p.status.as_str() {
"granted" => PermissionStatus::Granted,
"denied" => PermissionStatus::Denied,
"ask" => PermissionStatus::Ask,
_ => PermissionStatus::Denied, // Fallback
},
haex_timestamp: None,
haex_tombstone: None,
})
.collect()
}
fn parse_constraints(
resource_type: &str,
json_value: &serde_json::Value,
) -> Option<PermissionConstraints> {
match resource_type {
"db" => serde_json::from_value::<DbConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Database),
"fs" => serde_json::from_value::<FsConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Filesystem),
"http" => serde_json::from_value::<HttpConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Http),
"shell" => serde_json::from_value::<ShellConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Shell),
_ => None,
}
}
/// Filtert nur granted Permissions
pub fn filter_granted(&self) -> Vec<EditablePermission> {
self.permissions
.iter()
.filter(|p| p.status == "granted")
.cloned()
.collect()
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionManifest {
pub id: String,
pub name: String,
pub version: String,
pub author: Option<String>,
pub entry: String,
pub icon: Option<String>,
pub public_key: String,
pub signature: String,
pub permissions: ExtensionManifestPermissions,
pub homepage: Option<String>,
pub description: Option<String>,
}
impl ExtensionManifest {
/// Berechnet den Key Hash für diese Extension
pub fn calculate_key_hash(&self) -> Result<String, ExtensionError> {
ExtensionCrypto::calculate_key_hash(&self.public_key)
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })
}
/// Generiert die vollständige Extension ID mit Key Hash Prefix
pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
let key_hash = self.calculate_key_hash()?;
Ok(format!("{}-{}", key_hash, self.id))
}
pub fn to_editable_permissions(&self) -> EditablePermissions {
let mut database = Vec::new();
let mut filesystem = Vec::new();
let mut http = Vec::new();
if let Some(db) = &self.permissions.database {
for resource in &db.read {
database.push(EditableDatabasePermission {
operation: "read".to_string(),
resource: resource.clone(),
status: PermissionStatus::Granted,
});
}
for resource in &db.write {
database.push(EditableDatabasePermission {
operation: "write".to_string(),
resource: resource.clone(),
status: PermissionStatus::Granted,
});
}
}
if let Some(fs) = &self.permissions.filesystem {
for path in &fs.read {
filesystem.push(EditableFilesystemPermission {
operation: "read".to_string(),
path: path.clone(),
status: PermissionStatus::Granted,
});
}
for path in &fs.write {
filesystem.push(EditableFilesystemPermission {
operation: "write".to_string(),
path: path.clone(),
status: PermissionStatus::Granted,
});
}
}
if let Some(http_list) = &self.permissions.http {
for domain in http_list {
http.push(EditableHttpPermission {
domain: domain.clone(),
status: PermissionStatus::Granted,
});
}
}
EditablePermissions {
database,
filesystem,
http,
}
}
}
impl ExtensionManifest {
/// Konvertiert Manifest zu EditablePermissions (neue Version)
pub fn to_editable_permissions(&self) -> EditablePermissions {
let mut permissions = Vec::new();
// Database Permissions
if let Some(db) = &self.permissions.database {
for resource in &db.read {
permissions.push(EditablePermission {
resource_type: "db".to_string(),
action: "read".to_string(),
target: resource.clone(),
constraints: None,
status: "granted".to_string(),
});
}
for resource in &db.write {
permissions.push(EditablePermission {
resource_type: "db".to_string(),
action: "write".to_string(),
target: resource.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
// Filesystem Permissions
if let Some(fs) = &self.permissions.filesystem {
for path in &fs.read {
permissions.push(EditablePermission {
resource_type: "fs".to_string(),
action: "read".to_string(),
target: path.clone(),
constraints: None,
status: "granted".to_string(),
});
}
for path in &fs.write {
permissions.push(EditablePermission {
resource_type: "fs".to_string(),
action: "write".to_string(),
target: path.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
// HTTP Permissions
if let Some(http_list) = &self.permissions.http {
for domain in http_list {
permissions.push(EditablePermission {
resource_type: "http".to_string(),
action: "read".to_string(), // HTTP ist meist read
target: domain.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
// Shell Permissions
if let Some(shell_list) = &self.permissions.shell {
for command in shell_list {
permissions.push(EditablePermission {
resource_type: "shell".to_string(),
action: "read".to_string(), // Shell hat keine action mehr im Schema
target: command.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
EditablePermissions { permissions }
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionInfoResponse {
pub key_hash: String,
pub name: String,
pub full_id: String,
pub version: String,
pub display_name: Option<String>,
pub namespace: Option<String>,
pub allowed_origin: String,
}
impl ExtensionInfoResponse {
pub fn from_extension(extension: &Extension) -> Result<Self, ExtensionError> {
// Bestimme die allowed_origin basierend auf Tauri-Konfiguration
let allowed_origin = get_tauri_origin();
let key_hash = extension
.manifest
.calculate_key_hash()
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })?;
let full_id = extension
.manifest
.full_extension_id()
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })?;
Ok(Self {
key_hash,
name: extension.manifest.name.clone(),
full_id,
version: extension.manifest.version.clone(),
display_name: Some(extension.manifest.name.clone()),
namespace: extension.manifest.author.clone(),
allowed_origin,
})
}
}
fn get_tauri_origin() -> String {
#[cfg(target_os = "windows")]
{
"https://tauri.localhost".to_string()
}
#[cfg(target_os = "macos")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "linux")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "android")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "ios")]
{
"tauri://localhost".to_string()
}
}
/// Extension source type (production vs development)
#[derive(Debug, Clone)]
pub enum ExtensionSource {
Production {
path: PathBuf,
version: String,
},
Development {
dev_server_url: String,
manifest_path: PathBuf,
auto_reload: bool,
},
}
/// Complete extension data structure
#[derive(Debug, Clone)]
pub struct Extension {
pub id: String,
pub name: String,
pub source: ExtensionSource,
pub manifest: ExtensionManifest,
pub enabled: bool,
pub last_accessed: SystemTime,
}
/// Cached permission data for performance
#[derive(Debug, Clone)]
pub struct CachedPermission {
pub permissions: Vec<DbExtensionPermission>,
pub cached_at: SystemTime,
pub ttl: Duration,
}
/// Enhanced extension manager
#[derive(Default)]
pub struct ExtensionManager {
pub production_extensions: Mutex<HashMap<String, Extension>>,
pub dev_extensions: Mutex<HashMap<String, Extension>>,
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
}
impl ExtensionManager {
pub fn new() -> Self {
Self::default()
}
pub fn get_base_extension_dir(&self, app_handle: AppHandle) -> Result<PathBuf, ExtensionError> {
let path = app_handle
.path()
.app_local_data_dir() // Korrekt für Ressourcen
// Wenn du stattdessen App Local Data willst: .app_local_data_dir()
.map_err(|e| ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
})?
.join("extensions");
Ok(path)
}
pub fn get_extension_dir(
&self,
app_handle: AppHandle,
extension_id: &str,
extension_version: &str,
) -> Result<PathBuf, ExtensionError> {
let specific_extension_dir = self
.get_base_extension_dir(app_handle)?
.join(extension_id)
.join(extension_version);
Ok(specific_extension_dir)
}
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Extension ID cannot be empty".to_string(),
});
}
// Validate filesystem permissions
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
fs_perms.validate()?;
}
*/
match &extension.source {
ExtensionSource::Production { .. } => {
let mut extensions = self.production_extensions.lock().unwrap();
extensions.insert(extension.id.clone(), extension);
Ok(())
}
_ => Err(ExtensionError::ValidationError {
reason: "Expected Production source".to_string(),
}),
}
}
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Extension ID cannot be empty".to_string(),
});
}
// Validate filesystem permissions
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
fs_perms.validate()?;
} */
match &extension.source {
ExtensionSource::Development { .. } => {
let mut extensions = self.dev_extensions.lock().unwrap();
extensions.insert(extension.id.clone(), extension);
Ok(())
}
_ => Err(ExtensionError::ValidationError {
reason: "Expected Development source".to_string(),
}),
}
}
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
// Dev extensions take priority
let dev_extensions = self.dev_extensions.lock().unwrap();
if let Some(extension) = dev_extensions.get(extension_id) {
return Some(extension.clone());
}
// Then check production
let prod_extensions = self.production_extensions.lock().unwrap();
prod_extensions.get(extension_id).cloned()
}
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
{
let mut dev_extensions = self.dev_extensions.lock().unwrap();
if dev_extensions.remove(extension_id).is_some() {
return Ok(());
}
}
{
let mut prod_extensions = self.production_extensions.lock().unwrap();
if prod_extensions.remove(extension_id).is_some() {
return Ok(());
}
}
Err(ExtensionError::NotFound {
id: extension_id.to_string(),
})
}
pub async fn remove_extension_internal(
&self,
app_handle: AppHandle,
extension_id: String,
extension_version: String,
state: &State<'_, AppState>,
) -> Result<(), ExtensionError> {
// Permissions löschen (verwendet jetzt die neue Methode)
PermissionManager::delete_permissions(state, &extension_id).await?;
// Extension aus Manager entfernen
self.remove_extension(&extension_id)?;
let extension_dir =
self.get_extension_dir(app_handle, &extension_id, &extension_version)?;
// Dateien löschen
if extension_dir.exists() {
std::fs::remove_dir_all(&extension_dir)
.map_err(|e| ExtensionError::Filesystem { source: e })?;
}
Ok(())
}
pub async fn preview_extension_internal(
&self,
source_path: String,
) -> Result<ExtensionPreview, ExtensionError> {
let source = PathBuf::from(&source_path);
// ZIP in temp entpacken
let temp = std::env::temp_dir().join(format!("haexhub_preview_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive
.extract(&temp)
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot extract ZIP: {}", e),
})?;
// Manifest laden
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
// Signatur verifizieren
let content_hash = ExtensionCrypto::hash_directory(&temp)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
let is_valid_signature = ExtensionCrypto::verify_signature(
&manifest.public_key,
&content_hash,
&manifest.signature,
)
.is_ok();
let key_hash = manifest.calculate_key_hash()?;
// Editable permissions erstellen
let editable_permissions = manifest.to_editable_permissions();
// Cleanup
std::fs::remove_dir_all(&temp).ok();
Ok(ExtensionPreview {
manifest,
is_valid_signature,
key_hash,
editable_permissions,
})
}
pub async fn install_extension_with_permissions_internal(
&self,
app_handle: AppHandle,
source_path: String,
custom_permissions: EditablePermissions,
state: &State<'_, AppState>,
) -> Result<String, ExtensionError> {
let source = PathBuf::from(&source_path);
// 1. ZIP entpacken
let temp = std::env::temp_dir().join(format!("haexhub_ext_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive
.extract(&temp)
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot extract ZIP: {}", e),
})?;
// 2. Manifest laden
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
// 3. Signatur verifizieren
let content_hash = ExtensionCrypto::hash_directory(&temp)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
// 4. Key Hash berechnen
let key_hash = manifest.calculate_key_hash()?;
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
// 5. Zielverzeichnis
let extensions_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
})?
.join("extensions")
.join(&full_extension_id)
.join(&manifest.version);
std::fs::create_dir_all(&extensions_dir)
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// 6. Dateien kopieren
copy_directory(
temp.to_string_lossy().to_string(),
extensions_dir.to_string_lossy().to_string(),
)?;
// 7. Temp aufräumen
std::fs::remove_dir_all(&temp).ok();
// 8. Custom Permissions konvertieren und speichern
let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
let granted_permissions = permissions.filter_granted();
PermissionManager::save_permissions(&state.db, &granted_permissions).await?;
// 9. Extension registrieren
let extension = Extension {
id: full_extension_id.clone(),
name: manifest.name.clone(),
source: ExtensionSource::Production {
path: extensions_dir.clone(),
version: manifest.version.clone(),
},
manifest: manifest.clone(),
enabled: true,
last_accessed: SystemTime::now(),
};
state
.extension_manager
.add_production_extension(extension)?;
Ok(full_extension_id)
}
}
// For backward compatibility
#[derive(Default)]
pub struct ExtensionState {
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
}
impl ExtensionState {
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
let mut extensions = self.extensions.lock().unwrap();
extensions.insert(path, manifest);
}
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
let extensions = self.extensions.lock().unwrap();
extensions.values().find(|p| p.name == addon_id).cloned()
}
}
#[derive(Deserialize, Debug)]
struct ExtensionInfo {
id: String,
version: String,
}
#[derive(Debug)]
enum DataProcessingError {
HexDecoding(hex::FromHexError),
Utf8Conversion(std::string::FromUtf8Error),
JsonParsing(serde_json::Error),
}
// Implementierung von Display für benutzerfreundliche Fehlermeldungen
impl fmt::Display for DataProcessingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
DataProcessingError::Utf8Conversion(e) => {
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
}
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
}
}
}
// Implementierung von std::error::Error (optional, aber gute Praxis für bibliotheksähnlichen Code)
impl std::error::Error for DataProcessingError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
DataProcessingError::HexDecoding(e) => Some(e),
DataProcessingError::Utf8Conversion(e) => Some(e),
DataProcessingError::JsonParsing(e) => Some(e),
}
}
}
// Implementierung von From-Traits für einfache Verwendung des '?'-Operators
impl From<hex::FromHexError> for DataProcessingError {
fn from(err: hex::FromHexError) -> Self {
DataProcessingError::HexDecoding(err)
}
}
impl From<std::string::FromUtf8Error> for DataProcessingError {
fn from(err: std::string::FromUtf8Error) -> Self {
DataProcessingError::Utf8Conversion(err)
}
}
impl From<serde_json::Error> for DataProcessingError {
fn from(err: serde_json::Error) -> Self {
DataProcessingError::JsonParsing(err)
}
}
pub fn copy_directory(source: String, destination: String) -> Result<(), ExtensionError> {
println!(
"Kopiere Verzeichnis von '{}' nach '{}'",
source, destination
);
let source_path = PathBuf::from(&source);
let destination_path = PathBuf::from(&destination);
if !source_path.exists() || !source_path.is_dir() {
return Err(ExtensionError::Filesystem {
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Source directory '{}' not found", source),
),
});
}
// Optionen für fs_extra::dir::copy
let mut options = fs_extra::dir::CopyOptions::new();
options.overwrite = true; // Überschreibe Zieldateien, falls sie existieren
options.copy_inside = true; // Kopiere den *Inhalt* des Quellordners in den Zielordner
// options.content_only = true; // Alternative: nur Inhalt kopieren, Zielordner muss existieren
options.buffer_size = 64000; // Standard-Puffergröße, kann angepasst werden
// Führe die Kopieroperation aus
fs_extra::dir::copy(&source_path, &destination_path, &options).map_err(|e| {
ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
}
})?;
Ok(())
}
pub fn resolve_secure_extension_asset_path(
app_handle: AppHandle,
state: State<AppState>,
extension_id: &str,
extension_version: &str,
requested_asset_path: &str,
) -> Result<PathBuf, ExtensionError> {
// 1. Validiere die Extension ID
if extension_id.is_empty()
|| !extension_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return Err(ExtensionError::ValidationError {
reason: format!("Invalid extension ID: {}", extension_id),
});
}
if extension_version.is_empty()
|| !extension_version
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
{
return Err(ExtensionError::ValidationError {
reason: format!("Invalid extension version: {}", extension_version),
});
}
// 3. Verzeichnis für die spezifische Erweiterung
let specific_extension_dir =
state
.extension_manager
.get_extension_dir(app_handle, extension_id, extension_version)?;
// 4. Bereinige den angeforderten Asset-Pfad
let clean_relative_path = requested_asset_path
.replace('\\', "/")
.trim_start_matches('/')
.split('/')
.filter(|&part| !part.is_empty() && part != "." && part != "..")
.collect::<PathBuf>();
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
return Err(ExtensionError::ValidationError {
reason: "Empty or invalid asset path".to_string(),
});
}
// 5. Setze den finalen Pfad zusammen
let final_path = specific_extension_dir.join(clean_relative_path);
// 6. SICHERHEITSCHECK
match final_path.canonicalize() {
Ok(canonical_path) => {
let canonical_base = specific_extension_dir
.canonicalize()
.map_err(|e| ExtensionError::Filesystem { source: e })?;
if canonical_path.starts_with(&canonical_base) {
Ok(canonical_path)
} else {
eprintln!( /* ... Sicherheitswarnung ... */ );
Err(ExtensionError::SecurityViolation {
reason: format!("Path traversal attempt: {}", requested_asset_path),
})
}
}
Err(_) => {
// Fehler bei canonicalize (z.B. Pfad existiert nicht)
if final_path.starts_with(&specific_extension_dir) {
Ok(final_path) // Nicht-kanonisierten Pfad zurückgeben
} else {
eprintln!( /* ... Sicherheitswarnung ... */ );
Err(ExtensionError::SecurityViolation {
reason: format!("Invalid asset path: {}", requested_asset_path),
})
}
}
}
}
pub fn extension_protocol_handler<R: Runtime>(
state: State<AppState>,
app_handle: AppHandle,
request: &Request<Vec<u8>>,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
let uri_ref = request.uri();
println!("Protokoll Handler für: {}", uri_ref);
let host = uri_ref
.host()
.ok_or("Kein Host (Extension ID) in URI gefunden")?
.to_string();
let path_str = uri_ref.path();
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
let resource_segments: Vec<&str> = segments_iter.collect();
let raw_asset_path = resource_segments.join("/");
let asset_to_load = if raw_asset_path.is_empty() {
"index.html"
} else {
&raw_asset_path
};
match process_hex_encoded_json(&host) {
Ok(info) => {
println!("Daten erfolgreich verarbeitet:");
println!(" ID: {}", info.id);
println!(" Version: {}", info.version);
let absolute_secure_path = resolve_secure_extension_asset_path(
app_handle,
state,
&info.id,
&info.version,
&asset_to_load,
)?;
println!("absolute_secure_path: {}", absolute_secure_path.display());
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
match fs::read(&absolute_secure_path) {
Ok(content) => {
let mime_type = mime_guess::from_path(&absolute_secure_path)
.first_or(mime::APPLICATION_OCTET_STREAM)
.to_string();
let content_length = content.len();
println!(
"Liefere {} ({}, {} bytes) ", // Content-Length zum Log hinzugefügt
absolute_secure_path.display(),
mime_type,
content_length
);
Response::builder()
.status(200)
.header("Content-Type", mime_type)
.header("Content-Length", content_length.to_string()) // <-- HIER HINZUGEFÜGT
// Optional, aber gut für Streaming-Fähigkeit:
.header("Accept-Ranges", "bytes")
.body(content)
.map_err(|e| e.into())
}
Err(e) => {
eprintln!(
"Fehler beim Lesen der Datei {}: {}",
absolute_secure_path.display(),
e
);
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
404
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
403
} else {
500
};
Response::builder()
.status(status_code)
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
}
}
} else {
// Datei nicht gefunden oder es ist keine Datei
eprintln!(
"Asset nicht gefunden oder ist kein File: {}",
absolute_secure_path.display()
);
Response::builder()
.status(404) // HTTP 404 Not Found
.body(Vec::new())
.map_err(|e| e.into())
}
}
Err(e) => {
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
Response::builder()
.status(500)
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into())
}
}
}
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
// Schritt 1: Hex-String zu Bytes dekodieren
let bytes = hex::decode(hex_input)?; // Konvertiert hex::FromHexError automatisch
// Schritt 2: Bytes zu UTF-8-String konvertieren
let json_string = String::from_utf8(bytes)?; // Konvertiert FromUtf8Error automatisch
// Schritt 3: JSON-String zu Struktur parsen
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?; // Konvertiert serde_json::Error automatisch
Ok(extension_info)
}

View File

@ -1,7 +1,6 @@
// src-tauri/src/extension/database/mod.rs
pub mod executor;
use crate::crdt::hlc::HlcService;
use crate::crdt::transformer::CrdtTransformer;
use crate::crdt::trigger;
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
@ -22,15 +21,11 @@ use tauri::State;
/// Führt Statements mit korrekter Parameter-Bindung aus
pub struct StatementExecutor<'a> {
transaction: &'a Transaction<'a>,
hlc_service: &'a HlcService,
}
impl<'a> StatementExecutor<'a> {
fn new(transaction: &'a Transaction<'a>, hlc_service: &'a HlcService) -> Self {
Self {
transaction,
hlc_service,
}
fn new(transaction: &'a Transaction<'a>) -> Self {
Self { transaction }
}
/// Führt ein einzelnes Statement mit Parametern aus
@ -114,7 +109,6 @@ pub async fn extension_sql_execute(
params: Vec<JsonValue>,
extension_id: String,
state: State<'_, AppState>,
hlc_service: State<'_, HlcService>,
) -> Result<Vec<String>, ExtensionError> {
// Permission check
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
@ -130,15 +124,17 @@ pub async fn extension_sql_execute(
let tx = conn.transaction().map_err(DatabaseError::from)?;
let transformer = CrdtTransformer::new();
let executor = StatementExecutor::new(&tx, &hlc_service);
let executor = StatementExecutor::new(&tx);
// Generate HLC timestamp
let hlc_timestamp =
hlc_service
.new_timestamp_and_persist(&tx)
.map_err(|e| DatabaseError::HlcError {
reason: e.to_string(),
})?;
let hlc_timestamp = state
.hlc
.lock()
.unwrap()
.new_timestamp_and_persist(&tx)
.map_err(|e| DatabaseError::HlcError {
reason: e.to_string(),
})?;
// Transform statements
let mut modified_schema_tables = HashSet::new();
@ -302,13 +298,13 @@ fn count_sql_placeholders(sql: &str) -> usize {
}
/// Kürzt SQL für Fehlermeldungen
fn truncate_sql(sql: &str, max_length: usize) -> String {
/* fn truncate_sql(sql: &str, max_length: usize) -> String {
if sql.len() <= max_length {
sql.to_string()
} else {
format!("{}...", &sql[..max_length])
}
}
} */
#[cfg(test)]
mod tests {
@ -327,12 +323,12 @@ mod tests {
assert_eq!(count_sql_placeholders("SELECT * FROM users"), 0);
}
#[test]
/* #[test]
fn test_truncate_sql() {
let sql = "SELECT * FROM very_long_table_name";
assert_eq!(truncate_sql(sql, 10), "SELECT * F...");
assert_eq!(truncate_sql(sql, 50), sql);
}
} */
#[test]
fn test_validate_params() {

View File

@ -1,107 +0,0 @@
// src-tauri/src/models.rs
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use thiserror::Error;
/* #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionManifest {
pub id: String,
pub name: String,
pub version: String,
pub author: Option<String>,
pub entry: String,
pub icon: Option<String>,
pub permissions: ExtensionPermissions,
pub homepage: Option<String>,
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionPermissions {
pub database: Option<DatabasePermissions>,
pub http: Option<Vec<String>>,
pub filesystem: Option<String>,
}
/// Enum to represent the source of an extension
#[derive(Debug, Clone)]
pub enum ExtensionSource {
/// Production extension installed in app data
Production { path: PathBuf, version: String },
/// Development mode extension with live reloading
Development {
dev_server_url: String,
manifest_path: PathBuf,
auto_reload: bool,
},
} */
/*
#[derive(Default)]
pub struct ExtensionState {
pub extensions: Mutex<std::collections::HashMap<String, ExtensionManifest>>,
}
impl ExtensionState {
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
let mut extensions = self.extensions.lock().unwrap();
extensions.insert(path, manifest);
}
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
let extensions = self.extensions.lock().unwrap();
extensions.values().find(|p| p.name == addon_id).cloned()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DbExtensionPermission {
pub id: String,
pub extension_id: String,
pub resource: String,
pub operation: String,
pub path: String,
}
/// Comprehensive error type for all extension-related operations
#[derive(Error, Debug)]
pub enum ExtensionError {
/// Security violation detected
#[error("Security violation: {reason}")]
SecurityViolation { reason: String },
/// Extension not found
#[error("Extension not found: {id}")]
NotFound { id: String },
/// Permission denied
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
PermissionDenied {
extension_id: String,
operation: String,
resource: String,
},
/// IO error during extension operations
#[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error,
},
/// Error during extension manifest parsing
#[error("Manifest error: {reason}")]
ManifestError { reason: String },
/// Input validation error
#[error("Validation error: {reason}")]
ValidationError { reason: String },
/// Development server error
#[error("Dev server error: {reason}")]
DevServerError { reason: String },
}
*/

View File

@ -1,34 +0,0 @@
// models.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_path_pattern() {
// Valid patterns
assert!(validate_path_pattern("$PICTURE/**").is_ok());
assert!(validate_path_pattern("$DOCUMENT/myfiles/*").is_ok());
assert!(validate_path_pattern("$APPDATA/extensions/my-ext/**").is_ok());
// Invalid patterns
assert!(validate_path_pattern("").is_err());
assert!(validate_path_pattern("$INVALID/test").is_err());
assert!(validate_path_pattern("$PICTURE/../secret").is_err());
assert!(validate_path_pattern("relative/path").is_err());
}
#[test]
fn test_filesystem_permissions() {
let mut perms = FilesystemPermissions::new();
perms.add_read("$PICTURE/**");
perms.add_write("$APPDATA/my-ext/**");
assert!(perms.validate().is_ok());
assert_eq!(perms.read.as_ref().unwrap().len(), 1);
assert_eq!(perms.write.as_ref().unwrap().len(), 1);
}
}

View File

@ -1,441 +0,0 @@
// models.rs
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, Arc};
use std::time::{Duration, SystemTime};
use thiserror::Error;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionManifest {
pub id: String,
pub name: String,
pub version: String,
pub author: Option<String>,
pub entry: String,
pub icon: Option<String>,
pub permissions: ExtensionPermissions,
pub homepage: Option<String>,
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionPermissions {
pub database: Option<DatabasePermissions>,
pub http: Option<Vec<String>>,
pub filesystem: Option<FilesystemPermissions>,
pub shell: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DatabasePermissions {
pub read: Option<Vec<String>>,
pub write: Option<Vec<String>>,
pub create: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FilesystemPermissions {
/// Read access to files and directories
pub read: Option<Vec<FilesystemPath>>,
/// Write access to files and directories (includes create/delete)
pub write: Option<Vec<FilesystemPath>>,
}
/// Cross-platform filesystem path specification
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FilesystemPath {
/// The type of path (determines base directory)
pub path_type: FilesystemPathType,
/// Relative path from the base directory
pub relative_path: String,
/// Whether subdirectories are included (recursive)
pub recursive: bool,
}
/// Platform-agnostic path types that map to appropriate directories on each OS
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum FilesystemPathType {
/// App's data directory ($APPDATA on Windows, ~/.local/share on Linux, etc.)
AppData,
/// App's cache directory
AppCache,
/// App's configuration directory
AppConfig,
/// User's documents directory
Documents,
/// User's pictures directory
Pictures,
/// User's downloads directory
Downloads,
/// Temporary directory
Temp,
/// Extension's own private directory (always allowed)
ExtensionData,
/// Shared data between extensions (requires special permission)
SharedData,
}
impl FilesystemPath {
/// Creates a new filesystem path specification
pub fn new(path_type: FilesystemPathType, relative_path: &str, recursive: bool) -> Self {
Self {
path_type,
relative_path: relative_path.to_string(),
recursive,
}
}
/// Resolves the path to an actual system path
/// This would be implemented in your Tauri backend
pub fn resolve_system_path(&self, app_handle: &tauri::AppHandle) -> Result<std::path::PathBuf, ExtensionError> {
let base_dir = match self.path_type {
FilesystemPathType::AppData => app_handle.path().app_data_dir(),
FilesystemPathType::AppCache => app_handle.path().app_cache_dir(),
FilesystemPathType::AppConfig => app_handle.path().app_config_dir(),
FilesystemPathType::Documents => app_handle.path().document_dir(),
FilesystemPathType::Pictures => app_handle.path().picture_dir(),
FilesystemPathType::Downloads => app_handle.path().download_dir(),
FilesystemPathType::Temp => app_handle.path().temp_dir(),
FilesystemPathType::ExtensionData => {
app_handle.path().app_data_dir().map(|p| p.join("extensions"))
},
FilesystemPathType::SharedData => {
app_handle.path().app_data_dir().map(|p| p.join("shared"))
},
}.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to resolve base directory: {}", e),
})?;
let final_path = base_dir.join(&self.relative_path);
// Security check - ensure the resolved path is still within the base directory
if let (Ok(canonical_final), Ok(canonical_base)) = (final_path.canonicalize(), base_dir.canonicalize()) {
if !canonical_final.starts_with(&canonical_base) {
return Err(ExtensionError::SecurityViolation {
reason: format!("Path traversal detected: {} escapes base directory", self.relative_path),
});
}
}
Ok(final_path)
}
}
/// Enum to represent the source of an extension
#[derive(Debug, Clone)]
pub enum ExtensionSource {
/// Production extension installed in app data
Production {
path: PathBuf,
version: String,
},
/// Development mode extension with live reloading
Development {
dev_server_url: String,
manifest_path: PathBuf,
auto_reload: bool,
},
}
/// Complete extension data including source and runtime status
#[derive(Debug, Clone)]
pub struct Extension {
/// Unique extension ID
pub id: String,
/// Extension display name
pub name: String,
/// Source information (production or dev)
pub source: ExtensionSource,
/// Complete manifest data
pub manifest: ExtensionManifest,
/// Enabled status
pub enabled: bool,
/// Last access timestamp
pub last_accessed: SystemTime,
}
/// Cached permission data to avoid frequent database lookups
#[derive(Debug, Clone)]
pub struct CachedPermission {
/// The permissions that were fetched
pub permissions: Vec<DbExtensionPermission>,
/// When this cache entry was created
pub cached_at: SystemTime,
/// How long this cache entry is valid
pub ttl: Duration,
}
/// Enhanced extension manager with production/dev support and caching
#[derive(Default)]
pub struct ExtensionManager {
/// Production extensions loaded from app data directory
pub production_extensions: Mutex<HashMap<String, Extension>>,
/// Development mode extensions for live-reloading during development
pub dev_extensions: Mutex<HashMap<String, Extension>>,
/// Cache for extension permissions to improve performance
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
}
impl ExtensionManager {
/// Creates a new extension manager
pub fn new() -> Self {
Self {
production_extensions: Mutex::new(HashMap::new()),
dev_extensions: Mutex::new(HashMap::new()),
permission_cache: Mutex::new(HashMap::new()),
}
}
/// Adds a production extension to the manager
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Extension ID cannot be empty".to_string(),
});
}
match &extension.source {
ExtensionSource::Production { .. } => {
let mut extensions = self.production_extensions.lock().unwrap();
extensions.insert(extension.id.clone(), extension);
Ok(())
},
_ => Err(ExtensionError::ValidationError {
reason: "Expected Production source for production extension".to_string(),
})
}
}
/// Adds a development mode extension to the manager
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Extension ID cannot be empty".to_string(),
});
}
match &extension.source {
ExtensionSource::Development { .. } => {
let mut extensions = self.dev_extensions.lock().unwrap();
extensions.insert(extension.id.clone(), extension);
Ok(())
},
_ => Err(ExtensionError::ValidationError {
reason: "Expected Development source for dev extension".to_string(),
})
}
}
/// Gets an extension by its ID
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
// First check development extensions (they take priority)
let dev_extensions = self.dev_extensions.lock().unwrap();
if let Some(extension) = dev_extensions.get(extension_id) {
return Some(extension.clone());
}
// Then check production extensions
let prod_extensions = self.production_extensions.lock().unwrap();
prod_extensions.get(extension_id).cloned()
}
/// Removes an extension from the manager
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
// Check dev extensions first
{
let mut dev_extensions = self.dev_extensions.lock().unwrap();
if dev_extensions.remove(extension_id).is_some() {
return Ok(());
}
}
// Then check production extensions
{
let mut prod_extensions = self.production_extensions.lock().unwrap();
if prod_extensions.remove(extension_id).is_some() {
return Ok(());
}
}
Err(ExtensionError::NotFound {
id: extension_id.to_string(),
})
}
/// Gets cached permissions or indicates they need to be loaded
pub fn get_cached_permissions(
&self,
extension_id: &str,
resource: &str,
operation: &str,
) -> Option<Vec<DbExtensionPermission>> {
let cache = self.permission_cache.lock().unwrap();
let cache_key = format!("{}-{}-{}", extension_id, resource, operation);
if let Some(cached) = cache.get(&cache_key) {
let now = SystemTime::now();
if now.duration_since(cached.cached_at).unwrap_or(Duration::from_secs(0)) < cached.ttl {
return Some(cached.permissions.clone());
}
}
None
}
/// Updates the permission cache
pub fn update_permission_cache(
&self,
extension_id: &str,
resource: &str,
operation: &str,
permissions: Vec<DbExtensionPermission>,
) {
let mut cache = self.permission_cache.lock().unwrap();
let cache_key = format!("{}-{}-{}", extension_id, resource, operation);
cache.insert(cache_key, CachedPermission {
permissions,
cached_at: SystemTime::now(),
ttl: Duration::from_secs(60), // Cache for 60 seconds
});
}
/// Validates a manifest for security concerns
pub fn validate_manifest_security(&self, manifest: &ExtensionManifest) -> Result<(), ExtensionError> {
// Check for suspicious permission combinations
let has_filesystem = manifest.permissions.filesystem.is_some();
let has_database = manifest.permissions.database.is_some();
let has_shell = manifest.permissions.shell.is_some();
if has_filesystem && has_database && has_shell {
// This is a powerful combination, warn or check user confirmation elsewhere
}
// Validate ID format
if !manifest.id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
return Err(ExtensionError::ValidationError {
reason: "Invalid extension ID format. Must contain only alphanumeric characters, dash or underscore.".to_string()
});
}
Ok(())
}
/// Lists all enabled extensions (both dev and production)
pub fn list_enabled_extensions(&self) -> Vec<Extension> {
let mut extensions = Vec::new();
// Add enabled dev extensions first (higher priority)
{
let dev_extensions = self.dev_extensions.lock().unwrap();
extensions.extend(
dev_extensions
.values()
.filter(|ext| ext.enabled)
.cloned()
);
}
// Add enabled production extensions (avoiding duplicates)
{
let prod_extensions = self.production_extensions.lock().unwrap();
let dev_ids: std::collections::HashSet<String> = extensions.iter().map(|e| e.id.clone()).collect();
extensions.extend(
prod_extensions
.values()
.filter(|ext| ext.enabled && !dev_ids.contains(&ext.id))
.cloned()
);
}
extensions
}
}
// For backward compatibility - will be deprecated
#[derive(Default)]
pub struct ExtensionState {
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
}
impl ExtensionState {
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
let mut extensions = self.extensions.lock().unwrap();
extensions.insert(path, manifest);
}
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
let extensions = self.extensions.lock().unwrap();
extensions.values().find(|p| p.name == addon_id).cloned()
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DbExtensionPermission {
pub id: String,
pub extension_id: String,
pub resource: String,
pub operation: String,
pub path: String,
}
/// Comprehensive error type for all extension-related operations
#[derive(Error, Debug)]
pub enum ExtensionError {
/// Security violation detected
#[error("Security violation: {reason}")]
SecurityViolation {
reason: String
},
/// Extension not found
#[error("Extension not found: {id}")]
NotFound {
id: String
},
/// Permission denied
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
PermissionDenied {
extension_id: String,
operation: String,
resource: String
},
/// IO error during extension operations
#[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error
},
/// Error during extension manifest parsing
#[error("Manifest error: {reason}")]
ManifestError {
reason: String
},
/// Input validation error
#[error("Validation error: {reason}")]
ValidationError {
reason: String
},
/// Development server error
#[error("Dev server error: {reason}")]
DevServerError {
reason: String
},
}
// For Tauri Command Serialization
impl serde::Serialize for ExtensionError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}