Add os_string_extension.rs and refactor FileNameInfo

This commit is contained in:
Leonard Steppy 2025-02-03 22:11:25 +01:00
parent c143b29a3d
commit ad710581b3
3 changed files with 174 additions and 63 deletions

View File

@ -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<String>,
pub extension: Option<String>,
pub name: OsString,
pub version: Option<OsString>,
pub extension: Option<OsString>,
}
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<PathBuf> for FileNameInfo {
fn try_from(file: PathBuf) -> Result<Self, Self::Error> {
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::<u32>().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::<u32>().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<S>(&self, file_name: S) -> bool where S: AsRef<OsStr> {
pub fn matches<S>(&self, file_name: S) -> bool
where
S: AsRef<OsStr>,
{
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");

View File

@ -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::<Vec<_>>();
Ok(denoted_files)
@ -293,20 +294,23 @@ fn main() -> Result<(), String> {
server,
actions: {
let present_file_names: Vec<OsString> = 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<u8>) -> 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<P>(path: P) -> Result<(), String>
where
P: AsRef<Path>,

View File

@ -0,0 +1,82 @@
use std::ffi::OsStr;
pub trait OsStrExtension {
fn starts_with<S>(&self, prefix: S) -> bool
where
S: AsRef<OsStr>;
fn ends_with<S>(&self, suffix: S) -> bool
where
S: AsRef<OsStr>;
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<T> OsStrExtension for T
where
T: AsRef<OsStr>,
{
fn starts_with<S>(&self, prefix: S) -> bool
where
S: AsRef<OsStr>,
{
self
.as_ref()
.as_encoded_bytes()
.starts_with(prefix.as_ref().as_encoded_bytes())
}
fn ends_with<S>(&self, suffix: S) -> bool
where
S: AsRef<OsStr>,
{
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::<Vec<_>>()
.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!