@@ -148,16 +148,21 @@ pub(crate) async fn entry_file_type(entry: &DirEntry) -> Option<FileType> {
148148
149149/// Resolve the user's home directory: `HOME`, then `USERPROFILE`
150150/// (Windows), then a literal `"~"` — a harmless non-existent path so
151- /// downstream joins probe nothing rather than panic. The shared
151+ /// downstream joins probe nothing rather than panic. A set-but-empty
152+ /// variable counts as unset: honoring `""` would turn every
153+ /// `home_dir().join(…)` probe into a CWD-relative path, pointing the
154+ /// crawlers at directories inside the user's project. The shared
152155/// fallback chain for every crawler that scans well-known per-user
153156/// package roots (`~/.cargo`, `~/.m2`, `~/.nuget`, …) and for
154157/// telemetry's home-dir redaction; previously copy-pasted into each.
155158/// The go/composer crawlers deliberately use a stricter
156159/// no-home-means-no-path chain instead.
157160pub ( crate ) fn home_dir ( ) -> PathBuf {
158161 let home = std:: env:: var ( "HOME" )
159- . or_else ( |_| std:: env:: var ( "USERPROFILE" ) )
160- . unwrap_or_else ( |_| "~" . to_string ( ) ) ;
162+ . ok ( )
163+ . filter ( |h| !h. is_empty ( ) )
164+ . or_else ( || std:: env:: var ( "USERPROFILE" ) . ok ( ) . filter ( |h| !h. is_empty ( ) ) )
165+ . unwrap_or_else ( || "~" . to_string ( ) ) ;
161166 PathBuf :: from ( home)
162167}
163168
@@ -474,6 +479,35 @@ mod tests {
474479 assert_eq ! ( tokio:: fs:: read( & fresh) . await . unwrap( ) , b"x" ) ;
475480 }
476481
482+ /// Regression: a set-but-empty `HOME` (stripped CI/container/sudo
483+ /// environments) must be treated as unset, exactly like the documented
484+ /// no-home fallback. Honoring `""` made `home_dir()` return an empty
485+ /// `PathBuf`, so every `home_dir().join(".cargo")`-style probe became a
486+ /// CWD-relative path and the crawlers scanned directories inside the
487+ /// user's project as if they were the per-user package roots.
488+ #[ test]
489+ #[ serial_test:: serial]
490+ fn home_dir_treats_empty_home_as_unset ( ) {
491+ let prev_home = std:: env:: var ( "HOME" ) . ok ( ) ;
492+ let prev_profile = std:: env:: var ( "USERPROFILE" ) . ok ( ) ;
493+ std:: env:: set_var ( "HOME" , "" ) ;
494+ std:: env:: set_var ( "USERPROFILE" , "" ) ;
495+ let home = home_dir ( ) ;
496+ match prev_home {
497+ Some ( v) => std:: env:: set_var ( "HOME" , v) ,
498+ None => std:: env:: remove_var ( "HOME" ) ,
499+ }
500+ match prev_profile {
501+ Some ( v) => std:: env:: set_var ( "USERPROFILE" , v) ,
502+ None => std:: env:: remove_var ( "USERPROFILE" ) ,
503+ }
504+ assert_eq ! (
505+ home,
506+ PathBuf :: from( "~" ) ,
507+ "empty HOME/USERPROFILE must fall back to the harmless `~` sentinel"
508+ ) ;
509+ }
510+
477511 /// `entry_file_type` is the symlink-aware counterpart: it reports
478512 /// the link itself (`is_symlink`), never the resolved target.
479513 #[ cfg( unix) ]
0 commit comments