From 02ba29053be29149ec4ca4f73cd375f02f2cf21b Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Fri, 5 Jun 2020 23:27:57 -0500 Subject: [PATCH] fix panic problem by not converting to utf-8 all the time --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/dir.rs | 15 ++++++++++----- src/info.rs | 13 +++++++++++-- src/lib.rs | 4 +++- src/ops/list.rs | 3 ++- src/ops/put.rs | 43 +++++++++++++++++++++++++---------------- src/ops/restore.rs | 3 ++- src/utils.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 110 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eaa0ace..eae9a2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,7 @@ dependencies = [ "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", "libmount 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -156,6 +157,11 @@ dependencies = [ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "proc-macro-error" version = "0.4.11" @@ -388,6 +394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" "checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum proc-macro-error 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e7959c6467d962050d639361f7703b2051c43036d03493c36f01d440fdd3138a" "checksum proc-macro-error-attr 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e4002d9f55991d5e019fb940a90e1a95eb80c24e77cb2462dd4dc869604d543a" "checksum proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" diff --git a/Cargo.toml b/Cargo.toml index f8c2b9a..ba796ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ structopt = "0.3" thiserror = "1.0" walkdir = "2.2" xdg = "2.2" +percent-encoding = "2.1.0" diff --git a/src/dir.rs b/src/dir.rs index 4573333..f7c7c42 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -1,4 +1,6 @@ +use std::ffi::OsStr; use std::fs; +use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use walkdir::{DirEntry, WalkDir}; @@ -113,13 +115,16 @@ impl Iterator for TrashDirIter { entry }; - let name = entry.path().file_name().unwrap().to_str().unwrap(); - let deleted_path = if !name.ends_with(".trashinfo") { + let name = match entry.path().file_name() { + Some(name) => name, + None => return None, + }; + let deleted_path = if !name.as_bytes().ends_with(b".trashinfo") { return self.next(); } else { - self.0 - .join("files") - .join(name.trim_end_matches(".trashinfo")) + self.0.join("files").join(OsStr::from_bytes( + &name.as_bytes()[..name.len() - b".trashinfo".len()], + )) }; Some(TrashInfo::from_files(entry.path(), deleted_path).map_err(Error::from)) } diff --git a/src/info.rs b/src/info.rs index 0e042fe..ceeed5c 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,10 +1,14 @@ +use std::ffi::OsStr; use std::fs::File; use std::io::{self, BufRead, BufReader, Write}; +use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use chrono::{DateTime, Local, TimeZone}; +use percent_encoding::{percent_decode, }; use crate::errors::{Error, TrashInfoError}; +use crate::utils; const DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:%S"; @@ -67,7 +71,8 @@ impl TrashInfo { if let Some((key, value)) = parse_key_value(&line) { match key { "Path" => { - let value = PathBuf::from(value); + let value = percent_decode(value.as_bytes()).collect::>(); + let value = PathBuf::from(OsStr::from_bytes(&value)); path = Some(value) } "DeletionDate" => { @@ -102,7 +107,11 @@ impl TrashInfo { /// Write the current TrashInfo into a .trashinfo file. pub fn write(&self, mut out: impl Write) -> Result<(), io::Error> { writeln!(out, "[Trash Info]")?; - writeln!(out, "Path={}", self.path.to_str().unwrap())?; + writeln!( + out, + "Path={}", + utils::percent_encode(&self.path) + )?; writeln!( out, "DeletionDate={}", diff --git a/src/lib.rs b/src/lib.rs index ac6341d..96f6e4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,12 +12,14 @@ extern crate anyhow; #[macro_use] extern crate thiserror; +#[macro_use] +mod utils; + mod dir; mod errors; mod info; mod mounts; pub mod ops; -mod utils; use std::path::PathBuf; diff --git a/src/ops/list.rs b/src/ops/list.rs index 8cf2fe0..322d8c0 100644 --- a/src/ops/list.rs +++ b/src/ops/list.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use anyhow::Result; use crate::TrashDir; +use crate::utils; /// Options to pass to list #[derive(StructOpt)] @@ -30,7 +31,7 @@ pub fn list(options: ListOptions) -> Result<()> { .collect::>(); files.sort_unstable_by_key(|info| info.deletion_date); for info in files { - println!("{}\t{}", info.deletion_date, info.path.to_str().unwrap()); + println!("{}\t{}", info.deletion_date, utils::percent_encode(info.path)); } Ok(()) diff --git a/src/ops/put.rs b/src/ops/put.rs index a11cb88..8a7e982 100644 --- a/src/ops/put.rs +++ b/src/ops/put.rs @@ -7,20 +7,23 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use chrono::Local; -use crate::utils; +use crate::utils::{self}; use crate::{TrashDir, TrashInfo}; use crate::{HOME_MOUNT, MOUNTS}; #[derive(Debug, Error)] pub enum Error { - #[error("Refusing to remove directory {0} without '-r' option")] - MissingRecursiveOption(PathBuf), + // #[error("Refusing to remove directory {0} without '-r' option")] + // MissingRecursiveOption(PathBuf), #[error("Refusing to remove '.' or '..', skipping...")] CannotTrashDotDirs, #[error("Cancelled by user.")] CancelledByUser, + + #[error("Invalid filename.")] + InvalidFilename, } /// Options to pass to put @@ -153,19 +156,13 @@ impl DeletionStrategy { let target = target.as_ref(); // this will be None if target isn't a symlink - let link_info = target.read_link().ok(); - - // file is a directory - // if !link_info.is_some() && target.is_dir() && !options.recursive { - // bail!(Error::MissingRecursiveOption(target.to_path_buf())); - // } + let _link_info = target.read_link().ok(); let (trash_dir, requires_copy) = self.get_target_trash(); // prompt if not suppressed // TODO: streamline this logic better if !options.force && (requires_copy || options.prompt) { - // TODO: actually handle prompting instead of manually flushing if requires_copy { eprint!( "Removing file '{}' requires potentially expensive copying. Continue? [Y/n] ", @@ -174,6 +171,7 @@ impl DeletionStrategy { } else if options.prompt { eprint!("Remove file '{}'? [Y/n] ", target.to_str().unwrap()); } + // TODO: actually handle prompting instead of manually flushing io::stderr().flush()?; let should_continue = loop { @@ -196,14 +194,27 @@ impl DeletionStrategy { // preparing metadata let now = Local::now(); let elapsed = now.timestamp_millis(); - let file_name = format!( - "{}.{}", - elapsed, - target.file_name().unwrap().to_str().unwrap() - ); + + let elapsed_str = elapsed.to_string(); + let target_file = match target.file_name() { + Some(file) => file.to_os_string(), + None => bail!(Error::InvalidFilename), + }; + let file_name = crate::concat_os_str!(elapsed_str, ".", target_file); + // { + // let buf = vec![0; elapsed_str.len() + 1 + target_file.len()]; + // unimplemented!() + // // format!( + // // "{}.{}", + // // elapsed, + // // target.file_name().unwrap().to_str().unwrap() + // // ) + // }; let trash_file_path = trash_dir.files_dir()?.join(&file_name); - let trash_info_path = trash_dir.info_dir()?.join(file_name + ".trashinfo"); + let trash_info_path = trash_dir + .info_dir()? + .join(crate::concat_os_str!(file_name, ".trashinfo")); let trash_info = TrashInfo { path: utils::into_absolute(target)?, diff --git a/src/ops/restore.rs b/src/ops/restore.rs index 108a753..4559434 100644 --- a/src/ops/restore.rs +++ b/src/ops/restore.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use anyhow::Result; use crate::TrashDir; +use crate::utils; /// Options to pass to restore #[derive(StructOpt)] @@ -50,7 +51,7 @@ pub fn restore(options: RestoreOptions) -> Result<()> { "[{}]\t{}\t{}", i, info.deletion_date, - info.path.to_str().unwrap() + utils::percent_encode(&info.path) ); } diff --git a/src/utils.rs b/src/utils.rs index 5dc1aa4..12f78ed 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,15 @@ use std::env; +use std::ffi::{OsStr, OsString}; use std::fs; -use std::path::{Path, PathBuf}; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use anyhow::Result; +use percent_encoding::{AsciiSet}; use walkdir::WalkDir; +const MASK: &AsciiSet = percent_encoding::CONTROLS; + pub fn into_absolute(path: impl AsRef) -> Result { let path = path.as_ref(); @@ -49,3 +54,44 @@ pub fn recursive_copy(src: impl AsRef, dst: impl AsRef) -> Result<() Ok(()) } + +/// Percent-encodes a path, but only the file names, not the separators. +pub fn percent_encode(path: impl AsRef) -> String { + let path = path.as_ref(); + path.iter() + .map(|segment| { + percent_encoding::percent_encode(segment.as_bytes(), MASK).to_string() + }) + .collect::>() + .join(&MAIN_SEPARATOR.to_string()) +} + +pub(crate) fn concat_os_str_iter<'a>(ss: &Vec<&'a OsStr>) -> OsString { + let mut len = 0; + for s in ss { + len += s.len(); + } + + let mut buf = vec![0; len]; + let mut c = 0; + for s in ss { + let segment = &mut buf[c..c + s.len()]; + segment.copy_from_slice(s.as_bytes()); + c += s.len(); + } + + OsStr::from_bytes(&buf).to_os_string() +} + +/// Concatenates an arbitrary number of OsStrs +#[macro_export] +macro_rules! concat_os_str { + ($($str:expr),* $(,)?) => { + { + let _wtf = vec![ + $($str.as_ref(),)* + ]; + crate::utils::concat_os_str_iter(&_wtf) + } + }; +}