@@ -1134,8 +1134,6 @@ fn send_tauri_notification(title: &str, message: &str) {
11341134
11351135#[ cfg( unix) ]
11361136fn discover_modules ( ) -> BTreeMap < String , PathBuf > {
1137- use std:: os:: unix:: fs:: MetadataExt ;
1138-
11391137 let excluded = [
11401138 "aw-tauri" ,
11411139 "aw-client" ,
@@ -1160,14 +1158,28 @@ fn discover_modules() -> BTreeMap<String, PathBuf> {
11601158 // Create new PATH-like string
11611159 let new_paths = env:: join_paths ( paths) . unwrap_or_default ( ) ;
11621160
1163- // Build a set of paths to search
1161+ // Search starting with PATH entries
1162+ let found_modules = find_modules_in ( env:: split_paths ( & new_paths) . collect ( ) , & excluded) ;
1163+
1164+ debug ! (
1165+ "Discovered modules: {:?}" ,
1166+ found_modules. keys( ) . collect:: <Vec <_>>( )
1167+ ) ;
1168+ found_modules
1169+ }
1170+
1171+ /// Searches `dirs_to_search`, and any `aw-*` subdirectories, for executable `aw-*` modules.
1172+ #[ cfg( unix) ]
1173+ fn find_modules_in (
1174+ mut dirs_to_search : Vec < PathBuf > ,
1175+ excluded : & [ & str ] ,
1176+ ) -> BTreeMap < String , PathBuf > {
1177+ use std:: os:: unix:: fs:: MetadataExt ;
1178+
11641179 let mut found_modules = BTreeMap :: new ( ) ;
11651180 // Use (device, inode) pairs for cycle detection (works across filesystems)
11661181 let mut visited_inodes = HashSet :: new ( ) ;
11671182
1168- // Create a stack of directories to search, starting with PATH entries
1169- let mut dirs_to_search: Vec < PathBuf > = env:: split_paths ( & new_paths) . collect ( ) ;
1170-
11711183 // Process directories in depth-first order
11721184 while let Some ( dir) = dirs_to_search. pop ( ) {
11731185 // Use (device, inode) tuple to detect cycles (works across different filesystems)
@@ -1191,17 +1203,20 @@ fn discover_modules() -> BTreeMap<String, PathBuf> {
11911203 } ;
11921204 let path = entry. path ( ) ;
11931205
1194- let metadata = match entry. metadata ( ) {
1206+ // Follow symlinks: a symlink's own mode is always 0o777 on Linux, so checking it
1207+ // (as entry.metadata() would) accepted any aw-* link, even one to a
1208+ // non-executable or missing file, and never descended into linked directories.
1209+ let metadata = match fs:: metadata ( & path) {
11951210 Ok ( m) => m,
1196- Err ( _) => continue ,
1211+ Err ( _) => continue , // broken symlink or unreadable
11971212 } ;
11981213
11991214 // If it's a directory starting with "aw-", add to search stack
12001215 if metadata. is_dir ( ) {
12011216 dirs_to_search. push ( path) ;
12021217 }
12031218 // If it's an executable file
1204- else if metadata. is_file ( ) || metadata . file_type ( ) . is_symlink ( ) {
1219+ else if metadata. is_file ( ) {
12051220 // Skip if has extension or is excluded
12061221 if file_name. contains ( '.' ) || excluded. contains ( & file_name. as_str ( ) ) {
12071222 continue ;
@@ -1217,10 +1232,6 @@ fn discover_modules() -> BTreeMap<String, PathBuf> {
12171232 }
12181233 }
12191234
1220- debug ! (
1221- "Discovered modules: {:?}" ,
1222- found_modules. keys( ) . collect:: <Vec <_>>( )
1223- ) ;
12241235 found_modules
12251236}
12261237
@@ -1385,6 +1396,41 @@ mod tests {
13851396 assert ! ( output. stdout. ends_with( b"tail-marker\n " ) ) ;
13861397 }
13871398
1399+ #[ cfg( unix) ]
1400+ #[ test]
1401+ fn find_modules_follows_symlinks_to_their_targets ( ) {
1402+ use std:: os:: unix:: fs:: symlink;
1403+
1404+ let dir = env:: temp_dir ( ) . join ( format ! ( "aw-tauri-discover-test-{}" , std:: process:: id( ) ) ) ;
1405+ let _ = fs:: remove_dir_all ( & dir) ;
1406+ let bin = dir. join ( "bin" ) ;
1407+ let nested = dir. join ( "nested" ) ;
1408+ fs:: create_dir_all ( & bin) . unwrap ( ) ;
1409+ fs:: create_dir_all ( & nested) . unwrap ( ) ;
1410+
1411+ let make = |path : PathBuf , mode : u32 | {
1412+ fs:: write ( & path, "" ) . unwrap ( ) ;
1413+ fs:: set_permissions ( & path, fs:: Permissions :: from_mode ( mode) ) . unwrap ( ) ;
1414+ path
1415+ } ;
1416+ let exec = make ( dir. join ( "real-exec" ) , 0o755 ) ;
1417+ let plain = make ( dir. join ( "real-plain" ) , 0o644 ) ;
1418+ make ( nested. join ( "aw-nested" ) , 0o755 ) ;
1419+
1420+ symlink ( & exec, bin. join ( "aw-good" ) ) . unwrap ( ) ;
1421+ symlink ( & plain, bin. join ( "aw-not-executable" ) ) . unwrap ( ) ;
1422+ symlink ( dir. join ( "missing" ) , bin. join ( "aw-broken" ) ) . unwrap ( ) ;
1423+ symlink ( & nested, bin. join ( "aw-linked-dir" ) ) . unwrap ( ) ;
1424+
1425+ let found = find_modules_in ( vec ! [ bin] , & [ ] ) ;
1426+ assert_eq ! (
1427+ found. keys( ) . map( String :: as_str) . collect:: <Vec <_>>( ) ,
1428+ [ "aw-good" , "aw-nested" ]
1429+ ) ;
1430+
1431+ fs:: remove_dir_all ( & dir) . unwrap ( ) ;
1432+ }
1433+
13881434 #[ test]
13891435 fn read_tail_keeps_everything_under_the_limit ( ) {
13901436 assert_eq ! ( read_tail( & b"hello" [ ..] , 64 ) , b"hello" ) ;
0 commit comments