From ad710581b3698d1b2634a9098635bc6d9d05ea49 Mon Sep 17 00:00:00 2001 From: Steppy Date: Mon, 3 Feb 2025 22:11:25 +0100 Subject: [PATCH] Add os_string_extension.rs and refactor FileNameInfo --- src/file.rs | 87 ++++++++++++++++++++------------------ src/main.rs | 68 +++++++++++++++++++---------- src/os_string_extension.rs | 82 +++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 63 deletions(-) create mode 100644 src/os_string_extension.rs diff --git a/src/file.rs b/src/file.rs index 6de8cd1..4fbc969 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,20 +1,32 @@ +use crate::os_string_extension::OsStrExtension; +use crate::osf; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::fmt::{Display, Formatter}; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; -//TODO this whole structure should probably use OsString instead of String #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct FileNameInfo { - pub name: String, - pub version: Option, - pub extension: Option, + pub name: OsString, + pub version: Option, + pub extension: Option, } impl FileNameInfo { - pub fn to_full_file_name(&self) -> String { - self.to_string() + pub fn to_full_file_name(&self) -> OsString { + (osf!(&self.name) + + self + .version + .as_ref() + .map(|v| osf!("-") + v) + .unwrap_or_default() + + self + .extension + .as_ref() + .map(|extension| osf!(".") + extension) + .unwrap_or_default()) + .build() } } @@ -23,25 +35,27 @@ impl TryFrom for FileNameInfo { fn try_from(file: PathBuf) -> Result { let file_name = file.file_name().ok_or(FileInfoError::NotAFile)?; - let file_name = file_name - .to_str() - .ok_or(FileInfoError::InvalidCharactersInFileName)?; - let (file_name_without_version, extension) = - match file_name.rsplit_once('.').filter(|(_, ending)| { - ending.parse::().is_err() //there are usually no file extensions which are just a number, but rather versions + let (file_name_without_extension, extension) = + match file_name.rsplit_once(b'.').filter(|(_, ending)| { + //there are usually no file extensions which are just a number, but rather versions + // -> don't use split if ending is number + match ending.to_str() { + Some(ending) if ending.parse::().is_ok() => false, + _ => true, + } }) { - Some((name, ending)) => (name, Some(ending.to_string())), + Some((name, ending)) => (name, Some(ending.to_os_string())), None => (file_name, None), }; - let (name, version) = match file_name_without_version.split_once('-') { - Some((name, version)) => (name, Some(version.to_string())), - None => (file_name_without_version, None), + let (name, version) = match file_name_without_extension.split_once(b'-') { + Some((name, version)) => (name, Some(version.to_os_string())), + None => (file_name_without_extension, None), }; Ok(Self { - name: name.to_string(), + name: name.to_os_string(), version, extension, }) @@ -52,18 +66,8 @@ impl Display for FileNameInfo { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "{}{}{}", - self.name, - self - .version - .as_ref() - .map(|v| format!("-{v}")) - .unwrap_or_default(), - self - .extension - .as_ref() - .map(|extension| format!(".{extension}")) - .unwrap_or_default() + "{}", + self.to_full_file_name().to_string_lossy() ) } } @@ -111,8 +115,11 @@ impl FileMatcher { ..self } } - - pub fn matches(&self, file_name: S) -> bool where S: AsRef { + + pub fn matches(&self, file_name: S) -> bool + where + S: AsRef, + { let file_name = file_name.as_ref(); file_name.as_bytes().starts_with(self.name.as_bytes()) && self @@ -131,9 +138,9 @@ mod test_file_name_info { fn test_from_plugin() { assert_eq!( FileNameInfo { - name: "TestPlugin".to_string(), - version: Some("1.0.0".to_string()), - extension: Some("jar".to_string()), + name: "TestPlugin".into(), + version: Some("1.0.0".into()), + extension: Some("jar".into()), }, FileNameInfo::try_from(PathBuf::from("test-ressources/files/TestPlugin-1.0.0.jar")) .expect("valid file") @@ -144,9 +151,9 @@ mod test_file_name_info { fn test_from_unversioned() { assert_eq!( FileNameInfo { - name: "unversioned".to_string(), + name: "unversioned".into(), version: None, - extension: Some("jar".to_string()), + extension: Some("jar".into()), }, FileNameInfo::try_from(PathBuf::from("test-ressources/files/unversioned.jar")) .expect("valid file") @@ -157,8 +164,8 @@ mod test_file_name_info { fn test_from_versioned_bin() { assert_eq!( FileNameInfo { - name: "bin".to_string(), - version: Some("7.3".to_string()), + name: "bin".into(), + version: Some("7.3".into()), extension: None, }, FileNameInfo::try_from(PathBuf::from("test-ressources/files/bin-7.3")).expect("valid file") @@ -169,7 +176,7 @@ mod test_file_name_info { fn test_from_unversioned_bin() { assert_eq!( FileNameInfo { - name: "test".to_string(), + name: "test".into(), version: None, extension: None, }, @@ -189,7 +196,7 @@ mod test_file_matcher { assert!(matcher.matches("test-1.3.0.jar")); assert!(!matcher.matches("test")); } - + #[test] fn test_match_without_extension() { let matcher = FileMatcher::from("test"); diff --git a/src/main.rs b/src/main.rs index 75fe835..154de71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod command; mod file; mod logger; mod os_string_builder; +mod os_string_extension; mod server; use crate::action::{Action, FileAction, ServerActions}; @@ -10,16 +11,16 @@ use crate::command::{CommandSpecificError, ExecutionError, LogRunnable}; use crate::file::{FileMatcher, FileNameInfo}; use crate::logger::{LogLevel, Logger}; use crate::os_string_builder::ReplaceWithOsStr; +use crate::os_string_extension::OsStrExtension; use crate::server::{RelativeLocalPathAnker, ServerAddress}; use clap::{Parser, Subcommand, ValueEnum}; use lazy_regex::{lazy_regex, Lazy, Regex}; use server::{Server, ServerReference}; use std::cell::LazyCell; -use std::ffi::{OsStr, OsString}; +use std::ffi::OsString; use std::hash::Hash; use std::io::Write; use std::iter::once; -use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::{env, fs, io}; @@ -232,11 +233,11 @@ fn main() -> Result<(), String> { ))?; } - let denoted_files = output - .stdout - .split(|&b| b == b'\n') //split at line breaks + let denoted_files = osstring_from_ssh_output(output.stdout) + .split(b'\n') //split at line breaks + .into_iter() .filter(|bytes| !bytes.is_empty()) //needed since realpath sometimes gives us empty lines - .map(|bytes| PathBuf::from(OsStr::from_bytes(bytes))) + .map(PathBuf::from) .collect::>(); Ok(denoted_files) @@ -293,20 +294,23 @@ fn main() -> Result<(), String> { server, actions: { let present_file_names: Vec = match &server.address { - ServerAddress::Ssh { ssh_address } => ShellCmd::new("ls") - .arg(ssh_address) - .arg(osf!("ls ") + &working_directory) - .collect_output() - .map_err(|e| { - format!( - "Failed to query present files on server {}: {e}", - server.get_name() - ) - })? - .stdout - .split(|&b| b == b'\n') - .map(|bytes| OsStr::from_bytes(bytes).to_os_string()) - .collect(), + ServerAddress::Ssh { ssh_address } => osstring_from_ssh_output( + ShellCmd::new("ls") + .arg(ssh_address) + .arg(osf!("ls ") + &working_directory) + .collect_output() + .map_err(|e| { + format!( + "Failed to query present files on server {}: {e}", + server.get_name() + ) + })? + .stdout, + ) + .split(b'\n') + .into_iter() + .map(OsString::from) + .collect(), ServerAddress::Localhost => fs::read_dir(&working_directory) .map_err(|e| format!("Failed to get files in working directory: {e}"))? .map(|entry| entry.map_err(|e| format!("Failed to access directory entry: {e}"))) @@ -325,13 +329,17 @@ fn main() -> Result<(), String> { file_details .iter() .flat_map(|(file, file_name_info)| { - let mut file_matcher = - FileMatcher::from(file_name.as_ref().unwrap_or(&file_name_info.name)); + let mut file_matcher = FileMatcher::from( + file_name + .as_ref() + .map(OsString::from) + .unwrap_or(file_name_info.name.to_os_string()), + ); if let Some(extension) = file_name_info.extension.as_ref() { file_matcher = file_matcher.and_extension(extension); } - let file_name = OsString::from(file_name_info.to_full_file_name()); + let file_name = file_name_info.to_full_file_name(); let add_action = FileAction::new(file, Action::Add).expect("path points to file"); @@ -616,6 +624,20 @@ fn main() -> Result<(), String> { Ok(()) } +fn osstring_from_ssh_output(output: Vec) -> OsString { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + OsString::from_vec(output) + } + + #[cfg(windows)] + { + use std::os::windows::ffi::OsStringExt; + OsString::from_wide(output.iter().map(|&b| b as u16).collect()) + } +} + fn check_local_file_exists

(path: P) -> Result<(), String> where P: AsRef, diff --git a/src/os_string_extension.rs b/src/os_string_extension.rs new file mode 100644 index 0000000..ae1e907 --- /dev/null +++ b/src/os_string_extension.rs @@ -0,0 +1,82 @@ +use std::ffi::OsStr; + +pub trait OsStrExtension { + fn starts_with(&self, prefix: S) -> bool + where + S: AsRef; + fn ends_with(&self, suffix: S) -> bool + where + S: AsRef; + fn split(&self, separator: u8) -> Vec<&OsStr>; + fn splitn(&self, n: usize, separator: u8) -> Vec<&OsStr>; + fn split_once(&self, separator: u8) -> Option<(&OsStr, &OsStr)>; + fn rsplitn(&self, n: usize, separator: u8) -> Vec<&OsStr>; + fn rsplit_once(&self, separator: u8) -> Option<(&OsStr, &OsStr)>; +} + +impl OsStrExtension for T +where + T: AsRef, +{ + fn starts_with(&self, prefix: S) -> bool + where + S: AsRef, + { + self + .as_ref() + .as_encoded_bytes() + .starts_with(prefix.as_ref().as_encoded_bytes()) + } + + fn ends_with(&self, suffix: S) -> bool + where + S: AsRef, + { + self + .as_ref() + .as_encoded_bytes() + .ends_with(suffix.as_ref().as_encoded_bytes()) + } + + fn split(&self, separator: u8) -> Vec<&OsStr> { + self + .as_ref() + .as_encoded_bytes() + .split(|&b| b == separator) + .map(|bytes| unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }) + .collect() + } + + fn splitn(&self, n: usize, separator: u8) -> Vec<&OsStr> { + self + .as_ref() + .as_encoded_bytes() + .splitn(n, |&b| b == separator) + .map(|bytes| unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }) + .collect() + } + + fn split_once(&self, separator: u8) -> Option<(&OsStr, &OsStr)> { + let mut iter = self.splitn(2, separator).into_iter(); + Some((iter.next()?, iter.next()?)) + } + + fn rsplitn(&self, n: usize, separator: u8) -> Vec<&OsStr> { + self + .as_ref() + .as_encoded_bytes() + .rsplitn(n, |&b| b == separator) + .map(|bytes| unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }) + .collect::>() + .into_iter() + .rev() + .collect() + } + + fn rsplit_once(&self, separator: u8) -> Option<(&OsStr, &OsStr)> { + let mut iter = self.rsplitn(2, separator).into_iter(); + Some((iter.next()?, iter.next()?)) + } +} + +//TODO unit test! \ No newline at end of file