@@ -288,17 +288,37 @@ fn command_search_dirs() -> Vec<PathBuf> {
288288 unique
289289}
290290
291+ fn is_executable_file ( path : & Path ) -> bool {
292+ let Ok ( metadata) = path. metadata ( ) else {
293+ return false ;
294+ } ;
295+ if !metadata. is_file ( ) {
296+ return false ;
297+ }
298+
299+ #[ cfg( unix) ]
300+ {
301+ use std:: os:: unix:: fs:: PermissionsExt ;
302+ metadata. permissions ( ) . mode ( ) & 0o111 != 0
303+ }
304+
305+ #[ cfg( not( unix) ) ]
306+ {
307+ true
308+ }
309+ }
310+
291311fn resolve_workspace_command ( command : & str ) -> Option < PathBuf > {
292312 if command_looks_like_path ( command) {
293313 let path = PathBuf :: from ( command) ;
294- return path . exists ( ) . then_some ( path) ;
314+ return is_executable_file ( & path ) . then_some ( path) ;
295315 }
296316
297317 let file_name = executable_basename ( command) ;
298318 command_search_dirs ( )
299319 . into_iter ( )
300320 . map ( |dir| dir. join ( & file_name) )
301- . find ( |candidate| candidate . exists ( ) )
321+ . find ( |candidate| is_executable_file ( candidate ) )
302322}
303323
304324fn resolve_cache ( ) -> & ' static std:: sync:: Mutex < std:: collections:: HashMap < String , Option < PathBuf > > >
@@ -351,7 +371,7 @@ fn resolve_command_uncached(command: &str) -> Option<PathBuf> {
351371 }
352372
353373 for candidate in path_candidates_from_env ( command) {
354- if candidate . exists ( ) {
374+ if is_executable_file ( & candidate ) {
355375 return Some ( candidate) ;
356376 }
357377 }
@@ -361,7 +381,7 @@ fn resolve_command_uncached(command: &str) -> Option<PathBuf> {
361381 }
362382 for dir in common_binary_paths ( ) {
363383 let candidate = dir. join ( executable_basename ( command) ) ;
364- if candidate . exists ( ) {
384+ if is_executable_file ( & candidate ) {
365385 return Some ( candidate) ;
366386 }
367387 }
@@ -401,7 +421,7 @@ fn find_via_login_shell(command: &str) -> Option<PathBuf> {
401421 let stdout = run_in_login_shell ( & [ "-l" , "-c" , r#"command -v -- "$1""# , "_" , command] ) ?;
402422 let resolved = stdout. lines ( ) . rfind ( |line| !line. trim ( ) . is_empty ( ) ) ?;
403423 let path = PathBuf :: from ( resolved. trim ( ) ) ;
404- ( path. is_absolute ( ) && path . exists ( ) ) . then_some ( path)
424+ ( path. is_absolute ( ) && is_executable_file ( & path ) ) . then_some ( path)
405425}
406426
407427/// Return the user's full PATH from a login shell.
@@ -637,6 +657,34 @@ mod tests {
637657 ) ;
638658 }
639659
660+ #[ cfg( unix) ]
661+ #[ test]
662+ fn explicit_path_resolution_ignores_non_executable_files ( ) {
663+ use std:: os:: unix:: fs:: PermissionsExt ;
664+
665+ let dir =
666+ std:: env:: temp_dir ( ) . join ( format ! ( "buzz-discovery-path-{}" , uuid:: Uuid :: new_v4( ) ) ) ;
667+ std:: fs:: create_dir_all ( & dir) . expect ( "create temp dir" ) ;
668+ let bin = dir. join ( "buzz-acp" ) ;
669+ std:: fs:: write ( & bin, "" ) . expect ( "write placeholder" ) ;
670+ std:: fs:: set_permissions ( & bin, std:: fs:: Permissions :: from_mode ( 0o644 ) )
671+ . expect ( "chmod placeholder" ) ;
672+
673+ assert ! (
674+ super :: resolve_workspace_command( bin. to_str( ) . expect( "utf8 path" ) ) . is_none( ) ,
675+ "non-executable placeholder must not resolve"
676+ ) ;
677+
678+ std:: fs:: set_permissions ( & bin, std:: fs:: Permissions :: from_mode ( 0o755 ) )
679+ . expect ( "chmod executable" ) ;
680+ assert_eq ! (
681+ super :: resolve_workspace_command( bin. to_str( ) . expect( "utf8 path" ) ) ,
682+ Some ( bin. clone( ) )
683+ ) ;
684+
685+ let _ = std:: fs:: remove_dir_all ( dir) ;
686+ }
687+
640688 #[ test]
641689 fn classifies_available_when_adapter_found ( ) {
642690 let ( status, cmd, path) = classify_runtime (
0 commit comments