use std::ffi::OsStr;
#[cfg(test)]
mod tests;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(r#"The string {0} contains an invalid windows path character < > : " | ? * "#)]
InvalidCharacter(String),
#[error(r#"The string {0} contains forbidden ascii control character for windows path 0-31 "#)]
ForbiddenAscii(String),
#[error(r#"The path string {0} must not end with space or dot "#)]
MustNotEndWith(String),
#[error(r#"The path string {0} must not contain reserved words con, prn, aux, nul, com1-com9, lpt1-lpt9, . and .."#)]
ReservedWords(String),
#[error(r#"The parent of {0} does not exist."#)]
NoParent(String),
#[error(r#"The file_name of {0} does not exist."#)]
NoFileName(String),
#[error(r#"char_indices().nth error {0}"#)]
CharIndicesNthError(String),
#[error("I/O error: {path} {source}")]
IoError {
#[source]
source: std::io::Error,
path: String,
},
#[error("Unknown error.")]
Unknown,
}
pub type Result<T, E = Error> = core::result::Result<T, E>;
#[derive(Clone, Debug, PartialEq)]
pub struct CrossPathBuf {
cross_path: String,
}
impl CrossPathBuf {
pub fn new(str_path: &str) -> Result<Self> {
if str_path.contains("<")
|| str_path.contains(">")
|| str_path.contains(r#"""#)
|| str_path.contains("|")
|| str_path.contains("?")
|| str_path.contains("*")
{
return Err(Error::InvalidCharacter(str_path.to_string()));
}
for byte in str_path.bytes() {
match byte {
0x00..=0x1F | 0x7F => return Err(Error::ForbiddenAscii(str_path.to_string())),
_ => (),
}
}
if str_path.ends_with(" ") || str_path.ends_with(".") {
return Err(Error::MustNotEndWith(str_path.to_string()));
}
let mut cross_path = str_path.trim().replace(r#"\"#, "/");
let delimited_str_path = format!("/{}/", cross_path.trim_start_matches("/").trim_end_matches("/").to_lowercase());
if delimited_str_path.contains("/con/")
|| delimited_str_path.contains("/prn/")
|| delimited_str_path.contains("/aux/")
|| delimited_str_path.contains("/nul/")
|| delimited_str_path.contains("/com1/")
|| delimited_str_path.contains("/com2/")
|| delimited_str_path.contains("/com3/")
|| delimited_str_path.contains("/com4/")
|| delimited_str_path.contains("/com5/")
|| delimited_str_path.contains("/com6/")
|| delimited_str_path.contains("/com7/")
|| delimited_str_path.contains("/com8/")
|| delimited_str_path.contains("/com9/")
|| delimited_str_path.contains("/lpt1/")
|| delimited_str_path.contains("/lpt2/")
|| delimited_str_path.contains("/lpt3/")
|| delimited_str_path.contains("/lpt4/")
|| delimited_str_path.contains("/lpt5/")
|| delimited_str_path.contains("/lpt6/")
|| delimited_str_path.contains("/lpt7/")
|| delimited_str_path.contains("/lpt8/")
|| delimited_str_path.contains("/lpt9/")
|| delimited_str_path.contains("/./")
|| delimited_str_path.contains("/../")
{
return Err(Error::ReservedWords(str_path.to_string()));
}
let mut iter = cross_path.chars();
if let Some(first) = iter.next()
&& let Some(second) = iter.next()
&& second == ':'
{
cross_path = format!("/mnt/{}{}", first.to_lowercase(), iter.as_str());
}
if cross_path.contains(":") {
return Err(Error::InvalidCharacter(cross_path));
}
if cross_path.contains("//") {
return Err(Error::InvalidCharacter(cross_path));
}
Ok(CrossPathBuf { cross_path })
}
pub fn from_path(path: &std::path::Path) -> Result<Self> {
let str_path = path
.to_str()
.ok_or_else(|| Error::InvalidCharacter(path.to_string_lossy().to_string()))?;
Self::new(str_path)
}
pub fn to_path_buf_win(&self) -> std::path::PathBuf {
let mut win_path = self.cross_path.clone();
if win_path.starts_with("~")
&& let Some(home) = std::env::home_dir()
{
win_path = format!("{}{}", home.to_string_lossy(), win_path.trim_start_matches("~"));
}
if win_path.starts_with("/mnt/") {
win_path = win_path.trim_start_matches("/mnt/").to_string();
win_path.insert(1, ':');
}
if win_path.starts_with("/tmp") {
let tmp_dir = std::env::temp_dir();
win_path = format!("{}{}", tmp_dir.to_string_lossy(), win_path.trim_start_matches("/tmp"));
}
use std::str::FromStr;
std::path::PathBuf::from_str(&win_path).expect("PathBuf::from_str() returns Infallible error. Therefore the error cannot occur.")
}
pub fn to_path_buf_nix(&self) -> std::path::PathBuf {
let mut nix_path = self.cross_path.clone();
if nix_path.starts_with("~")
&& let Some(home) = std::env::home_dir()
{
nix_path = format!("{}{}", home.to_string_lossy(), nix_path.trim_start_matches("~"));
}
use std::str::FromStr;
std::path::PathBuf::from_str(&nix_path).expect("PathBuf::from_str() returns Infallible error. Therefore the error cannot occur.")
}
pub fn to_path_buf_current_os(&self) -> std::path::PathBuf {
if cfg!(windows) {
self.to_path_buf_win()
} else {
self.to_path_buf_nix()
}
}
pub fn as_str(&self) -> &str {
&self.cross_path
}
pub fn exists(&self) -> bool {
if cfg!(windows) {
self.to_path_buf_win().exists()
} else {
self.to_path_buf_nix().exists()
}
}
pub fn is_file(&self) -> bool {
if cfg!(windows) {
self.to_path_buf_win().is_file()
} else {
self.to_path_buf_nix().is_file()
}
}
pub fn is_dir(&self) -> bool {
if cfg!(windows) {
self.to_path_buf_win().is_dir()
} else {
self.to_path_buf_nix().is_dir()
}
}
pub fn join_relative(&self, str_path: &str) -> Result<Self> {
let second_path = CrossPathBuf::new(str_path)?;
let cross_path = format!(
"{}/{}",
self.cross_path.trim_end_matches("/"),
second_path.as_str().trim_start_matches("/")
);
Ok(CrossPathBuf { cross_path })
}
pub fn read_to_string(&self) -> Result<String> {
let content = std::fs::read_to_string(self.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
Ok(content)
}
pub fn write_str_to_file(&self, content: &str) -> Result<()> {
self.create_dir_all_for_file()?;
std::fs::write(self.to_path_buf_current_os(), content).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
Ok(())
}
pub fn write_bytes_to_file(&self, content: &[u8]) -> Result<()> {
self.create_dir_all_for_file()?;
std::fs::write(self.to_path_buf_current_os(), content).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
Ok(())
}
pub fn create_dir_all(&self) -> Result<()> {
std::fs::create_dir_all(self.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
Ok(())
}
pub fn create_dir_all_for_file(&self) -> Result<()> {
let path = self.to_path_buf_current_os();
let parent = path.parent().ok_or_else(|| Error::NoParent(self.cross_path.clone()))?;
std::fs::create_dir_all(parent).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
Ok(())
}
pub fn trim_start_slash(&self) -> Result<Self> {
let cross_path = self.cross_path.trim_start_matches('/').trim().to_string();
Ok(CrossPathBuf { cross_path })
}
pub fn trim_end_slash(&self) -> Result<Self> {
let cross_path = self.cross_path.trim_end_matches('/').trim().to_string();
Ok(CrossPathBuf { cross_path })
}
pub fn add_start_slash(&self) -> Result<Self> {
let cross_path = format!("/{}", self.cross_path.trim_start_matches('/').trim());
Ok(CrossPathBuf { cross_path })
}
pub fn add_end_slash(&self) -> Result<Self> {
let cross_path = format!("{}/", self.cross_path.trim_end_matches('/').trim());
Ok(CrossPathBuf { cross_path })
}
pub fn file_name(&self) -> Result<String> {
let file_name = self
.to_path_buf_current_os()
.file_name()
.ok_or_else(|| Error::NoFileName(self.cross_path.clone()))?
.to_string_lossy()
.to_string();
Ok(file_name)
}
pub fn extension(&self) -> Result<String> {
let _file_name = self
.to_path_buf_current_os()
.file_name()
.ok_or_else(|| Error::NoFileName(self.cross_path.clone()))?;
let file_extension = self
.to_path_buf_current_os()
.extension()
.unwrap_or_else(|| OsStr::new(""))
.to_string_lossy()
.to_string();
Ok(file_extension)
}
pub fn file_stem(&self) -> Result<String> {
let file_stem = self
.to_path_buf_current_os()
.file_stem()
.ok_or_else(|| Error::NoFileName(self.cross_path.clone()))?
.to_string_lossy()
.to_string();
Ok(file_stem)
}
pub fn parent(&self) -> Result<Self> {
CrossPathBuf::new(
&self
.to_path_buf_current_os()
.parent()
.ok_or_else(|| Error::NoParent(self.cross_path.clone()))?
.to_string_lossy(),
)
}
pub fn replace_extension(&self, extension: &str) -> Result<Self> {
let old_extension = self.extension()?;
let dot_separator = if extension.is_empty() { "" } else { "." };
let cross_path = format!(
"{}{dot_separator}{extension}",
self.cross_path.trim_end_matches(&old_extension).trim_end_matches(".")
);
CrossPathBuf::new(&cross_path)
}
pub fn add_extension(&self, extension: &str) -> Result<Self> {
let cross_path = format!("{}.{extension}", self.cross_path);
CrossPathBuf::new(&cross_path)
}
pub fn short_string(&self, max_char: u16) -> Result<String> {
fn byte_pos_from_chars(text: &str, char_pos: usize) -> Result<usize> {
Ok(text
.char_indices()
.nth(char_pos)
.ok_or_else(|| Error::NoFileName(text.to_string()))?
.0)
}
if self.cross_path.chars().count() > max_char as usize {
let half_in_char = (max_char / 2 - 2) as usize;
let pos1_in_bytes = byte_pos_from_chars(&self.cross_path, half_in_char)?;
let pos2_in_bytes = byte_pos_from_chars(&self.cross_path, self.cross_path.chars().count() - half_in_char)?;
Ok(format!(
"{}...{}",
&self.cross_path[..pos1_in_bytes],
&self.cross_path[pos2_in_bytes..]
))
} else {
Ok(self.cross_path.to_string())
}
}
pub fn decompress_tar_gz(&self, destination_folder: &CrossPathBuf) -> Result<()> {
destination_folder.create_dir_all()?;
let tar_gz = std::fs::File::open(self.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
let tar = flate2::read::GzDecoder::new(tar_gz);
let mut archive = tar::Archive::new(tar);
archive
.unpack(destination_folder.to_path_buf_current_os())
.map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
Ok(())
}
pub fn remove_file(&self) -> Result<()> {
if std::fs::exists(self.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})? {
std::fs::remove_file(self.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
}
Ok(())
}
pub fn remove_dir_all(&self) -> Result<()> {
if std::fs::exists(self.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})? {
std::fs::remove_dir_all(self.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
}
Ok(())
}
pub fn copy_file_to_file(&self, destination_file: &CrossPathBuf) -> Result<()> {
if self.to_path_buf_current_os() != destination_file.to_path_buf_current_os() {
destination_file.create_dir_all_for_file()?;
std::fs::copy(self.to_path_buf_current_os(), destination_file.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
}
Ok(())
}
pub fn rename_or_move(&self, destination_file: &CrossPathBuf) -> Result<()> {
if self.to_path_buf_current_os() != destination_file.to_path_buf_current_os() {
destination_file.create_dir_all_for_file()?;
std::fs::rename(self.to_path_buf_current_os(), destination_file.to_path_buf_current_os()).map_err(|err| Error::IoError {
source: (err),
path: (self.cross_path.clone()),
})?;
}
Ok(())
}
}
impl std::fmt::Display for CrossPathBuf {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(self.as_str(), f)
}
}
impl From<CrossPathBuf> for std::path::PathBuf {
fn from(cross_path: CrossPathBuf) -> Self {
cross_path.to_path_buf_current_os()
}
}