Skip to content

Conversation

@NitronPlus
Copy link
Contributor

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

    Replace system-dependent zip/unzip commands with the `zip` crate for
    cross-platform compatibility and better error handling.
Copilot AI review requested due to automatic review settings November 13, 2025 03:17
Copy link

Copilot AI left a 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/unzip commands with the zip crate for compression and extraction operations
  • Added helper functions for file filtering and introduced a TempDirGuard for 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,
};

Copy link

Copilot AI Nov 13, 2025

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.

Suggested change
// 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());
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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

tauri-plugin-opener = "2.4.0"
uuid = { version = "1.10", features = ["v4"] }
dirs = "5.0"
zip = "6"
Copy link

Copilot AI Nov 13, 2025

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.

Suggested change
zip = "6"
zip = "6.2"

Copilot uses AI. Check for mistakes.
Comment on lines 19 to 29
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
}
Copy link

Copilot AI Nov 13, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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

Copy link

Copilot AI left a 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.

Comment on lines 244 to 246
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))?;
Copy link

Copilot AI Nov 13, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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;
Copy link

Copilot AI Nov 13, 2025

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().

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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,
};
Copy link

Copilot AI Nov 13, 2025

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.

Suggested change
};
};
// Additional path traversal protection
if !outpath.starts_with(target_dir) {
return Err("Path traversal detected".to_string());
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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

Comment on lines +133 to +134
let file =
File::create(&output_path).map_err(|e| format!("Failed to create zip file: {}", e))?;
Copy link

Copilot AI Nov 13, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@NitronPlus NitronPlus Nov 13, 2025

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

Comment on lines 111 to 115
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))?;
}
Copy link

Copilot AI Nov 13, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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

.unwrap_or(false)
}

/// Compress a directory to a zip file
Copy link

Copilot AI Nov 13, 2025

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".

Suggested change
/// Compress a directory to a zip file
/// Add directory contents to an existing zip archive

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant