From eeec42efb748e21837d88f034bca2cb053f80eb0 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Wed, 4 Feb 2026 12:36:39 -0800 Subject: [PATCH] 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. --- cspell.config.yaml | 4 + library/src/cache/disk_io.rs | 9 +- library/src/cache/patch_manager.rs | 26 +- library/src/file_errors.rs | 495 +++++++++++++++++++++++++++++ library/src/lib.rs | 1 + library/src/network.rs | 11 +- library/src/updater.rs | 21 +- 7 files changed, 541 insertions(+), 26 deletions(-) create mode 100644 library/src/file_errors.rs diff --git a/cspell.config.yaml b/cspell.config.yaml index ff82809..06b9dd9 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -35,7 +35,10 @@ words: - Decompressor - dllexport - dlopen + - EDQUOT - endtemplate + - ENOSPC + - EROFS - eseidel - ffigen - gclient @@ -65,6 +68,7 @@ words: - swiftpm - symbolication - unbootable + - unbooted - Verdana - vmcode - withf diff --git a/library/src/cache/disk_io.rs b/library/src/cache/disk_io.rs index cceef63..61a1acd 100644 --- a/library/src/cache/disk_io.rs +++ b/library/src/cache/disk_io.rs @@ -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)) diff --git a/library/src/cache/patch_manager.rs b/library/src/cache/patch_manager.rs index 99a0a47..22f12ea 100644 --- a/library/src/cache/patch_manager.rs +++ b/library/src/cache/patch_manager.rs @@ -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) } } diff --git a/library/src/file_errors.rs b/library/src/file_errors.rs new file mode 100644 index 0000000..803c05c --- /dev/null +++ b/library/src/file_errors.rs @@ -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 { + /// Adds enhanced error context to an IO operation result. + fn with_file_context(self, operation: FileOperation, path: &Path) -> anyhow::Result; +} + +impl IoResultExt for std::io::Result { + fn with_file_context(self, operation: FileOperation, path: &Path) -> anyhow::Result { + 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 = 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 = + 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 = + 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)); + } +} diff --git a/library/src/lib.rs b/library/src/lib.rs index 85ecf9e..4f3849e 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -12,6 +12,7 @@ pub mod c_api; mod cache; mod config; mod events; +mod file_errors; mod logging; mod network; mod time; diff --git a/library/src/network.rs b/library/src/network.rs index 741df77..a98f236 100644 --- a/library/src/network.rs +++ b/library/src/network.rs @@ -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(()) } diff --git a/library/src/updater.rs b/library/src/updater.rs index 451c9ef..0b772d6 100644 --- a/library/src/updater.rs +++ b/library/src/updater.rs @@ -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: // - 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> { #[cfg(all(not(test), not(target_os = "ios"), not(target_os = "android")))] fn patch_base(config: &UpdateConfig) -> anyhow::Result> { - 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(()) }