feat: add enhanced error messages for file operations (#310)
* feat: add enhanced error messages for file operations Add a file_errors module that provides context-aware error messages for file operations. When file operations fail, users now see: - The specific operation that failed (create, read, write, rename, etc.) - The full path involved - Helpful hints about possible causes based on error type - Android-specific hints for permission errors (SELinux, Work Profile, MDM/Knox policies, app cloning features) This helps diagnose issues like "Permission denied (os error 13)" by indicating which operation failed and suggesting possible causes.
This commit is contained in:
Vendored
+6
-3
@@ -1,4 +1,5 @@
|
||||
use anyhow::{bail, Context};
|
||||
use crate::file_errors::{FileOperation, IoResultExt};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{
|
||||
fs::File,
|
||||
@@ -21,9 +22,10 @@ where
|
||||
// Because File::create can sometimes fail if the full directory path doesn't exist,
|
||||
// we create the directories in its path first.
|
||||
std::fs::create_dir_all(containing_dir)
|
||||
.with_context(|| format!("Failed to create dir {:?}", path_as_ref))?;
|
||||
.with_file_context(FileOperation::CreateDir, containing_dir)?;
|
||||
|
||||
let file = File::create(path).with_context(|| format!("File::create for {:?}", path_as_ref))?;
|
||||
let file = File::create(path)
|
||||
.with_file_context(FileOperation::CreateFile, path_as_ref)?;
|
||||
let writer = BufWriter::new(file);
|
||||
serde_json::to_writer_pretty(writer, serializable)
|
||||
.with_context(|| format!("failed to serialize to {:?}", path_as_ref))
|
||||
@@ -41,7 +43,8 @@ where
|
||||
bail!("File {} does not exist", path_as_ref.display());
|
||||
}
|
||||
|
||||
let file = File::open(path_as_ref)?;
|
||||
let file = File::open(path_as_ref)
|
||||
.with_file_context(FileOperation::ReadFile, path_as_ref)?;
|
||||
let reader = BufReader::new(file);
|
||||
serde_json::from_reader(reader)
|
||||
.with_context(|| format!("failed to deserialize from {:?}", &path_as_ref))
|
||||
|
||||
Vendored
+14
-12
@@ -1,6 +1,7 @@
|
||||
use super::{disk_io, signing, PatchInfo};
|
||||
use crate::yaml::PatchVerificationMode;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crate::file_errors::{FileOperation, IoResultExt};
|
||||
use core::fmt::Debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@@ -231,7 +232,8 @@ impl PatchManager {
|
||||
);
|
||||
}
|
||||
|
||||
let artifact_size_on_disk = std::fs::metadata(&artifact_path)?.len();
|
||||
let artifact_size_on_disk = std::fs::metadata(&artifact_path)
|
||||
.with_file_context(FileOperation::GetMetadata, &artifact_path)?.len();
|
||||
if artifact_size_on_disk != patch.size {
|
||||
bail!(
|
||||
"Patch {} has size {} on disk, but expected size {}",
|
||||
@@ -275,7 +277,7 @@ impl PatchManager {
|
||||
shorebird_error!("Failed to delete patch dir {}: {}", patch_dir.display(), e);
|
||||
e
|
||||
})
|
||||
.with_context(|| format!("Failed to delete patch dir {}", &patch_dir.display()))
|
||||
.with_file_context(FileOperation::DeleteDir, &patch_dir)
|
||||
}
|
||||
|
||||
/// Deletes artifacts for the provided bad_patch_number and attempts to set the next_boot_patch to the last
|
||||
@@ -392,14 +394,17 @@ impl ManagePatches for PatchManager {
|
||||
|
||||
let patch_path = self.patch_artifact_path(patch_number);
|
||||
|
||||
std::fs::create_dir_all(self.patch_dir(patch_number))
|
||||
.with_context(|| format!("create_dir_all failed for {}", patch_path.display()))?;
|
||||
let patch_dir = self.patch_dir(patch_number);
|
||||
std::fs::create_dir_all(&patch_dir)
|
||||
.with_file_context(FileOperation::CreateDir, &patch_dir)?;
|
||||
|
||||
std::fs::rename(file_path, &patch_path)?;
|
||||
std::fs::rename(file_path, &patch_path)
|
||||
.with_file_context(FileOperation::RenameFile, file_path)?;
|
||||
|
||||
let new_patch = PatchMetadata {
|
||||
number: patch_number,
|
||||
size: std::fs::metadata(&patch_path)?.len(),
|
||||
size: std::fs::metadata(&patch_path)
|
||||
.with_file_context(FileOperation::GetMetadata, &patch_path)?.len(),
|
||||
hash: hash.to_owned(),
|
||||
signature: signature.map(|s| s.to_owned()),
|
||||
};
|
||||
@@ -524,12 +529,9 @@ impl ManagePatches for PatchManager {
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
self.patches_state = PatchesState::default();
|
||||
self.save_patches_state()?;
|
||||
std::fs::remove_dir_all(self.patches_dir()).with_context(|| {
|
||||
format!(
|
||||
"Failed to delete patches dir {}",
|
||||
self.patches_dir().display()
|
||||
)
|
||||
})
|
||||
let patches_dir = self.patches_dir();
|
||||
std::fs::remove_dir_all(&patches_dir)
|
||||
.with_file_context(FileOperation::DeleteDir, &patches_dir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,495 @@
|
||||
// This module provides enhanced error messages for file operations.
|
||||
// It detects specific error types and provides more helpful context.
|
||||
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
|
||||
/// Describes the type of file operation that failed.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FileOperation {
|
||||
CreateDir,
|
||||
CreateFile,
|
||||
WriteFile,
|
||||
ReadFile,
|
||||
DeleteFile,
|
||||
DeleteDir,
|
||||
RenameFile,
|
||||
GetMetadata,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FileOperation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FileOperation::CreateDir => write!(f, "create directory"),
|
||||
FileOperation::CreateFile => write!(f, "create file"),
|
||||
FileOperation::WriteFile => write!(f, "write to file"),
|
||||
FileOperation::ReadFile => write!(f, "read file"),
|
||||
FileOperation::DeleteFile => write!(f, "delete file"),
|
||||
FileOperation::DeleteDir => write!(f, "delete directory"),
|
||||
FileOperation::RenameFile => write!(f, "rename/move file"),
|
||||
FileOperation::GetMetadata => write!(f, "get file metadata"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an enhanced error message for a file operation failure.
|
||||
///
|
||||
/// This function takes an IO error and adds context about what operation failed,
|
||||
/// what path was involved, and provides hints about possible causes based on
|
||||
/// the error type.
|
||||
pub fn enhance_io_error(
|
||||
error: &std::io::Error,
|
||||
operation: FileOperation,
|
||||
path: &Path,
|
||||
) -> String {
|
||||
let base_message = format!(
|
||||
"Failed to {} '{}': {}",
|
||||
operation,
|
||||
path.display(),
|
||||
error
|
||||
);
|
||||
|
||||
let hint = get_error_hint(error, operation);
|
||||
|
||||
if hint.is_empty() {
|
||||
base_message
|
||||
} else {
|
||||
format!("{}\nPossible cause: {}", base_message, hint)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a hint about possible causes for the given error type.
|
||||
fn get_error_hint(error: &std::io::Error, operation: FileOperation) -> String {
|
||||
match error.kind() {
|
||||
ErrorKind::PermissionDenied => get_permission_denied_hint(operation),
|
||||
ErrorKind::NotFound => get_not_found_hint(operation),
|
||||
ErrorKind::AlreadyExists => {
|
||||
"A file or directory with this name already exists.".to_string()
|
||||
}
|
||||
ErrorKind::StorageFull => {
|
||||
"The device storage is full. Free up space and try again.".to_string()
|
||||
}
|
||||
ErrorKind::ReadOnlyFilesystem => {
|
||||
"The filesystem is mounted as read-only.".to_string()
|
||||
}
|
||||
_ => {
|
||||
// Check raw OS error for cases not covered by ErrorKind
|
||||
if let Some(os_error) = error.raw_os_error() {
|
||||
get_os_error_hint(os_error)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns hints specific to permission denied errors.
|
||||
fn get_permission_denied_hint(operation: FileOperation) -> String {
|
||||
match operation {
|
||||
FileOperation::CreateDir | FileOperation::CreateFile | FileOperation::WriteFile => {
|
||||
"The app may not have write access to this location.".to_string()
|
||||
}
|
||||
FileOperation::ReadFile => {
|
||||
"The app may not have read access to this file.".to_string()
|
||||
}
|
||||
FileOperation::DeleteFile | FileOperation::DeleteDir => {
|
||||
"The app may not have permission to delete this item.".to_string()
|
||||
}
|
||||
FileOperation::RenameFile => {
|
||||
"The app may not have permission to move files in this location.".to_string()
|
||||
}
|
||||
FileOperation::GetMetadata => {
|
||||
"The app may not have permission to access this file's metadata.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns hints specific to not found errors.
|
||||
fn get_not_found_hint(operation: FileOperation) -> String {
|
||||
match operation {
|
||||
FileOperation::CreateDir | FileOperation::CreateFile | FileOperation::WriteFile => {
|
||||
"The parent directory may not exist.".to_string()
|
||||
}
|
||||
FileOperation::RenameFile => {
|
||||
"The source file or destination directory may not exist.".to_string()
|
||||
}
|
||||
_ => {
|
||||
"The file or directory does not exist.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns hints for specific OS error codes not covered by ErrorKind.
|
||||
fn get_os_error_hint(os_error: i32) -> String {
|
||||
// Unix/Linux error codes
|
||||
match os_error {
|
||||
28 => "The device storage is full (ENOSPC). Free up space and try again.".to_string(),
|
||||
30 => "The filesystem is mounted as read-only (EROFS).".to_string(),
|
||||
122 => "Disk quota exceeded (EDQUOT). The user's storage quota has been reached.".to_string(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait extension for adding enhanced context to IO Results.
|
||||
pub trait IoResultExt<T> {
|
||||
/// Adds enhanced error context to an IO operation result.
|
||||
fn with_file_context(self, operation: FileOperation, path: &Path) -> anyhow::Result<T>;
|
||||
}
|
||||
|
||||
impl<T> IoResultExt<T> for std::io::Result<T> {
|
||||
fn with_file_context(self, operation: FileOperation, path: &Path) -> anyhow::Result<T> {
|
||||
self.map_err(|e| {
|
||||
let enhanced_message = enhance_io_error(&e, operation, path);
|
||||
anyhow::Error::new(e).context(enhanced_message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{Error, ErrorKind};
|
||||
|
||||
// ==================== FileOperation Display Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_operation_display_all_variants() {
|
||||
assert_eq!(format!("{}", FileOperation::CreateDir), "create directory");
|
||||
assert_eq!(format!("{}", FileOperation::CreateFile), "create file");
|
||||
assert_eq!(format!("{}", FileOperation::WriteFile), "write to file");
|
||||
assert_eq!(format!("{}", FileOperation::ReadFile), "read file");
|
||||
assert_eq!(format!("{}", FileOperation::DeleteFile), "delete file");
|
||||
assert_eq!(format!("{}", FileOperation::DeleteDir), "delete directory");
|
||||
assert_eq!(format!("{}", FileOperation::RenameFile), "rename/move file");
|
||||
assert_eq!(format!("{}", FileOperation::GetMetadata), "get file metadata");
|
||||
}
|
||||
|
||||
// ==================== enhance_io_error Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_enhance_io_error_includes_operation_path_and_error() {
|
||||
let error = Error::new(ErrorKind::Other, "some error");
|
||||
let path = Path::new("/some/path/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::ReadFile, path);
|
||||
|
||||
assert!(message.contains("Failed to read file"));
|
||||
assert!(message.contains("/some/path/file.txt"));
|
||||
assert!(message.contains("some error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enhance_io_error_no_hint_for_unknown_error() {
|
||||
let error = Error::new(ErrorKind::Other, "unknown error");
|
||||
let path = Path::new("/path/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::ReadFile, path);
|
||||
|
||||
// Should not contain "Possible cause" for unknown errors
|
||||
assert!(!message.contains("Possible cause"));
|
||||
}
|
||||
|
||||
// ==================== Permission Denied Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_create_dir() {
|
||||
let error = Error::new(ErrorKind::PermissionDenied, "Permission denied");
|
||||
let path = Path::new("/protected/dir");
|
||||
let message = enhance_io_error(&error, FileOperation::CreateDir, path);
|
||||
|
||||
assert!(message.contains("Failed to create directory"));
|
||||
assert!(message.contains("write access"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_create_file() {
|
||||
let error = Error::new(ErrorKind::PermissionDenied, "Permission denied");
|
||||
let path = Path::new("/protected/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::CreateFile, path);
|
||||
|
||||
assert!(message.contains("Failed to create file"));
|
||||
assert!(message.contains("write access"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_write_file() {
|
||||
let error = Error::new(ErrorKind::PermissionDenied, "Permission denied");
|
||||
let path = Path::new("/protected/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::WriteFile, path);
|
||||
|
||||
assert!(message.contains("Failed to write to file"));
|
||||
assert!(message.contains("write access"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_read_file() {
|
||||
let error = Error::new(ErrorKind::PermissionDenied, "Permission denied");
|
||||
let path = Path::new("/protected/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::ReadFile, path);
|
||||
|
||||
assert!(message.contains("Failed to read file"));
|
||||
assert!(message.contains("read access"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_delete_file() {
|
||||
let error = Error::new(ErrorKind::PermissionDenied, "Permission denied");
|
||||
let path = Path::new("/protected/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::DeleteFile, path);
|
||||
|
||||
assert!(message.contains("Failed to delete file"));
|
||||
assert!(message.contains("permission to delete"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_delete_dir() {
|
||||
let error = Error::new(ErrorKind::PermissionDenied, "Permission denied");
|
||||
let path = Path::new("/protected/dir");
|
||||
let message = enhance_io_error(&error, FileOperation::DeleteDir, path);
|
||||
|
||||
assert!(message.contains("Failed to delete directory"));
|
||||
assert!(message.contains("permission to delete"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_rename_file() {
|
||||
let error = Error::new(ErrorKind::PermissionDenied, "Permission denied");
|
||||
let path = Path::new("/protected/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::RenameFile, path);
|
||||
|
||||
assert!(message.contains("Failed to rename/move file"));
|
||||
assert!(message.contains("permission to move"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_get_metadata() {
|
||||
let error = Error::new(ErrorKind::PermissionDenied, "Permission denied");
|
||||
let path = Path::new("/protected/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::GetMetadata, path);
|
||||
|
||||
assert!(message.contains("Failed to get file metadata"));
|
||||
assert!(message.contains("permission to access"));
|
||||
assert!(message.contains("metadata"));
|
||||
}
|
||||
|
||||
// ==================== Not Found Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_not_found_create_dir() {
|
||||
let error = Error::new(ErrorKind::NotFound, "No such file or directory");
|
||||
let path = Path::new("/nonexistent/parent/newdir");
|
||||
let message = enhance_io_error(&error, FileOperation::CreateDir, path);
|
||||
|
||||
assert!(message.contains("Failed to create directory"));
|
||||
assert!(message.contains("parent directory may not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_found_create_file() {
|
||||
let error = Error::new(ErrorKind::NotFound, "No such file or directory");
|
||||
let path = Path::new("/nonexistent/parent/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::CreateFile, path);
|
||||
|
||||
assert!(message.contains("Failed to create file"));
|
||||
assert!(message.contains("parent directory may not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_found_write_file() {
|
||||
let error = Error::new(ErrorKind::NotFound, "No such file or directory");
|
||||
let path = Path::new("/nonexistent/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::WriteFile, path);
|
||||
|
||||
assert!(message.contains("Failed to write to file"));
|
||||
assert!(message.contains("parent directory may not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_found_read_file() {
|
||||
let error = Error::new(ErrorKind::NotFound, "No such file or directory");
|
||||
let path = Path::new("/nonexistent/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::ReadFile, path);
|
||||
|
||||
assert!(message.contains("Failed to read file"));
|
||||
assert!(message.contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_found_delete_file() {
|
||||
let error = Error::new(ErrorKind::NotFound, "No such file or directory");
|
||||
let path = Path::new("/nonexistent/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::DeleteFile, path);
|
||||
|
||||
assert!(message.contains("Failed to delete file"));
|
||||
assert!(message.contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_found_delete_dir() {
|
||||
let error = Error::new(ErrorKind::NotFound, "No such file or directory");
|
||||
let path = Path::new("/nonexistent/dir");
|
||||
let message = enhance_io_error(&error, FileOperation::DeleteDir, path);
|
||||
|
||||
assert!(message.contains("Failed to delete directory"));
|
||||
assert!(message.contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_found_rename_file() {
|
||||
let error = Error::new(ErrorKind::NotFound, "No such file or directory");
|
||||
let path = Path::new("/nonexistent/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::RenameFile, path);
|
||||
|
||||
assert!(message.contains("Failed to rename/move file"));
|
||||
assert!(message.contains("source file or destination directory may not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_found_get_metadata() {
|
||||
let error = Error::new(ErrorKind::NotFound, "No such file or directory");
|
||||
let path = Path::new("/nonexistent/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::GetMetadata, path);
|
||||
|
||||
assert!(message.contains("Failed to get file metadata"));
|
||||
assert!(message.contains("does not exist"));
|
||||
}
|
||||
|
||||
// ==================== Other Error Kind Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_already_exists_error() {
|
||||
let error = Error::new(ErrorKind::AlreadyExists, "File exists");
|
||||
let path = Path::new("/existing/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::CreateFile, path);
|
||||
|
||||
assert!(message.contains("Failed to create file"));
|
||||
assert!(message.contains("already exists"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_storage_full_error() {
|
||||
let error = Error::new(ErrorKind::StorageFull, "No space left on device");
|
||||
let path = Path::new("/data/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::WriteFile, path);
|
||||
|
||||
assert!(message.contains("Failed to write to file"));
|
||||
assert!(message.contains("storage is full"));
|
||||
assert!(message.contains("Free up space"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_only_filesystem_error() {
|
||||
let error = Error::new(ErrorKind::ReadOnlyFilesystem, "Read-only file system");
|
||||
let path = Path::new("/readonly/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::WriteFile, path);
|
||||
|
||||
assert!(message.contains("Failed to write to file"));
|
||||
assert!(message.contains("read-only"));
|
||||
}
|
||||
|
||||
// ==================== OS Error Code Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_get_os_error_hint_enospc() {
|
||||
// Test the get_os_error_hint function directly for ENOSPC (28)
|
||||
let hint = get_os_error_hint(28);
|
||||
assert!(hint.contains("ENOSPC"));
|
||||
assert!(hint.contains("storage is full"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_os_error_hint_erofs() {
|
||||
// Test the get_os_error_hint function directly for EROFS (30)
|
||||
let hint = get_os_error_hint(30);
|
||||
assert!(hint.contains("EROFS"));
|
||||
assert!(hint.contains("read-only"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_os_error_hint_edquot() {
|
||||
// Test the get_os_error_hint function directly for EDQUOT (122)
|
||||
let hint = get_os_error_hint(122);
|
||||
assert!(hint.contains("EDQUOT"));
|
||||
assert!(hint.contains("quota"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_os_error_hint_unknown_code() {
|
||||
// Unknown error codes should return empty string
|
||||
let hint = get_os_error_hint(9999);
|
||||
assert!(hint.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_os_error_unknown_code_in_enhance() {
|
||||
// Use an unlikely error code that won't map to a known ErrorKind
|
||||
let error = Error::from_raw_os_error(9999);
|
||||
let path = Path::new("/data/file.txt");
|
||||
let message = enhance_io_error(&error, FileOperation::WriteFile, path);
|
||||
|
||||
// Should not have a "Possible cause" hint for unknown OS errors
|
||||
assert!(!message.contains("Possible cause"));
|
||||
}
|
||||
|
||||
// ==================== IoResultExt Trait Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_io_result_ext_ok() {
|
||||
let result: std::io::Result<i32> = Ok(42);
|
||||
let path = Path::new("/some/path");
|
||||
|
||||
let converted = result.with_file_context(FileOperation::ReadFile, path);
|
||||
|
||||
assert!(converted.is_ok());
|
||||
assert_eq!(converted.unwrap(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_io_result_ext_err() {
|
||||
let result: std::io::Result<i32> =
|
||||
Err(Error::new(ErrorKind::PermissionDenied, "Permission denied"));
|
||||
let path = Path::new("/protected/file.txt");
|
||||
|
||||
let converted = result.with_file_context(FileOperation::ReadFile, path);
|
||||
|
||||
assert!(converted.is_err());
|
||||
let err_string = converted.unwrap_err().to_string();
|
||||
assert!(err_string.contains("Failed to read file"));
|
||||
assert!(err_string.contains("/protected/file.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_io_result_ext_preserves_error_chain() {
|
||||
let result: std::io::Result<i32> =
|
||||
Err(Error::new(ErrorKind::NotFound, "No such file"));
|
||||
let path = Path::new("/missing/file.txt");
|
||||
|
||||
let converted = result.with_file_context(FileOperation::ReadFile, path);
|
||||
let err = converted.unwrap_err();
|
||||
|
||||
// The error chain should contain both the enhanced message and the original error
|
||||
let err_string = format!("{:?}", err);
|
||||
assert!(err_string.contains("No such file"));
|
||||
assert!(err_string.contains("Failed to read file"));
|
||||
}
|
||||
|
||||
// ==================== FileOperation Debug/Clone Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_file_operation_debug() {
|
||||
assert_eq!(format!("{:?}", FileOperation::CreateDir), "CreateDir");
|
||||
assert_eq!(format!("{:?}", FileOperation::ReadFile), "ReadFile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_operation_clone() {
|
||||
let op = FileOperation::WriteFile;
|
||||
let cloned = op;
|
||||
assert_eq!(format!("{}", op), format!("{}", cloned));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_operation_copy() {
|
||||
let op1 = FileOperation::DeleteDir;
|
||||
let op2 = op1; // Copy
|
||||
assert_eq!(format!("{}", op1), format!("{}", op2));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod c_api;
|
||||
mod cache;
|
||||
mod config;
|
||||
mod events;
|
||||
mod file_errors;
|
||||
mod logging;
|
||||
mod network;
|
||||
mod time;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file's job is to deal with the update_server and network side
|
||||
// of the updater library.
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use anyhow::bail;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
@@ -10,6 +10,7 @@ use std::string::ToString;
|
||||
|
||||
use crate::config::{current_arch, current_platform, UpdateConfig};
|
||||
use crate::events::PatchEvent;
|
||||
use crate::file_errors::{FileOperation, IoResultExt};
|
||||
|
||||
pub fn patches_check_url(base_url: &str) -> String {
|
||||
format!("{base_url}/api/v1/patches/check")
|
||||
@@ -234,12 +235,14 @@ pub fn download_to_path(
|
||||
if let Some(parent) = path.parent() {
|
||||
shorebird_debug!("Creating download directory: {:?}", parent);
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("create_dir_all failed for {}", parent.display()))?;
|
||||
.with_file_context(FileOperation::CreateDir, parent)?;
|
||||
}
|
||||
|
||||
shorebird_info!("Writing patch to: {:?}", path);
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(&bytes)?;
|
||||
let mut file = File::create(path)
|
||||
.with_file_context(FileOperation::CreateFile, path)?;
|
||||
file.write_all(&bytes)
|
||||
.with_file_context(FileOperation::WriteFile, path)?;
|
||||
shorebird_info!("Wrote patch to: {:?}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+14
-7
@@ -6,6 +6,7 @@ use std::io::{Cursor, Read, Seek};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crate::file_errors::{FileOperation, IoResultExt};
|
||||
use dyn_clone::DynClone;
|
||||
|
||||
use crate::cache::{PatchInfo, UpdaterState};
|
||||
@@ -297,9 +298,11 @@ fn check_hash(path: &Path, expected_string: &str) -> anyhow::Result<()> {
|
||||
// Based on guidance from:
|
||||
// <https://github.com/RustCrypto/hashes#hashing-readable-objects>
|
||||
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut file = fs::File::open(path)
|
||||
.with_file_context(FileOperation::ReadFile, path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
std::io::copy(&mut file, &mut hasher)?;
|
||||
std::io::copy(&mut file, &mut hasher)
|
||||
.with_file_context(FileOperation::ReadFile, path)?;
|
||||
// Check that the length from copy is the same as the file size?
|
||||
let hash = hasher.finalize();
|
||||
let hash_matches = hash.as_slice() == expected;
|
||||
@@ -339,7 +342,8 @@ fn patch_base(config: &UpdateConfig) -> anyhow::Result<Box<dyn ReadSeek>> {
|
||||
|
||||
#[cfg(all(not(test), not(target_os = "ios"), not(target_os = "android")))]
|
||||
fn patch_base(config: &UpdateConfig) -> anyhow::Result<Box<dyn ReadSeek>> {
|
||||
let file = fs::File::open(&config.libapp_path)?;
|
||||
let file = fs::File::open(&config.libapp_path)
|
||||
.with_file_context(FileOperation::ReadFile, &config.libapp_path)?;
|
||||
Ok(Box::new(file))
|
||||
}
|
||||
|
||||
@@ -520,9 +524,10 @@ where
|
||||
shorebird_info!("Inflating patch from {:?}", patch_path);
|
||||
let compressed_patch_r = BufReader::new(
|
||||
fs::File::open(patch_path)
|
||||
.context(format!("Failed to open patch file: {:?}", patch_path))?,
|
||||
.with_file_context(FileOperation::ReadFile, patch_path)?,
|
||||
);
|
||||
let output_file_w = fs::File::create(output_path)?;
|
||||
let output_file_w = fs::File::create(output_path)
|
||||
.with_file_context(FileOperation::CreateFile, output_path)?;
|
||||
|
||||
// Set up a pipe to connect the writing from the decompression thread
|
||||
// to the reading of the decompressed patch data on this thread.
|
||||
@@ -542,11 +547,13 @@ where
|
||||
});
|
||||
|
||||
// Do the patch, using the uncompressed patch data from the pipe.
|
||||
let mut fresh_r = bipatch::Reader::new(patch_r, base_r)?;
|
||||
let mut fresh_r = bipatch::Reader::new(patch_r, base_r)
|
||||
.context("Failed to initialize patch reader")?;
|
||||
|
||||
// Write out the resulting patched file to the new location.
|
||||
let mut output_w = BufWriter::new(output_file_w);
|
||||
std::io::copy(&mut fresh_r, &mut output_w)?;
|
||||
std::io::copy(&mut fresh_r, &mut output_w)
|
||||
.with_file_context(FileOperation::WriteFile, output_path)?;
|
||||
shorebird_info!("Patch successfully applied to {:?}", output_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user