diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d2ac2..5bf69cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ Released on ?? - **Enhancements** - Write exitcode to log when termscp terminates +- Bugfix: + - [Issue 104](https://github.com/veeso/termscp/issues/104): Fixed termscp panics when displaying long non-ascii filenames ## 0.8.1 diff --git a/src/explorer/formatter.rs b/src/explorer/formatter.rs index 24f136b..abfd6e0 100644 --- a/src/explorer/formatter.rs +++ b/src/explorer/formatter.rs @@ -28,12 +28,14 @@ // Locals use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time}; use crate::utils::path::diff_paths; +use crate::utils::string::secure_substring; // Ext use bytesize::ByteSize; use regex::Regex; use remotefs::File; use std::path::PathBuf; use std::time::UNIX_EPOCH; +use unicode_width::UnicodeWidthStr; #[cfg(target_family = "unix")] use users::{get_group_by_gid, get_user_by_uid}; // Types @@ -303,9 +305,9 @@ impl Formatter { true => file_len - 2, false => file_len - 1, }; - let mut name: String = match name.len() >= file_len { + let mut name: String = match name.width() >= file_len { false => name, - true => format!("{}…", &name[0..last_idx]), + true => format!("{}…", secure_substring(&name, 0, last_idx)), }; if fsentry.is_dir() { name.push('/'); @@ -916,6 +918,113 @@ mod tests { assert_eq!(formatter.fmt(&entry).as_str(), "File path: c/bar.txt"); } + #[test] + #[cfg(target_family = "unix")] + fn should_fmt_utf8_path() { + let t: SystemTime = SystemTime::now(); + let entry = File { + path: PathBuf::from("/tmp/a/b/c/россия"), + metadata: Metadata { + accessed: Some(t), + created: Some(t), + modified: Some(t), + file_type: FileType::Symlink, + size: 8192, + symlink: Some(PathBuf::from("project.info")), + uid: None, + gid: None, + mode: Some(UnixPex::from(0o644)), + }, + }; + let formatter: Formatter = Formatter::new("File path: {PATH}"); + assert_eq!( + formatter.fmt(&entry).as_str(), + "File path: /tmp/a/b/c/россия" + ); + let formatter: Formatter = Formatter::new("File path: {PATH:8}"); + assert_eq!(formatter.fmt(&entry).as_str(), "File path: /tmp/…/c/россия"); + } + + #[test] + fn should_fmt_short_ascii_name() { + let entry = File { + path: PathBuf::from("/tmp/foo.txt"), + metadata: Metadata { + accessed: None, + created: None, + modified: None, + file_type: FileType::File, + size: 8192, + symlink: None, + uid: None, + gid: None, + mode: None, + }, + }; + let formatter: Formatter = Formatter::new("{NAME:8}"); + assert_eq!(formatter.fmt(&entry).as_str(), "foo.txt "); + } + + #[test] + fn should_fmt_exceeding_length_ascii_name() { + let entry = File { + path: PathBuf::from("/tmp/christian-visintin.txt"), + metadata: Metadata { + accessed: None, + created: None, + modified: None, + file_type: FileType::File, + size: 8192, + symlink: None, + uid: None, + gid: None, + mode: None, + }, + }; + let formatter: Formatter = Formatter::new("{NAME:8}"); + assert_eq!(formatter.fmt(&entry).as_str(), "christi…"); + } + + #[test] + fn should_fmt_short_utf8_name() { + let entry = File { + path: PathBuf::from("/tmp/россия"), + metadata: Metadata { + accessed: None, + created: None, + modified: None, + file_type: FileType::File, + size: 8192, + symlink: None, + uid: None, + gid: None, + mode: None, + }, + }; + let formatter: Formatter = Formatter::new("{NAME:8}"); + assert_eq!(formatter.fmt(&entry).as_str(), "россия "); + } + + #[test] + fn should_fmt_long_utf8_name() { + let entry = File { + path: PathBuf::from("/tmp/喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵"), + metadata: Metadata { + accessed: None, + created: None, + modified: None, + file_type: FileType::File, + size: 8192, + symlink: None, + uid: None, + gid: None, + mode: None, + }, + }; + let formatter: Formatter = Formatter::new("{NAME:8}"); + assert_eq!(formatter.fmt(&entry).as_str(), "喵喵喵喵喵喵喵…"); + } + /// Dummy formatter, just yelds an 'A' at the end of the current string fn dummy_fmt( _fmt: &Formatter, diff --git a/src/utils/fmt.rs b/src/utils/fmt.rs index 637e4a3..a34cd22 100644 --- a/src/utils/fmt.rs +++ b/src/utils/fmt.rs @@ -31,6 +31,7 @@ use chrono::prelude::*; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; use tuirealm::tui::style::Color; +use unicode_width::UnicodeWidthStr; /// ### fmt_pex /// @@ -85,7 +86,7 @@ pub fn fmt_path_elide(p: &Path, width: usize) -> String { /// This function allows to specify an extra length to consider to elide path pub fn fmt_path_elide_ex(p: &Path, width: usize, extra_len: usize) -> String { let fmt_path: String = format!("{}", p.display()); - match fmt_path.len() + extra_len > width as usize { + match fmt_path.width() + extra_len > width as usize { false => fmt_path, true => { // Elide diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f12692e..910c5c7 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -32,6 +32,7 @@ pub mod fmt; pub mod parser; pub mod path; pub mod random; +pub mod string; pub mod ui; #[cfg(test)] diff --git a/src/utils/string.rs b/src/utils/string.rs new file mode 100644 index 0000000..bf15277 --- /dev/null +++ b/src/utils/string.rs @@ -0,0 +1,45 @@ +//! # String +//! +//! String related utilities + +/** + * MIT License + * + * termscp - Copyright (c) 2022 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/// Get a substring considering utf8 characters +pub fn secure_substring(string: &str, start: usize, end: usize) -> String { + assert!(end >= start); + string.chars().take(end).skip(start).collect() +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn should_get_secure_substring() { + assert_eq!(secure_substring("christian", 2, 5).as_str(), "ris"); + assert_eq!(secure_substring("россия", 3, 5).as_str(), "си"); + } +}