-
Notifications
You must be signed in to change notification settings - Fork 722
refactor(backup): replace system zip/unzip with pure Rust implementation #773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
Replace system-dependent zip/unzip commands with the `zip` crate for
cross-platform compatibility and better error handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the backup/restore system by replacing platform-dependent system commands (zip/unzip) with a pure Rust implementation using the zip crate. This improves cross-platform compatibility, error handling, and maintainability while fixing two bugs in the restore functionality.
Key Changes:
- Replaced system
zip/unzipcommands with thezipcrate for compression and extraction operations - Added helper functions for file filtering and introduced a
TempDirGuardfor reliable cleanup - Implemented comprehensive unit tests covering roundtrip compression, system compatibility, and edge cases
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| src-tauri/src/backup.rs | Complete rewrite of backup/restore logic using pure Rust zip operations with improved error handling, file filtering utilities, automatic cleanup via RAII guard, and extensive test coverage |
| src-tauri/Cargo.toml | Added zip, walkdir dependencies and tempfile dev-dependency to support the new Rust-based implementation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Some(path) => target_dir.join(path), | ||
| None => continue, | ||
| }; | ||
|
|
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The enclosed_name() method already provides protection against path traversal attacks by checking if the path is safe. However, there's no explicit validation that outpath stays within target_dir. While enclosed_name() should handle this, it would be more defensive to add an explicit check:
let outpath = match file.enclosed_name() {
Some(path) => target_dir.join(path),
None => continue,
};
// Verify the output path is still within target_dir
if !outpath.starts_with(target_dir) {
return Err("Archive contains files outside target directory".to_string());
}This provides defense in depth against potential security issues.
| // Defense in depth: ensure outpath is within target_dir | |
| if !outpath.starts_with(target_dir) { | |
| return Err("Archive contains files outside target directory".to_string()); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import_app_data function will explicit specify target dir , it will make sure outpath stays within target_dir
src-tauri/Cargo.toml
Outdated
| tauri-plugin-opener = "2.4.0" | ||
| uuid = { version = "1.10", features = ["v4"] } | ||
| dirs = "5.0" | ||
| zip = "6" |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version specification for the zip crate is too loose. Using "6" will match any 6.x version, which could introduce breaking changes. Consider pinning to a more specific version:
zip = "6.2"Or for more stability:
zip = "=6.2.0"This ensures reproducible builds and prevents unexpected breaking changes.
| zip = "6" | |
| zip = "6.2" |
| fn should_skip_file(path: &Path) -> bool { | ||
| if let Some(file_name) = path.file_name() { | ||
| if let Some(file_str) = file_name.to_str() { | ||
| // Skip temp_import directory and SQLite temporary files | ||
| return file_str == TEMP_IMPORT_DIR | ||
| || path.to_string_lossy().ends_with(SQLITE_SHM_SUFFIX) | ||
| || path.to_string_lossy().ends_with(SQLITE_WAL_SUFFIX); | ||
| } | ||
| } | ||
| false | ||
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The check for TEMP_IMPORT_DIR only checks the file name, but should check if the path starts with or contains the temp_import directory. This could fail to skip nested files within temp_import.
Consider using:
return file_str == TEMP_IMPORT_DIR
|| path.to_string_lossy().starts_with(TEMP_IMPORT_DIR)
|| path.to_string_lossy().ends_with(SQLITE_SHM_SUFFIX)
|| path.to_string_lossy().ends_with(SQLITE_WAL_SUFFIX);Or better yet, check if any component of the path matches TEMP_IMPORT_DIR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should explicit match the dir name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src-tauri/src/backup.rs
Outdated
| let rel_path_display = rel_path.to_string_lossy(); | ||
| fs::create_dir_all(&dest_path) | ||
| .map_err(|e| format!("Failed to create directory {}: {}", rel_path_display, e))?; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Same issue as above: rel_path_display is only used in error messages. Move its declaration into the error closure to avoid allocation in the success path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will be modified in the next commit
| use tauri_plugin_store::StoreExt; | ||
| use serde_json::Value; | ||
| use walkdir::WalkDir; | ||
| use zip::write::SimpleFileOptions; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SimpleFileOptions type does not exist in the zip crate (version 2.x uses FileOptions). This code will not compile with the correct version of the zip crate. Please update to use FileOptions::default() instead of SimpleFileOptions::default().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zip crate version is 6.0 , use SImpleFileOption is ok
| let outpath = match file.enclosed_name() { | ||
| Some(path) => target_dir.join(path), | ||
| None => continue, | ||
| }; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential path traversal vulnerability: While enclosed_name() provides some protection, it's recommended to additionally verify that the outpath stays within target_dir after joining. Consider adding: if !outpath.starts_with(target_dir) { return Err("Path traversal detected".to_string()); } after line 96.
| }; | |
| }; | |
| // Additional path traversal protection | |
| if !outpath.starts_with(target_dir) { | |
| return Err("Path traversal detected".to_string()); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
app will make sure the outpath stays within target_dir
| let file = | ||
| File::create(&output_path).map_err(|e| format!("Failed to create zip file: {}", e))?; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Race condition potential: If the zip file already exists at output_path, File::create will truncate it. However, if the subsequent compression fails, the original backup file will be lost. Consider writing to a temporary file first and then renaming it to the final destination only after successful compression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
app is running on singletion, would not be happen
| let mut outfile = | ||
| File::create(&outpath).map_err(|e| format!("Failed to create file: {}", e))?; | ||
| std::io::copy(&mut file, &mut outfile) | ||
| .map_err(|e| format!("Failed to extract file: {}", e))?; | ||
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Missing file permissions preservation: The extraction process doesn't preserve Unix file permissions or attributes from the original files. For cross-platform compatibility, consider using set_permissions() with the permissions from the zip entry if available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will be modified in the next commit
src-tauri/src/backup.rs
Outdated
| .unwrap_or(false) | ||
| } | ||
|
|
||
| /// Compress a directory to a zip file |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function documentation says "Compress a directory to a zip file" but the function signature shows it takes a mutable ZipWriter reference, not creating the zip file itself. Consider updating the documentation to: "Add directory contents to an existing zip archive".
| /// Compress a directory to a zip file | |
| /// Add directory contents to an existing zip archive |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will be modified in the next commit
Replace system-dependent zip/unzip commands with the zip crate for cross-platform compatibility and better error handling.
Fix two bugs when restore backup files, see #771
I also write five unit test in backup.rs, it will import tempfile crates in dev-dependencies. And use walkdir crate to rewrite copy_dir_recursive function .If you prefer the previous version , I will change it and file a new PR
Adding zip and walkdir crates will increase the final binary size by approximately 1MB on Windows