use std::path::{ Path, PathBuf };
use crate::{ Error, Result };
#[inline]
pub fn encode_path( path : &Path ) -> Result< String >
{
let path_str = path
.to_str()
.ok_or_else( || Error::path_encoding
(
format!( "{}", path.display() ),
"path contains invalid UTF-8".to_string()
))?;
let components : Vec< &str > = path_str
.trim_start_matches( '/' )
.trim_end_matches( '/' )
.split( '/' )
.collect();
if components.is_empty() || ( components.len() == 1 && components[ 0 ].is_empty() )
{
return Err( Error::path_encoding
(
path_str,
"path is empty after normalization"
));
}
let mut result = String::with_capacity( path_str.len() );
result.push( '-' );
for ( i, component ) in components.iter().enumerate()
{
let component_normalized = component.replace( '_', "-" );
if i > 0
{
if let Some( stripped ) = component_normalized.strip_prefix( '-' )
{
result.push_str( "--" ); result.push_str( stripped ); }
else
{
result.push( '-' ); result.push_str( &component_normalized ); }
}
else
{
if let Some( stripped ) = component_normalized.strip_prefix( '-' )
{
result.push( '-' ); result.push_str( stripped );
}
else
{
result.push_str( &component_normalized );
}
}
}
Ok( result )
}
#[inline]
pub fn decode_path( encoded : &str ) -> Result< PathBuf >
{
if !encoded.starts_with( '-' )
{
return Err( Error::path_encoding
(
encoded,
"encoded path must start with '-'"
));
}
if encoded.len() == 1
{
return Err( Error::path_encoding
(
encoded,
"encoded path is empty after removing prefix"
));
}
Ok( decode_v1_heuristic( encoded ) )
}
fn decode_v1_heuristic( encoded : &str ) -> PathBuf
{
let path_str = &encoded[ 1.. ];
let mut result = String::with_capacity( path_str.len() + 10 );
result.push( '/' );
let mut chars = path_str.chars().peekable();
let mut current_component = String::new();
let mut in_hyphen_prefixed = false;
while let Some( ch ) = chars.next()
{
if ch == '-'
{
if chars.peek() == Some( &'-' )
{
if !current_component.is_empty()
{
if let Some( stripped ) = current_component.strip_prefix( '-' )
{
result.push( '-' );
result.push_str( &decode_component( stripped, true ) );
}
else
{
result.push_str( &decode_component( ¤t_component, in_hyphen_prefixed ) );
}
current_component.clear();
}
result.push( '/' );
result.push( '-' );
chars.next(); in_hyphen_prefixed = true;
}
else
{
current_component.push( ch );
}
}
else
{
current_component.push( ch );
}
}
if !current_component.is_empty()
{
if let Some( stripped ) = current_component.strip_prefix( '-' )
{
result.push( '-' );
result.push_str( &decode_component( stripped, true ) );
}
else
{
result.push_str( &decode_component( ¤t_component, in_hyphen_prefixed ) );
}
}
PathBuf::from( result )
}
fn decode_component( component : &str, is_hyphen_prefixed : bool ) -> String
{
if is_hyphen_prefixed
{
return component.replace( '-', "_" );
}
const PATH_COMPONENTS : &[ &str ] = &[
"home", "usr", "opt", "tmp", "var", "etc", "bin", "lib", "src",
"projects", "user", "root",
];
const PROJECT_COMPONENTS : &[ &str ] = &[
"module", "modules", "crates", "crate", "lib", "bin", "src", "tests", "examples",
];
let parts : Vec< &str > = component.split( '-' ).collect();
if parts.len() == 1
{
return component.to_string();
}
let mut result = String::new();
let module_idx = parts.iter().position( |&p| p == "module" || p == "modules" );
for ( i, part ) in parts.iter().enumerate()
{
if i > 0
{
let prev_part = parts[ i - 1 ];
let sep_char = if let Some( mod_idx ) = module_idx
{
if i == mod_idx + 1
{
'/'
}
else if i == mod_idx + 2
{
'_'
}
else if i <= mod_idx
{
let prev_is_known = PATH_COMPONENTS.contains( &prev_part )
|| PROJECT_COMPONENTS.contains( &prev_part );
let curr_is_known = PATH_COMPONENTS.contains( part )
|| PROJECT_COMPONENTS.contains( part );
if prev_is_known || curr_is_known { '/' } else { '-' }
}
else
{
'/'
}
}
else if PATH_COMPONENTS.contains( part ) || PATH_COMPONENTS.contains( &prev_part )
{
'/'
}
else if PROJECT_COMPONENTS.contains( part ) || PROJECT_COMPONENTS.contains( &prev_part )
{
'/'
}
else
{
'/'
};
result.push( sep_char );
}
result.push_str( part );
}
result
}
#[cfg( test )]
mod tests
{
use super::*;
#[test]
fn test_encode_basic_path()
{
let path = Path::new( "/home/user/project" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "-home-user-project" );
}
#[test]
fn test_encode_without_leading_slash()
{
let path = Path::new( "home/user/project" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "-home-user-project" );
}
#[test]
fn test_encode_with_trailing_slash()
{
let path = Path::new( "/home/user/project/" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "-home-user-project" );
}
#[test]
fn test_decode_basic()
{
let decoded = decode_path( "-home-user-project" ).unwrap();
assert_eq!( decoded, PathBuf::from( "/home/user/project" ) );
}
#[test]
fn test_roundtrip()
{
let original = Path::new( "/home/user/project/subdir" );
let encoded = encode_path( original ).unwrap();
let decoded = decode_path( &encoded ).unwrap();
let original_normalized = original.to_str().unwrap().trim_end_matches( '/' );
let decoded_normalized = decoded.to_str().unwrap().trim_end_matches( '/' );
assert_eq!( original_normalized, decoded_normalized );
}
#[test]
fn test_decode_missing_prefix()
{
let result = decode_path( "home-user-project" );
assert!( result.is_err() );
}
#[test]
fn test_encode_empty_path()
{
let path = Path::new( "/" );
let result = encode_path( path );
assert!( result.is_err() );
}
#[test]
fn test_decode_hyphen_prefixed_component()
{
let decoded = decode_path( "-commands--default_topic" ).unwrap();
assert_eq!( decoded, PathBuf::from( "/commands/-default_topic" ) );
}
#[test]
fn test_decode_multiple_hyphen_components()
{
let decoded = decode_path( "-foo--bar--baz" ).unwrap();
assert_eq!( decoded, PathBuf::from( "/foo/-bar/-baz" ) );
}
#[test]
fn test_decode_real_world_claude_path()
{
let decoded = decode_path( "-home-alice-projects-claude-commands--default_topic" ).unwrap();
assert_eq!( decoded, PathBuf::from( "/home/alice/projects/claude/commands/-default_topic" ) );
}
#[test]
fn test_encode_hyphen_prefixed_component()
{
let path = Path::new( "/commands/-default_topic" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "-commands--default-topic" );
}
#[test]
fn test_encode_multiple_hyphen_components()
{
let path = Path::new( "/foo/-bar/-baz" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "-foo--bar--baz" );
}
#[test]
fn test_roundtrip_hyphen_prefixed()
{
let original = Path::new( "/home/user/project/-default_topic" );
let encoded = encode_path( original ).unwrap();
let decoded = decode_path( &encoded ).unwrap();
assert_eq!( original, decoded.as_path() );
}
#[test]
fn test_roundtrip_multiple_hyphen_dirs()
{
let original = Path::new( "/commands/-default_topic/-commit/-plan" );
let encoded = encode_path( original ).unwrap();
let decoded = decode_path( &encoded ).unwrap();
assert_eq!( original, decoded.as_path() );
}
#[test]
fn test_backwards_compat_no_double_hyphen()
{
let decoded = decode_path( "-home-user-project-subdir" ).unwrap();
assert_eq!( decoded, PathBuf::from( "/home/user/project/subdir" ) );
}
#[test]
fn test_encode_nested_hyphen_path()
{
let path = Path::new( "/a/-b_c/-d_e" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "-a--b-c--d-e" );
}
#[test]
fn test_decode_nested_hyphen_path()
{
let decoded = decode_path( "-a--b-c--d-e" ).unwrap();
assert_eq!( decoded, PathBuf::from( "/a/-b_c/-d_e" ) );
}
#[test]
fn test_edge_case_single_hyphen_prefixed()
{
let path = Path::new( "/-commit" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "--commit" );
let decoded = decode_path( &encoded ).unwrap();
assert_eq!( decoded, PathBuf::from( "/-commit" ) );
}
#[test]
fn test_component_with_underscore()
{
let path = Path::new( "/commands/-default_topic" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "-commands--default-topic" );
let decoded = decode_path( &encoded ).unwrap();
assert_eq!( decoded, PathBuf::from( "/commands/-default_topic" ) );
}
#[test]
fn test_real_world_my_agent_path()
{
let path = Path::new( "/home/alice/projects/consumer-app/module/my_agent/-default_topic" );
let encoded = encode_path( path ).unwrap();
assert_eq!
(
encoded,
"-home-alice-projects-consumer-app-module-my-agent--default-topic"
);
let decoded = decode_path( &encoded ).unwrap();
assert_eq!
(
decoded,
PathBuf::from( "/home/alice/projects/consumer-app/module/my_agent/-default_topic" )
);
}
#[test]
fn test_consecutive_hyphen_dirs()
{
let path = Path::new( "/-a/-b/-c" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "--a--b--c" );
let decoded = decode_path( &encoded ).unwrap();
assert_eq!( decoded, PathBuf::from( "/-a/-b/-c" ) );
}
#[test]
fn test_mixed_normal_and_hyphen_dirs()
{
let path = Path::new( "/commands/-commit_sessions/-plan" );
let encoded = encode_path( path ).unwrap();
assert_eq!( encoded, "-commands--commit-sessions--plan" );
let decoded = decode_path( &encoded ).unwrap();
assert_eq!( decoded, PathBuf::from( "/commands/-commit_sessions/-plan" ) );
}
}