diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index ba1e593..1aa583f 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -155,13 +155,35 @@ impl ExtensionManager { fn extract_and_validate_extension( bytes: Vec, temp_prefix: &str, + app_handle: &AppHandle, ) -> Result { - let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4())); + // Use app_cache_dir for better Android compatibility + let cache_dir = app_handle + .path() + .app_cache_dir() + .map_err(|e| ExtensionError::InstallationFailed { + reason: format!("Cannot get app cache dir: {}", e), + })?; + let temp_id = uuid::Uuid::new_v4(); + let temp = cache_dir.join(format!("{}_{}", temp_prefix, temp_id)); + let zip_file_path = cache_dir.join(format!("{}_{}_{}.haextension", temp_prefix, temp_id, "temp")); + + // Write bytes to a temporary ZIP file first (important for Android file system) + fs::write(&zip_file_path, &bytes).map_err(|e| { + ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e) + })?; + + // Create extraction directory fs::create_dir_all(&temp) .map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?; - let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| { + // Open ZIP file from disk (more reliable on Android than from memory) + let zip_file = fs::File::open(&zip_file_path).map_err(|e| { + ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e) + })?; + + let mut archive = ZipArchive::new(zip_file).map_err(|e| { ExtensionError::InstallationFailed { reason: format!("Invalid ZIP: {}", e), } @@ -173,6 +195,9 @@ impl ExtensionManager { reason: format!("Cannot extract ZIP: {}", e), })?; + // Clean up temporary ZIP file + let _ = fs::remove_file(&zip_file_path); + // Read haextension_dir from config if it exists, otherwise use default let config_path = temp.join("haextension.config.json"); let haextension_dir = if config_path.exists() { @@ -491,9 +516,10 @@ impl ExtensionManager { pub async fn preview_extension_internal( &self, + app_handle: &AppHandle, file_bytes: Vec, ) -> Result { - let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?; + let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?; let is_valid_signature = ExtensionCrypto::verify_signature( &extracted.manifest.public_key, @@ -518,7 +544,7 @@ impl ExtensionManager { custom_permissions: EditablePermissions, state: &State<'_, AppState>, ) -> Result { - let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?; + let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?; // Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft) ExtensionCrypto::verify_signature( diff --git a/src-tauri/src/extension/crypto.rs b/src-tauri/src/extension/crypto.rs index a49bc74..a2197c2 100644 --- a/src-tauri/src/extension/crypto.rs +++ b/src-tauri/src/extension/crypto.rs @@ -48,7 +48,9 @@ impl ExtensionCrypto { let relative = path.strip_prefix(dir) .unwrap_or(&path) .to_string_lossy() - .to_string(); + .to_string() + // Normalisiere Pfad-Separatoren zu Unix-Style (/) für plattformübergreifende Konsistenz + .replace('\\', "/"); (relative, path) }) .collect(); @@ -56,16 +58,30 @@ impl ExtensionCrypto { // 3. Sortiere nach relativen Pfaden relative_files.sort_by(|a, b| a.0.cmp(&b.0)); - println!("=== Files to hash ({}): ===", relative_files.len()); - for (rel, _) in &relative_files { - println!(" - {}", rel); - } - let mut hasher = Sha256::new(); + // Canonicalize manifest path for comparison (important on Android where symlinks may differ) + // Also ensure the canonical path is still within the allowed directory (security check) + let canonical_manifest_path = manifest_path.canonicalize() + .unwrap_or_else(|_| manifest_path.to_path_buf()); + + // Security: Verify canonical manifest path is still within dir + let canonical_dir = dir.canonicalize() + .unwrap_or_else(|_| dir.to_path_buf()); + + if !canonical_manifest_path.starts_with(&canonical_dir) { + return Err(ExtensionError::ManifestError { + reason: format!("Manifest path resolves outside of extension directory (potential path traversal)"), + }); + } + // 4. Inhalte der sortierten Dateien hashen for (_relative, file_path) in relative_files { - if file_path == manifest_path { + // Canonicalize file_path for comparison + let canonical_file_path = file_path.canonicalize() + .unwrap_or_else(|_| file_path.clone()); + + if canonical_file_path == canonical_manifest_path { // FÜR DIE MANIFEST.JSON: let content_str = fs::read_to_string(&file_path) .map_err(|e| ExtensionError::Filesystem { source: e })?; @@ -94,8 +110,12 @@ impl ExtensionCrypto { reason: format!("Failed to serialize manifest: {}", e), } })?; - println!("canonical_manifest_content: {}", canonical_manifest_content); - hasher.update(canonical_manifest_content.as_bytes()); + + // Normalisiere Zeilenenden zu Unix-Style (\n), wie Node.js JSON.stringify es macht + // Dies ist wichtig für plattformübergreifende Konsistenz (Desktop vs Android) + let normalized_content = canonical_manifest_content.replace("\r\n", "\n"); + + hasher.update(normalized_content.as_bytes()); } else { // FÜR ALLE ANDEREN DATEIEN: let content = diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index f5536e3..2f607f6 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -82,12 +82,13 @@ pub async fn get_all_extensions( #[tauri::command] pub async fn preview_extension( + app_handle: AppHandle, state: State<'_, AppState>, file_bytes: Vec, ) -> Result { state .extension_manager - .preview_extension_internal(file_bytes) + .preview_extension_internal(&app_handle, file_bytes) .await } diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 40014ac..b9d2dfd 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -14,18 +14,44 @@ @apply cursor-not-allowed; } + /* Define safe-area-insets as CSS custom properties for JavaScript access */ + :root { + --safe-area-inset-top: env(safe-area-inset-top, 0px); + --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); + --safe-area-inset-left: env(safe-area-inset-left, 0px); + --safe-area-inset-right: env(safe-area-inset-right, 0px); + } + + /* Verhindere Scrolling auf html und body */ + html { + overflow: hidden; + margin: 0; + padding: 0; + height: 100dvh; + height: 100vh; /* Fallback */ + width: 100%; + } + + body { + overflow: hidden; + margin: 0; + height: 100%; + width: 100%; + padding: 0; + } + #__nuxt { - /* Stellt sicher, dass die App immer die volle Höhe hat */ - min-height: 100vh; + /* Volle Höhe des body */ + height: 100%; + width: 100%; - /* Sorgt dafür, dass Padding die Höhe nicht sprengt */ - @apply box-border; + /* Safe-Area Paddings auf root element - damit ALLES davon profitiert */ + padding-top: var(--safe-area-inset-top); + padding-bottom: var(--safe-area-inset-bottom); + padding-left: var(--safe-area-inset-left); + padding-right: var(--safe-area-inset-right); - /* Die Safe-Area Paddings */ - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); + box-sizing: border-box; } } diff --git a/src/components/haex/debug/overlay.vue b/src/components/haex/debug/overlay.vue new file mode 100644 index 0000000..74ac1e9 --- /dev/null +++ b/src/components/haex/debug/overlay.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/components/haex/desktop/index.vue b/src/components/haex/desktop/index.vue index d75ff99..e63d624 100644 --- a/src/components/haex/desktop/index.vue +++ b/src/components/haex/desktop/index.vue @@ -1,7 +1,7 @@