From 22bca8632b3c81eaa8e48e72b96295d4d2f2b9fb Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 4 Feb 2025 15:41:16 -0700 Subject: [PATCH] Format --- src/app.rs | 80 ++++---- src/operation/mod.rs | 476 ++++++++++++++++++++++++------------------- 2 files changed, 300 insertions(+), 256 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2f49153..df56ebd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -59,6 +59,7 @@ use trash::TrashItem; #[cfg(feature = "wayland")] use wayland_client::{protocol::wl_output::WlOutput, Proxy}; +use crate::operation::{OperationError, OperationErrorType}; use crate::{ clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, config::{AppTheme, Config, DesktopConfig, Favorite, IconSizes, TabConfig}, @@ -71,7 +72,6 @@ use crate::{ spawn_detached::spawn_detached, tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION}, }; -use crate::operation::{OperationError, OperationErrorType}; #[derive(Clone, Debug)] pub enum Mode { @@ -418,13 +418,13 @@ pub enum DialogPage { to: PathBuf, name: String, archive_type: ArchiveType, - password: Option + password: Option, }, EmptyTrash, FailedOperation(u64), ExtractPassword { id: u64, - password: String + password: String, }, MountError { mounter_key: MounterKey, @@ -1890,7 +1890,7 @@ impl Application for App { to, name, archive_type, - password: None + password: None, }); return widget::text_input::focus(self.dialog_text_input.clone()); } @@ -1968,7 +1968,7 @@ impl Application for App { to, name, archive_type, - password + password, } => { let extension = archive_type.extension(); let name = format!("{}{}", name, extension); @@ -1977,7 +1977,7 @@ impl Application for App { paths, to, archive_type, - password + password, }) } DialogPage::EmptyTrash => { @@ -1986,18 +1986,15 @@ impl Application for App { DialogPage::FailedOperation(id) => { log::warn!("TODO: retry operation {}", id); } - DialogPage::ExtractPassword { - id, - password - } => { + DialogPage::ExtractPassword { id, password } => { let (operation, _, _err) = self.failed_operations.get(&id).unwrap(); let new_op = match &operation { Operation::Extract { to, paths, .. } => Operation::Extract { to: to.clone(), paths: paths.clone(), - password: Some(password) + password: Some(password), }, - _ => unreachable!() + _ => unreachable!(), }; self.operation(new_op); } @@ -2117,7 +2114,7 @@ impl Application for App { self.operation(Operation::Extract { paths, to: destination, - password: None + password: None, }); } } @@ -2598,19 +2595,18 @@ impl Application for App { if let Some((op, controller)) = self.pending_operations.remove(&id) { // Only show dialog if not cancelled if !controller.is_cancelled() { - self.dialog_pages.push_back( - match err.kind { - OperationErrorType::Generic(_) => DialogPage::FailedOperation(id), - OperationErrorType::PasswordRequired => DialogPage::ExtractPassword { - id: id, - password: String::from("") - } - } - ); + self.dialog_pages.push_back(match err.kind { + OperationErrorType::Generic(_) => DialogPage::FailedOperation(id), + OperationErrorType::PasswordRequired => DialogPage::ExtractPassword { + id: id, + password: String::from(""), + }, + }); } // Remove from progress self.progress_operations.remove(&id); - self.failed_operations.insert(id, (op, controller, err.to_string())); + self.failed_operations + .insert(id, (op, controller, err.to_string())); } // Close progress notification if all relavent operations are finished if !self @@ -3605,7 +3601,7 @@ impl Application for App { to, name, archive_type, - password + password, } => { let mut dialog = widget::dialog().title(fl!("create-archive")); @@ -3683,10 +3679,11 @@ impl Application for App { if *archive_type == ArchiveType::Zip { let password_unwrapped = password.clone().unwrap_or_else(String::default); - dialog = dialog.control( - widget::column::with_children(vec![ - widget::text::body(fl!("password")).into(), - widget::text_input("", password_unwrapped).password().on_input(move |password_unwrapped| { + dialog = dialog.control(widget::column::with_children(vec![ + widget::text::body(fl!("password")).into(), + widget::text_input("", password_unwrapped) + .password() + .on_input(move |password_unwrapped| { Message::DialogUpdate(DialogPage::Compress { paths: paths.clone(), to: to.clone(), @@ -3695,10 +3692,9 @@ impl Application for App { password: Some(password_unwrapped), }) }) - .on_submit_maybe(complete_maybe) - .into(), - ]) - ); + .on_submit_maybe(complete_maybe) + .into(), + ])); } dialog @@ -3726,21 +3722,18 @@ impl Application for App { widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) } - DialogPage::ExtractPassword { - id, - password - } => { + DialogPage::ExtractPassword { id, password } => { widget::dialog() .title(fl!("extract-password-required")) .icon(widget::icon::from_name("dialog-error").size(64)) - .control(widget::text_input("", password).password().on_input(move |password| { - Message::DialogUpdate(DialogPage::ExtractPassword { - id: *id, - password - }) - })) + .control(widget::text_input("", password).password().on_input( + move |password| { + Message::DialogUpdate(DialogPage::ExtractPassword { id: *id, password }) + }, + )) .primary_action( - widget::button::suggested(fl!("extract-here")).on_press(Message::DialogComplete), + widget::button::suggested(fl!("extract-here")) + .on_press(Message::DialogComplete), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), @@ -4777,7 +4770,6 @@ impl Application for App { .await .send(Message::PendingError(id, err)) .await; - } } diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 6bc4e5b..83c8eed 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -1,17 +1,3 @@ -use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt}; -use std::{ - borrow::Cow, - fs, - io::{self, Read, Write}, - path::{Path, PathBuf}, - sync::Arc, -}; -use std::collections::VecDeque; -use std::fmt::{Formatter}; -use tokio::sync::{mpsc, Mutex as TokioMutex}; -use walkdir::WalkDir; -use zip::AesMode::Aes256; -use zip::result::ZipError; use crate::{ app::{ArchiveType, DialogPage, Message}, config::IconSizes, @@ -20,6 +6,20 @@ use crate::{ spawn_detached::spawn_detached, tab, }; +use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt}; +use std::collections::VecDeque; +use std::fmt::Formatter; +use std::{ + borrow::Cow, + fs, + io::{self, Read, Write}, + path::{Path, PathBuf}, + sync::Arc, +}; +use tokio::sync::{mpsc, Mutex as TokioMutex}; +use walkdir::WalkDir; +use zip::result::ZipError; +use zip::AesMode::Aes256; pub use self::controller::{Controller, ControllerState}; pub mod controller; @@ -94,7 +94,7 @@ fn zip_extract>( archive: &mut zip::ZipArchive, directory: P, controller: Controller, - password: Option + password: Option, ) -> zip::result::ZipResult<()> { use std::{ffi::OsString, fs}; use zip::result::ZipError; @@ -115,7 +115,6 @@ fn zip_extract>( Ok(()) } - #[cfg(unix)] let mut files_by_unix_mode = Vec::new(); let mut buffer = vec![0; 4 * 1024 * 1024]; @@ -131,8 +130,9 @@ fn zip_extract>( let mut file = match &password { None => archive.by_index(i), - Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()) - }.map_err(|e| e)?; + Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), + } + .map_err(|e| e)?; let filepath = file .enclosed_name() .ok_or(ZipError::InvalidArchive("Invalid file path"))?; @@ -193,8 +193,9 @@ fn zip_extract>( } let mut file = match &password { None => archive.by_index(i), - Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()) - }.map_err(|e| e)?; + Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), + } + .map_err(|e| e)?; // create all pending dirs while let Some(pending_dir) = pending_directory_creates.pop_front() { @@ -322,7 +323,9 @@ async fn copy_or_move( }); } - context.recursive_copy_or_move(from_to_pairs, moving).map_err(OperationError::from_str)?; + context + .recursive_copy_or_move(from_to_pairs, moving) + .map_err(OperationError::from_str)?; Ok(context.op_sel) }) @@ -446,7 +449,7 @@ pub enum Operation { paths: Vec, to: PathBuf, archive_type: ArchiveType, - password: Option + password: Option, }, /// Copy items Copy { @@ -463,7 +466,7 @@ pub enum Operation { Extract { paths: Vec, to: PathBuf, - password: Option + password: Option, }, /// Move items Move { @@ -493,7 +496,7 @@ pub enum Operation { #[derive(Clone, Debug)] pub enum OperationErrorType { Generic(String), - PasswordRequired + PasswordRequired, } #[derive(Clone, Debug)] pub struct OperationError { @@ -512,7 +515,7 @@ impl std::fmt::Display for OperationError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self.kind { OperationErrorType::Generic(s) => s.fmt(f), - OperationErrorType::PasswordRequired => f.write_str("Password required") + OperationErrorType::PasswordRequired => f.write_str("Password required"), } } } @@ -548,7 +551,11 @@ impl Operation { progress = progress() ), Self::EmptyTrash => fl!("emptying-trash", progress = progress()), - Self::Extract { paths, to, password: _ } => fl!( + Self::Extract { + paths, + to, + password: _, + } => fl!( "extracting", items = paths.len(), from = paths_parent_name(paths), @@ -603,7 +610,11 @@ impl Operation { to = fl!("trash") ), Self::EmptyTrash => fl!("emptied-trash"), - Self::Extract { paths, to, password: _ } => fl!( + Self::Extract { + paths, + to, + password: _, + } => fl!( "extracted", items = paths.len(), from = paths_parent_name(paths), @@ -674,123 +685,147 @@ impl Operation { paths, to, archive_type, - password + password, } => { - tokio::task::spawn_blocking(move || -> Result { - let Some(relative_root) = to.parent() else { - return Err(OperationError::from_str(format!("path {:?} has no parent directory", to))); - }; + tokio::task::spawn_blocking( + move || -> Result { + let Some(relative_root) = to.parent() else { + return Err(OperationError::from_str(format!( + "path {:?} has no parent directory", + to + ))); + }; - let op_sel = OperationSelection { - ignored: paths.clone(), - selected: vec![to.clone()], - }; + let op_sel = OperationSelection { + ignored: paths.clone(), + selected: vec![to.clone()], + }; - let mut paths = paths; - for path in paths.clone().iter() { - if path.is_dir() { - let new_paths_it = WalkDir::new(path).into_iter(); - for entry in new_paths_it.skip(1) { - let entry = entry.map_err(OperationError::from_str)?; - paths.push(entry.into_path()); - } - } - } - - match archive_type { - ArchiveType::Tgz => { - let mut archive = fs::File::create(&to) - .map(io::BufWriter::new) - .map(|w| { - flate2::write::GzEncoder::new(w, flate2::Compression::default()) - }) - .map(tar::Builder::new) - .map_err(OperationError::from_str)?; - - let total_paths = paths.len(); - for (i, path) in paths.iter().enumerate() { - controller.check().map_err(OperationError::from_str)?; - - controller.set_progress((i as f32) / total_paths as f32); - - if let Some(relative_path) = - path.strip_prefix(relative_root).map_err(OperationError::from_str)?.to_str() - { - archive - .append_path_with_name(path, relative_path) - .map_err(OperationError::from_str)?; + let mut paths = paths; + for path in paths.clone().iter() { + if path.is_dir() { + let new_paths_it = WalkDir::new(path).into_iter(); + for entry in new_paths_it.skip(1) { + let entry = entry.map_err(OperationError::from_str)?; + paths.push(entry.into_path()); } } - - archive.finish().map_err(OperationError::from_str)?; } - ArchiveType::Zip => { - let mut archive = fs::File::create(&to) - .map(io::BufWriter::new) - .map(zip::ZipWriter::new) - .map_err(OperationError::from_str)?; - let total_paths = paths.len(); - let mut buffer = vec![0; 4 * 1024 * 1024]; - for (i, path) in paths.iter().enumerate() { - controller.check().map_err(OperationError::from_str)?; + match archive_type { + ArchiveType::Tgz => { + let mut archive = fs::File::create(&to) + .map(io::BufWriter::new) + .map(|w| { + flate2::write::GzEncoder::new( + w, + flate2::Compression::default(), + ) + }) + .map(tar::Builder::new) + .map_err(OperationError::from_str)?; - controller.set_progress((i as f32) / total_paths as f32); + let total_paths = paths.len(); + for (i, path) in paths.iter().enumerate() { + controller.check().map_err(OperationError::from_str)?; - let mut zip_options = zip::write::SimpleFileOptions::default(); - if password.is_some() { - zip_options = zip_options.with_aes_encryption(Aes256, password.as_deref().unwrap()); - } - if let Some(relative_path) = - path.strip_prefix(relative_root).map_err(OperationError::from_str)?.to_str() - { - if path.is_file() { - let mut file = fs::File::open(path).map_err(OperationError::from_str)?; - let metadata = file.metadata().map_err(OperationError::from_str)?; - let total = metadata.len(); - if total >= 4 * 1024 * 1024 * 1024 { - // The large file option must be enabled for files above 4 GiB - zip_options = zip_options.large_file(true); - } - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - let mode = metadata.mode(); - zip_options = zip_options.unix_permissions(mode); - } + controller.set_progress((i as f32) / total_paths as f32); + + if let Some(relative_path) = path + .strip_prefix(relative_root) + .map_err(OperationError::from_str)? + .to_str() + { archive - .start_file(relative_path, zip_options) - .map_err(OperationError::from_str)?; - let mut current = 0; - loop { - controller.check().map_err(OperationError::from_str)?; - - let count = file.read(&mut buffer).map_err(OperationError::from_str)?; - if count == 0 { - break; - } - archive.write_all(&buffer[..count]).map_err(OperationError::from_str)?; - current += count; - - let file_progress = current as f32 / total as f32; - let total_progress = - (i as f32 + file_progress) / total_paths as f32; - controller.set_progress(total_progress); - } - } else { - archive - .add_directory(relative_path, zip_options) + .append_path_with_name(path, relative_path) .map_err(OperationError::from_str)?; } } + + archive.finish().map_err(OperationError::from_str)?; } + ArchiveType::Zip => { + let mut archive = fs::File::create(&to) + .map(io::BufWriter::new) + .map(zip::ZipWriter::new) + .map_err(OperationError::from_str)?; - archive.finish().map_err(OperationError::from_str)?; + let total_paths = paths.len(); + let mut buffer = vec![0; 4 * 1024 * 1024]; + for (i, path) in paths.iter().enumerate() { + controller.check().map_err(OperationError::from_str)?; + + controller.set_progress((i as f32) / total_paths as f32); + + let mut zip_options = zip::write::SimpleFileOptions::default(); + if password.is_some() { + zip_options = zip_options.with_aes_encryption( + Aes256, + password.as_deref().unwrap(), + ); + } + if let Some(relative_path) = path + .strip_prefix(relative_root) + .map_err(OperationError::from_str)? + .to_str() + { + if path.is_file() { + let mut file = fs::File::open(path) + .map_err(OperationError::from_str)?; + let metadata = file + .metadata() + .map_err(OperationError::from_str)?; + let total = metadata.len(); + if total >= 4 * 1024 * 1024 * 1024 { + // The large file option must be enabled for files above 4 GiB + zip_options = zip_options.large_file(true); + } + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + let mode = metadata.mode(); + zip_options = zip_options.unix_permissions(mode); + } + archive + .start_file(relative_path, zip_options) + .map_err(OperationError::from_str)?; + let mut current = 0; + loop { + controller + .check() + .map_err(OperationError::from_str)?; + + let count = file + .read(&mut buffer) + .map_err(OperationError::from_str)?; + if count == 0 { + break; + } + archive + .write_all(&buffer[..count]) + .map_err(OperationError::from_str)?; + current += count; + + let file_progress = current as f32 / total as f32; + let total_progress = + (i as f32 + file_progress) / total_paths as f32; + controller.set_progress(total_progress); + } + } else { + archive + .add_directory(relative_path, zip_options) + .map_err(OperationError::from_str)?; + } + } + } + + archive.finish().map_err(OperationError::from_str)?; + } } - } - Ok(op_sel) - }) + Ok(op_sel) + }, + ) .await .map_err(OperationError::from_str)? //.map_err(|e| e)? @@ -830,7 +865,8 @@ impl Operation { controller.set_progress(i as f32 / count as f32); - trash::os_limited::purge_all([item]).map_err(OperationError::from_str)?; + trash::os_limited::purge_all([item]) + .map_err(OperationError::from_str)?; } Ok(()) }) @@ -839,127 +875,141 @@ impl Operation { } Ok(OperationSelection::default()) } - Self::Extract { paths, to, password } => { - tokio::task::spawn_blocking(move || -> Result { - let total_paths = paths.len(); - let mut op_sel = OperationSelection::default(); - for (i, path) in paths.iter().enumerate() { - controller.check().map_err(OperationError::from_str)?; + Self::Extract { + paths, + to, + password, + } => { + tokio::task::spawn_blocking( + move || -> Result { + let total_paths = paths.len(); + let mut op_sel = OperationSelection::default(); + for (i, path) in paths.iter().enumerate() { + controller.check().map_err(OperationError::from_str)?; - controller.set_progress((i as f32) / total_paths as f32); + controller.set_progress((i as f32) / total_paths as f32); - if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { - let dir_name = get_directory_name(file_name); - let mut new_dir = to.join(dir_name); + if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { + let dir_name = get_directory_name(file_name); + let mut new_dir = to.join(dir_name); - if new_dir.exists() { - if let Some(new_dir_parent) = new_dir.parent() { - new_dir = copy_unique_path(&new_dir, new_dir_parent); + if new_dir.exists() { + if let Some(new_dir_parent) = new_dir.parent() { + new_dir = copy_unique_path(&new_dir, new_dir_parent); + } } - } - op_sel.ignored.push(path.clone()); - op_sel.selected.push(new_dir.clone()); + op_sel.ignored.push(path.clone()); + op_sel.selected.push(new_dir.clone()); - let controller = controller.clone(); - let mime = mime_for_path(path); - let password = password.clone(); - match mime.essence_str() { - "application/gzip" | "application/x-compressed-tar" => { - OpReader::new(path, controller) + let controller = controller.clone(); + let mime = mime_for_path(path); + let password = password.clone(); + match mime.essence_str() { + "application/gzip" | "application/x-compressed-tar" => { + OpReader::new(path, controller) + .map(io::BufReader::new) + .map(flate2::read::GzDecoder::new) + .map(tar::Archive::new) + .and_then(|mut archive| archive.unpack(&new_dir)) + .map_err(OperationError::from_str)? + } + "application/x-tar" => OpReader::new(path, controller) .map(io::BufReader::new) - .map(flate2::read::GzDecoder::new) .map(tar::Archive::new) .and_then(|mut archive| archive.unpack(&new_dir)) + .map_err(OperationError::from_str)?, + "application/zip" => fs::File::open(path) + .map(io::BufReader::new) + .map(zip::ZipArchive::new) .map_err(OperationError::from_str)? - } - "application/x-tar" => OpReader::new(path, controller) - .map(io::BufReader::new) - .map(tar::Archive::new) - .and_then(|mut archive| archive.unpack(&new_dir)) - .map_err(OperationError::from_str)?, - "application/zip" => fs::File::open(path) - .map(io::BufReader::new) - .map(zip::ZipArchive::new) - .map_err(OperationError::from_str)? - .and_then(move |mut archive| { - zip_extract(&mut archive, &new_dir, controller, password) - }) - .map_err(|e| match e { - ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED) | - ZipError::InvalidPassword => { - OperationError { + .and_then(move |mut archive| { + zip_extract( + &mut archive, + &new_dir, + controller, + password, + ) + }) + .map_err(|e| match e { + ZipError::UnsupportedArchive( + ZipError::PASSWORD_REQUIRED, + ) + | ZipError::InvalidPassword => OperationError { kind: OperationErrorType::PasswordRequired, - } - }, - _ => OperationError::from_str(e) - })?, - #[cfg(feature = "bzip2")] - "application/x-bzip" | "application/x-bzip-compressed-tar" => { - OpReader::new(path, controller) - .map(io::BufReader::new) - .map(bzip2::read::BzDecoder::new) - .map(tar::Archive::new) - .and_then(|mut archive| archive.unpack(&new_dir)) - .map_err(OperationError::from_str)? + }, + _ => OperationError::from_str(e), + })?, + #[cfg(feature = "bzip2")] + "application/x-bzip" | "application/x-bzip-compressed-tar" => { + OpReader::new(path, controller) + .map(io::BufReader::new) + .map(bzip2::read::BzDecoder::new) + .map(tar::Archive::new) + .and_then(|mut archive| archive.unpack(&new_dir)) + .map_err(OperationError::from_str)? + } + #[cfg(feature = "liblzma")] + "application/x-xz" | "application/x-xz-compressed-tar" => { + OpReader::new(path, controller) + .map(io::BufReader::new) + .map(liblzma::read::XzDecoder::new) + .map(tar::Archive::new) + .and_then(|mut archive| archive.unpack(&new_dir)) + .map_err(OperationError::from_str)? + } + _ => Err(OperationError::from_str(format!( + "unsupported mime type {:?}", + mime + )))?, } - #[cfg(feature = "liblzma")] - "application/x-xz" | "application/x-xz-compressed-tar" => { - OpReader::new(path, controller) - .map(io::BufReader::new) - .map(liblzma::read::XzDecoder::new) - .map(tar::Archive::new) - .and_then(|mut archive| archive.unpack(&new_dir)) - .map_err(OperationError::from_str)? - } - _ => Err(OperationError::from_str(format!("unsupported mime type {:?}", mime)))?, } } - } - Ok(op_sel) - }) + Ok(op_sel) + }, + ) .await .map_err(OperationError::from_str)? //.map_err(OperationError::from_str)? } Self::Move { paths, to } => copy_or_move(paths, to, true, msg_tx, controller).await, - Self::NewFolder { path } => { - tokio::task::spawn_blocking(move || -> Result { + Self::NewFolder { path } => tokio::task::spawn_blocking( + move || -> Result { controller.check().map_err(OperationError::from_str)?; fs::create_dir(&path).map_err(OperationError::from_str)?; Ok(OperationSelection { ignored: Vec::new(), selected: vec![path], }) - }) - .await - .map_err(OperationError::from_str)? - } - Self::NewFile { path } => { - tokio::task::spawn_blocking(move || -> Result { + }, + ) + .await + .map_err(OperationError::from_str)?, + Self::NewFile { path } => tokio::task::spawn_blocking( + move || -> Result { controller.check().map_err(OperationError::from_str)?; fs::File::create(&path).map_err(OperationError::from_str)?; Ok(OperationSelection { ignored: Vec::new(), selected: vec![path], }) - }) - .await - .map_err(OperationError::from_str)? - } - Self::Rename { from, to } => { - tokio::task::spawn_blocking(move || -> Result { + }, + ) + .await + .map_err(OperationError::from_str)?, + Self::Rename { from, to } => tokio::task::spawn_blocking( + move || -> Result { controller.check().map_err(OperationError::from_str)?; fs::rename(&from, &to).map_err(OperationError::from_str)?; Ok(OperationSelection { ignored: vec![from], selected: vec![to], }) - }) - .await - .map_err(OperationError::from_str)? - } + }, + ) + .await + .map_err(OperationError::from_str)?, #[cfg(target_os = "macos")] Self::Restore { .. } => { // TODO: add support for macos @@ -995,7 +1045,9 @@ impl Operation { controller.check().map_err(OperationError::from_str)?; - let mut perms = fs::metadata(&path).map_err(OperationError::from_str)?.permissions(); + let mut perms = fs::metadata(&path) + .map_err(OperationError::from_str)? + .permissions(); let current_mode = perms.mode(); let new_mode = current_mode | 0o111; perms.set_mode(new_mode);