Skip to content

Commit 1da317b

Browse files
committed
fix(manager): follow symlinks when discovering modules
Discovery used the directory entry's own metadata, which doesn't follow symlinks. A symlink's mode is 0o777 on Linux (0o755 on macOS), so any aw-* link counted as an executable module, even one pointing at a non-executable or missing file, and symlinked aw-* directories were never searched. Stat the target instead, skipping broken links. The directory walk is split out of discover_modules() so it can be tested without PATH or the user config.
1 parent 4368f18 commit 1da317b

1 file changed

Lines changed: 59 additions & 13 deletions

File tree

‎src-tauri/src/manager.rs‎

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,8 +1134,6 @@ fn send_tauri_notification(title: &str, message: &str) {
11341134

11351135
#[cfg(unix)]
11361136
fn 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

Comments
 (0)