Skip to content

Commit

Permalink
Merge pull request #1296 from SupperZum/path_relative
Browse files Browse the repository at this point in the history
READY : Path relative
  • Loading branch information
Wandalen authored May 1, 2024
2 parents a8a634d + 20a6e1d commit 4694148
Show file tree
Hide file tree
Showing 5 changed files with 1,351 additions and 1 deletion.
383 changes: 382 additions & 1 deletion module/core/proper_path_tools/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,392 @@ pub( crate ) mod private
Ok( format!( "{}_{}_{}_{}", timestamp, pid, tid, count ) )
}

/// Finds the common directory path among a collection of paths.
///
/// Given an iterator of path strings, this function determines the common directory
/// path shared by all paths. If no common directory path exists, it returns `None`.
///
/// # Arguments
///
/// * `paths` - An iterator of path strings (`&str`).
///
/// # Returns
///
/// * `Option<String>` - The common directory path shared by all paths, if it exists.
/// If no common directory path exists, returns `None`.
///
/// # Examples
///
/// ```
/// use proper_path_tools::path::path_common;
///
/// let paths = vec![ "/a/b/c", "/a/b/d", "/a/b/e" ];
/// let common_path = path_common( paths.into_iter() );
/// assert_eq!( common_path, Some( "/a/b/".to_string() ) );
/// ```
///
pub fn path_common< 'a, I >( paths : I ) -> Option< String >
where
I : Iterator< Item = &'a str >,
{
use std::collections::HashMap;
let orig_paths : Vec< String > = paths.map( | path | path.to_string() ).collect();

if orig_paths.is_empty()
{
return None;
}

// Create a map to store directory frequencies
let mut dir_freqs : HashMap< String, usize > = HashMap::new();

let mut paths = orig_paths.clone();
// Iterate over paths to count directory frequencies
for path in paths.iter_mut()
{
path_remove_dots( path );
path_remove_double_dots( path );
// Split path into directories
let dirs : Vec< &str > = path.split( '/' ).collect();

// Iterate over directories
for i in 0..dirs.len()
{

// Construct directory path
let mut dir_path = dirs[ 0..i + 1 ].join( "/" );


// Increment frequency count
*dir_freqs.entry( dir_path.clone() ).or_insert( 0 ) += 1;

if i != dirs.len() - 1 && !dirs[ i + 1 ].is_empty()
{
dir_path.push( '/' );
*dir_freqs.entry( dir_path ).or_insert( 0 ) += 1;
}
}
}

// Find the directory with the highest frequency
let common_dir = dir_freqs
.into_iter()
.filter( | ( _, freq ) | *freq == paths.len() )
.map( | ( dir, _ ) | dir )
.max_by_key( | dir | dir.len() )
.unwrap_or_default();

let mut result = common_dir.to_string();

if result.is_empty()
{
if orig_paths.iter().any( | path | path.starts_with( '/' ) )
{
result.push( '/' );
}
else if orig_paths.iter().any( | path | path.starts_with( ".." ) )
{
result.push_str( ".." );
}
else
{
result.push( '.' );
}

}

Some( result )


}

/// Removes dot segments (".") from the given path string.
///
/// Dot segments in a path represent the current directory and can be safely removed
/// without changing the meaning of the path.
///
/// # Arguments
///
/// * `path` - A mutable reference to a string representing the path to be cleaned.
///
fn path_remove_dots( path : &mut String )
{
let mut cleaned_parts = vec![];

for part in path.split( '/' )
{
if part == "."
{
continue;
}

cleaned_parts.push( part );

}

*path = cleaned_parts.join( "/" );

}

/// Removes dot-dot segments ("..") from the given path string.
///
/// Dot-dot segments in a path represent the parent directory and can be safely resolved
/// to simplify the path.
///
/// # Arguments
///
/// * `path` - A mutable reference to a string representing the path to be cleaned.
///
fn path_remove_double_dots( path : &mut String )
{

let mut cleaned_parts: Vec< &str > = Vec::new();
let mut delete_empty_part = false;

for part in path.split( '/' )
{
if part == ".."
{
if let Some( pop ) = cleaned_parts.pop()
{
if pop.is_empty()
{
delete_empty_part = true;
}

if pop == ".."
{
cleaned_parts.push("..");
cleaned_parts.push("..");
}
}
else
{
cleaned_parts.push( ".." );
}
}
else
{
cleaned_parts.push( part );
}
}
if delete_empty_part
{
*path = format!( "/{}", cleaned_parts.join( "/" ) );
}
else
{
*path = cleaned_parts.join( "/" );
}

}


/// Rebase the file path relative to a new base path, optionally removing a common prefix.
///
/// # Arguments
///
/// * `file_path` - The original file path to rebase.
/// * `new_path` - The new base path to which the file path will be rebased.
/// * `old_path` - An optional common prefix to remove from the file path before rebasing.
///
/// # Returns
///
/// Returns the rebased file path if successful, or None if any error occurs.
///
/// # Examples
///
/// Rebase a file path to a new base path without removing any common prefix:
///
/// ```
/// use std::path::PathBuf;
///
/// let file_path = "/home/user/documents/file.txt";
/// let new_path = "/mnt/storage";
/// let rebased_path = proper_path_tools::path::rebase( file_path, new_path, None ).unwrap();
/// assert_eq!( rebased_path, PathBuf::from( "/mnt/storage/home/user/documents/file.txt" ) );
/// ```
///
/// Rebase a file path to a new base path after removing a common prefix:
///
/// ```
/// use std::path::PathBuf;
///
/// let file_path = "/home/user/documents/file.txt";
/// let new_path = "/mnt/storage";
/// let old_path = "/home/user";
/// let rebased_path = proper_path_tools::path::rebase( file_path, new_path, Some( old_path ) ).unwrap();
/// assert_eq!( rebased_path, PathBuf::from( "/mnt/storage/documents/file.txt" ) );
/// ```
///
pub fn rebase< T : AsRef< std::path::Path > >( file_path : T, new_path : T, old_path : Option< T > ) -> Option< std::path::PathBuf >
{
use std::path::Path;
use std::path::PathBuf;

let new_path = Path::new( new_path.as_ref() );
let mut main_file_path = Path::new( file_path.as_ref() );

if old_path.is_some()
{
let common = path_common( vec![ file_path.as_ref().to_str().unwrap(), old_path.unwrap().as_ref().to_str().unwrap() ].into_iter() )?;

main_file_path = match main_file_path.strip_prefix( common )
{
Ok( rel ) => rel,
Err( _ ) => return None,
};
}

let mut rebased_path = PathBuf::new();
rebased_path.push( new_path );
rebased_path.push( main_file_path.strip_prefix( "/" ).unwrap_or( main_file_path ) );

Some( normalize( rebased_path ) )
}


/// Computes the relative path from one path to another.
///
/// This function takes two paths and returns a relative path from the `from` path to the `to` path.
/// If the paths have different roots, the function returns the `to` path.
///
/// # Arguments
///
/// * `from` - The starting path.
/// * `to` - The target path.
///
/// # Returns
///
/// A `std::path::PathBuf` representing the relative path from `from` to `to`.
///
/// # Examples
///
/// ```
/// use std::path::PathBuf;
///
/// let from = "/a/b";
/// let to = "/a/c/d";
/// let relative_path = proper_path_tools::path::path_relative( from, to );
/// assert_eq!( relative_path, PathBuf::from( "../c/d" ) );
/// ```
pub fn path_relative< T : AsRef< std::path::Path > >( from : T, to : T ) -> std::path::PathBuf
{
use std::path::PathBuf;

let mut from = from.as_ref().to_string_lossy().to_string();
let mut to = to.as_ref().to_string_lossy().to_string();

from = from.replace( ':', "" );
to = to.replace( ':', "" );


if from == "./"
{
from.push_str( &to );
return PathBuf::from( from )
}

if from == "."
{
return PathBuf::from( to )
}

path_remove_double_dots( &mut from );
path_remove_double_dots( &mut to );
path_remove_dots( &mut from );
path_remove_dots( &mut to );

let mut from_parts: Vec< &str > = from.split( '/' ).collect();
let mut to_parts: Vec< &str > = to.split( '/' ).collect();


if from_parts.len() == 1 && from_parts[ 0 ].is_empty()
{
from_parts.pop();
}

if to_parts.len() == 1 && to_parts[ 0 ].is_empty()
{
to_parts.pop();
}

let mut common_prefix = 0;
for ( idx, ( f, t ) ) in from_parts.iter().zip( to_parts.iter() ).enumerate()
{
if f != t
{
break;
}
common_prefix = idx + 1;
}

let mut result = Vec::new();

// Add ".." for each directory not in common
for i in common_prefix..from_parts.len()
{
if from_parts[ common_prefix ].is_empty() ||
(
i == from_parts.len() - 1
&& from_parts[ i ].is_empty()
&& !to_parts.last().unwrap_or( &"" ).is_empty()
)
{
continue;
}

result.push( ".." );
}

// Add the remaining directories from 'to'
for part in to_parts.iter().skip( common_prefix )
{
result.push( *part );
}

// Join the parts into a string
let mut relative_path = result.join( "/" );



// If the relative path is empty or the 'to' path is the same as the 'from' path,
// set the relative path to "."
if relative_path.is_empty() || from == to
{
relative_path = ".".to_string();
}


if to.ends_with( '/' ) && !relative_path.ends_with( '/' ) && to != "/"
{
relative_path.push( '/' );
}


if from.ends_with( '/' ) && to.starts_with( '/' ) && relative_path.starts_with( ".." ) && relative_path != ".."
{
relative_path.replace_range( ..2 , "." );
}

if from.ends_with( '/' ) && to.starts_with( '/' ) && relative_path == ".."
{
relative_path = "./..".to_string();
}

PathBuf::from( relative_path )
}




}

crate::mod_interface!
{

protected use path_relative;
protected use rebase;
protected use path_common;
protected use is_glob;
protected use normalize;
protected use canonicalize;
Expand Down
Loading

0 comments on commit 4694148

Please sign in to comment.