mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
generate table structs from ts in rust
This commit is contained in:
3
src-tauri/src/build/mod.rs
Normal file
3
src-tauri/src/build/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
// src-tauri/src/build/mod.rs
|
||||
pub mod rust_types;
|
||||
pub mod table_names;
|
||||
24
src-tauri/src/build/rust_types.rs
Normal file
24
src-tauri/src/build/rust_types.rs
Normal 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");
|
||||
}
|
||||
64
src-tauri/src/build/table_names.rs
Normal file
64
src-tauri/src/build/table_names.rs
Normal 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");
|
||||
}
|
||||
@ -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
|
||||
|
||||
210
src-tauri/src/database/generated.rs
Normal file
210
src-tauri/src/database/generated.rs
Normal 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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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 },
|
||||
}
|
||||
*/
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user