Skip to content

Commit 969d043

Browse files
committed
feat(cli): Windows filesystem support — preopen each drive as /{letter}
On Windows, FsConfig::preopens() now enumerates accessible drives (A–Z, skipping any whose std::fs::metadata call fails) and returns one preopen per drive: guest /{lowercase letter} → host {uppercase letter}:\. Unix keeps the single virtual-root preopen at /. A no_std-ish fallback remains for other platforms. Matcher also normalises backslashes to forward slashes in the compiled patterns on Windows so globset can match against Path separators that end up as / after globset's own normalisation. User-written Windows patterns should already use forward slashes (globset convention): --fs-allow 'C:/Users/alex/project/**' --fs-deny '/c/Windows/System32/**' Known caveats: - cross-drive rename/link is not handled specially; both sides must be in an allowed path. - Windows long-path / UNC forms (\\?\C:\...) are not automatically translated; cap-std handles the low-level conversion, users write regular drive-letter paths.
1 parent 13648c5 commit 969d043

2 files changed

Lines changed: 64 additions & 16 deletions

File tree

act-cli/src/config.rs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,26 +55,60 @@ impl FsConfig {
5555
}
5656
}
5757

58-
/// Preopens for this policy. Layer 1 uses a single virtual-root preopen
59-
/// at host `/` for every non-`Deny` mode — the `FsMatcher` is the only
60-
/// enforcement point. The guest sees the whole host filesystem in its
61-
/// namespace but can't actually open anything the matcher denies.
58+
/// Preopens for this policy.
6259
///
63-
/// Deny mode: no preopens at all (guest can't name anything).
60+
/// For every non-`Deny` mode, the `FsMatcher` is the only enforcement
61+
/// point — the guest sees the whole (platform-native) host filesystem in
62+
/// its namespace but can't actually open anything the matcher denies.
63+
///
64+
/// - Unix: a single virtual-root preopen `{guest: "/", host: "/"}`.
65+
/// - Windows: one preopen per accessible drive, guest `/{lowercase letter}`
66+
/// → host `{uppercase letter}:\`. Drives are enumerated by attempting
67+
/// `std::fs::metadata` on each letter A–Z; inaccessible drives are
68+
/// silently skipped.
6469
///
65-
/// This is Unix-first: preopening `/` assumes a POSIX root. Windows
66-
/// support requires preopening each drive separately and is deferred.
70+
/// Deny mode: no preopens at all (guest can't name anything).
6771
pub fn preopens(&self) -> Result<Vec<DirMount>> {
6872
match self.mode {
6973
PolicyMode::Deny => Ok(vec![]),
70-
PolicyMode::Open | PolicyMode::Allowlist => Ok(vec![DirMount {
71-
guest: "/".to_string(),
72-
host: PathBuf::from("/"),
73-
}]),
74+
PolicyMode::Open | PolicyMode::Allowlist => Ok(platform_root_preopens()),
7475
}
7576
}
7677
}
7778

79+
#[cfg(unix)]
80+
fn platform_root_preopens() -> Vec<DirMount> {
81+
vec![DirMount {
82+
guest: "/".to_string(),
83+
host: PathBuf::from("/"),
84+
}]
85+
}
86+
87+
#[cfg(windows)]
88+
fn platform_root_preopens() -> Vec<DirMount> {
89+
let mut mounts = Vec::new();
90+
for letter in b'A'..=b'Z' {
91+
let c = letter as char;
92+
let host = PathBuf::from(format!("{}:\\", c));
93+
// `metadata` trips DriveNotReady / access errors for absent drives;
94+
// treat any failure as "skip this letter".
95+
if std::fs::metadata(&host).is_ok() {
96+
let guest = format!("/{}", c.to_ascii_lowercase());
97+
mounts.push(DirMount { guest, host });
98+
}
99+
}
100+
mounts
101+
}
102+
103+
#[cfg(not(any(unix, windows)))]
104+
fn platform_root_preopens() -> Vec<DirMount> {
105+
// Fall back to a POSIX-ish root on esoteric platforms.
106+
vec![DirMount {
107+
guest: "/".to_string(),
108+
host: PathBuf::from("/"),
109+
}]
110+
}
111+
78112
/// Derived directory mount for wasmtime's `preopened_dir`.
79113
#[derive(Debug, Clone, PartialEq)]
80114
pub struct DirMount {

act-cli/src/fs_matcher.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,28 @@ fn compile_set(label: &str, patterns: &[String]) -> Result<GlobSet> {
9898
/// Expand `~` and make patterns absolute. Relative patterns are resolved
9999
/// against the current directory; patterns beginning with `~` expand against
100100
/// the home directory. `**` and other globset metacharacters are left intact.
101+
///
102+
/// On Windows, backslashes are normalised to forward slashes so the pattern
103+
/// matches the `/`-separated paths globset operates on. User-written Windows
104+
/// patterns should already use forward slashes (e.g. `C:/Users/alex/**`);
105+
/// this normalisation only catches strays introduced by path joining.
101106
fn expand_pattern(pattern: &str) -> String {
102107
let expanded = shellexpand::tilde(pattern).into_owned();
103-
if Path::new(&expanded).is_absolute() {
104-
return expanded;
108+
let absolute = if Path::new(&expanded).is_absolute() {
109+
expanded
110+
} else {
111+
match std::env::current_dir() {
112+
Ok(cwd) => cwd.join(&expanded).to_string_lossy().into_owned(),
113+
Err(_) => expanded,
114+
}
115+
};
116+
#[cfg(windows)]
117+
{
118+
absolute.replace('\\', "/")
105119
}
106-
match std::env::current_dir() {
107-
Ok(cwd) => cwd.join(&expanded).to_string_lossy().into_owned(),
108-
Err(_) => expanded,
120+
#[cfg(not(windows))]
121+
{
122+
absolute
109123
}
110124
}
111125

0 commit comments

Comments
 (0)