@@ -14,7 +14,7 @@ use anyhow::{Context, Result, bail};
1414use clap:: Parser ;
1515use std:: collections:: HashMap ;
1616use std:: fs;
17- use std:: path:: { Path , PathBuf } ;
17+ use std:: path:: { Component , Path , PathBuf } ;
1818
1919/// Uninstall CLI command.
2020#[ derive( Debug , Parser ) ]
@@ -177,6 +177,13 @@ impl UninstallCli {
177177
178178 // Dry run mode - stop here
179179 if self . dry_run {
180+ if self . backup {
181+ println ! ( ) ;
182+ println ! (
183+ "Backup location: {}" ,
184+ get_backup_root( ) ?. join( "<timestamp>" ) . display( )
185+ ) ;
186+ }
180187 println ! ( "\n [DRY RUN] No files were deleted." ) ;
181188 return Ok ( ( ) ) ;
182189 }
@@ -319,8 +326,9 @@ fn collect_removal_items() -> Result<Vec<RemovalItem>> {
319326 // 1. Binary locations
320327 items. extend ( collect_binary_locations ( & home_dir) ?) ;
321328
322- // 2. Cortex home directory (~/.cortex)
323- items. extend ( collect_cortex_home_items ( & home_dir) ?) ;
329+ // 2. Cortex home directory (~/.cortex or CORTEX_HOME)
330+ let cortex_home = cortex_common:: get_cortex_home ( ) . unwrap_or_else ( || home_dir. join ( ".cortex" ) ) ;
331+ items. extend ( collect_cortex_home_items ( & cortex_home) ?) ;
324332
325333 // 3. Platform-specific locations
326334 #[ cfg( target_os = "windows" ) ]
@@ -386,10 +394,9 @@ fn collect_binary_locations(home_dir: &Path) -> Result<Vec<RemovalItem>> {
386394 Ok ( items)
387395}
388396
389- /// Collect items from the ~/.cortex directory.
390- fn collect_cortex_home_items ( home_dir : & Path ) -> Result < Vec < RemovalItem > > {
397+ /// Collect items from the Cortex home directory.
398+ fn collect_cortex_home_items ( cortex_home : & Path ) -> Result < Vec < RemovalItem > > {
391399 let mut items = Vec :: new ( ) ;
392- let cortex_home = home_dir. join ( ".cortex" ) ;
393400
394401 if !cortex_home. exists ( ) {
395402 return Ok ( items) ;
@@ -508,7 +515,7 @@ fn collect_cortex_home_items(home_dir: &Path) -> Result<Vec<RemovalItem>> {
508515 == 0
509516 {
510517 items. push ( RemovalItem {
511- path : cortex_home. clone ( ) ,
518+ path : cortex_home. to_path_buf ( ) ,
512519 description : "Cortex home directory" . to_string ( ) ,
513520 size : get_dir_size ( & cortex_home) ,
514521 requires_sudo : false ,
@@ -517,7 +524,7 @@ fn collect_cortex_home_items(home_dir: &Path) -> Result<Vec<RemovalItem>> {
517524 } else {
518525 // Add the parent directory itself at the end (to be removed after contents)
519526 items. push ( RemovalItem {
520- path : cortex_home,
527+ path : cortex_home. to_path_buf ( ) ,
521528 description : "Cortex home directory (if empty)" . to_string ( ) ,
522529 size : 0 ,
523530 requires_sudo : false ,
@@ -710,10 +717,9 @@ fn prompt_yes_no() -> Result<bool> {
710717
711718/// Create a backup of items before removal.
712719fn create_backup ( items : & [ RemovalItem ] ) -> Result < ( ) > {
713- let backup_dir = dirs:: home_dir ( )
714- . context ( "Could not determine home directory" ) ?
715- . join ( ".cortex-backup" )
716- . join ( chrono:: Local :: now ( ) . format ( "%Y%m%d_%H%M%S" ) . to_string ( ) ) ;
720+ let backup_root = get_backup_root ( ) ?;
721+ let backup_dir = backup_root. join ( chrono:: Local :: now ( ) . format ( "%Y%m%d_%H%M%S" ) . to_string ( ) ) ;
722+ let home_dir = dirs:: home_dir ( ) . unwrap_or_default ( ) ;
717723
718724 fs:: create_dir_all ( & backup_dir) ?;
719725
@@ -723,19 +729,19 @@ fn create_backup(items: &[RemovalItem]) -> Result<()> {
723729 if !item. path . exists ( ) {
724730 continue ;
725731 }
732+ if item. path == backup_root || item. path . starts_with ( & backup_root) {
733+ continue ;
734+ }
726735
727- let relative_path = item
728- . path
729- . strip_prefix ( dirs:: home_dir ( ) . unwrap_or_default ( ) )
730- . unwrap_or ( & item. path ) ;
736+ let relative_path = backup_relative_path ( & item. path , & home_dir) ;
731737 let backup_path = backup_dir. join ( relative_path) ;
732738
733739 if let Some ( parent) = backup_path. parent ( ) {
734740 fs:: create_dir_all ( parent) ?;
735741 }
736742
737743 if item. path . is_dir ( ) {
738- copy_dir_all ( & item. path , & backup_path) ?;
744+ copy_dir_all_excluding ( & item. path , & backup_path, & backup_root ) ?;
739745 } else {
740746 fs:: copy ( & item. path , & backup_path) ?;
741747 }
@@ -745,16 +751,42 @@ fn create_backup(items: &[RemovalItem]) -> Result<()> {
745751 Ok ( ( ) )
746752}
747753
748- /// Copy a directory recursively.
749- fn copy_dir_all ( src : & Path , dst : & Path ) -> Result < ( ) > {
754+ fn get_backup_root ( ) -> Result < PathBuf > {
755+ Ok ( cortex_common:: get_cortex_home ( )
756+ . context ( "Could not determine Cortex home directory" ) ?
757+ . join ( ".cortex-backup" ) )
758+ }
759+
760+ fn backup_relative_path ( path : & Path , home_dir : & Path ) -> PathBuf {
761+ if let Ok ( relative_path) = path. strip_prefix ( home_dir) {
762+ return relative_path. to_path_buf ( ) ;
763+ }
764+
765+ path. components ( )
766+ . filter_map ( |component| match component {
767+ Component :: Normal ( part) => Some ( part) ,
768+ _ => None ,
769+ } )
770+ . collect ( )
771+ }
772+
773+ fn path_contains ( path : & Path , child : & Path ) -> bool {
774+ path == child || child. starts_with ( path)
775+ }
776+
777+ /// Copy a directory recursively, skipping a path if it appears inside the source.
778+ fn copy_dir_all_excluding ( src : & Path , dst : & Path , excluded : & Path ) -> Result < ( ) > {
750779 fs:: create_dir_all ( dst) ?;
751780 for entry in fs:: read_dir ( src) ? {
752781 let entry = entry?;
753782 let src_path = entry. path ( ) ;
783+ if !excluded. as_os_str ( ) . is_empty ( ) && path_contains ( & src_path, excluded) {
784+ continue ;
785+ }
754786 let dst_path = dst. join ( entry. file_name ( ) ) ;
755787
756788 if src_path. is_dir ( ) {
757- copy_dir_all ( & src_path, & dst_path) ?;
789+ copy_dir_all_excluding ( & src_path, & dst_path, excluded ) ?;
758790 } else {
759791 fs:: copy ( & src_path, & dst_path) ?;
760792 }
@@ -926,6 +958,7 @@ fn clean_rc_file(path: &Path, patterns: &[&str]) -> Result<()> {
926958#[ cfg( test) ]
927959mod tests {
928960 use super :: * ;
961+ use serial_test:: serial;
929962
930963 #[ test]
931964 fn test_format_size ( ) {
@@ -976,4 +1009,44 @@ mod tests {
9761009 | InstallMethod :: Unknown => { }
9771010 }
9781011 }
1012+
1013+ #[ test]
1014+ #[ serial]
1015+ fn test_backup_root_uses_cortex_home ( ) {
1016+ let original = std:: env:: var_os ( "CORTEX_HOME" ) ;
1017+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1018+ let cortex_home = temp_dir. path ( ) . join ( "custom-cortex-home" ) ;
1019+
1020+ // SAFETY: This serialized test restores CORTEX_HOME before returning.
1021+ unsafe {
1022+ std:: env:: set_var ( "CORTEX_HOME" , & cortex_home) ;
1023+ }
1024+
1025+ assert_eq ! (
1026+ get_backup_root( ) . unwrap( ) ,
1027+ cortex_home. join( ".cortex-backup" )
1028+ ) ;
1029+
1030+ // SAFETY: This serialized test restores CORTEX_HOME to its original value.
1031+ unsafe {
1032+ match original {
1033+ Some ( value) => std:: env:: set_var ( "CORTEX_HOME" , value) ,
1034+ None => std:: env:: remove_var ( "CORTEX_HOME" ) ,
1035+ }
1036+ }
1037+ }
1038+
1039+ #[ test]
1040+ fn test_backup_relative_path_never_returns_absolute_path ( ) {
1041+ let relative_path = backup_relative_path (
1042+ Path :: new ( "/tmp/custom-cortex-home/config.toml" ) ,
1043+ Path :: new ( "/home/tester" ) ,
1044+ ) ;
1045+
1046+ assert ! ( !relative_path. is_absolute( ) ) ;
1047+ assert_eq ! (
1048+ relative_path,
1049+ PathBuf :: from( "tmp/custom-cortex-home/config.toml" )
1050+ ) ;
1051+ }
9791052}
0 commit comments