diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index a766b6147e..71bf57d66d 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -24,6 +24,11 @@ use crate::protocol::NetworkAccess; use crate::protocol::SandboxPolicy; use crate::protocol::WritableRoot; +mod windows_glob; + +pub use windows_glob::WindowsDenyReadGlobScan; +pub use windows_glob::windows_deny_read_glob_scan; + const PROTECTED_METADATA_GIT_PATH_NAME: &str = ".git"; const PROTECTED_METADATA_AGENTS_PATH_NAME: &str = ".agents"; const PROTECTED_METADATA_CODEX_PATH_NAME: &str = ".codex"; diff --git a/codex-rs/protocol/src/permissions/windows_glob.rs b/codex-rs/protocol/src/permissions/windows_glob.rs new file mode 100644 index 0000000000..641caa5ff0 --- /dev/null +++ b/codex-rs/protocol/src/permissions/windows_glob.rs @@ -0,0 +1,43 @@ +//! Windows deny-glob scan bounds shared by policy validation and native ACL expansion. + +/// Literal scan root and maximum traversal depth for a Windows deny glob. +pub struct WindowsDenyReadGlobScan<'a> { + pub root: &'a str, + pub pattern_suffix: &'a str, + pub max_depth: Option, +} + +/// Plans lexical scan bounds without accessing the controller's filesystem. +pub fn windows_deny_read_glob_scan( + pattern: &str, + configured_max_depth: Option, +) -> WindowsDenyReadGlobScan<'_> { + let first_glob = pattern.find(['*', '?', '[']).unwrap_or(pattern.len()); + let literal_prefix = &pattern[..first_glob]; + let (root, pattern_suffix) = match literal_prefix.rfind(['/', '\\']) { + Some(index) => { + let drive_root = index > 0 && literal_prefix.as_bytes()[index - 1] == b':'; + let end = if index == 0 || drive_root { + index + 1 + } else { + index + }; + (&literal_prefix[..end], &pattern[index + 1..]) + } + None => (".", pattern), + }; + let components = pattern_suffix + .split(['/', '\\']) + .filter(|component| !component.is_empty()) + .collect::>(); + let max_depth = if components.contains(&"**") { + configured_max_depth + } else { + Some(configured_max_depth.map_or(components.len(), |depth| depth.min(components.len()))) + }; + WindowsDenyReadGlobScan { + root, + pattern_suffix, + max_depth, + } +} diff --git a/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs b/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs index 29604df91b..f7b39839f9 100644 --- a/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs +++ b/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs @@ -3,6 +3,7 @@ use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::ReadDenyMatcher; +use codex_protocol::permissions::windows_deny_read_glob_scan; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashSet; use std::path::PathBuf; @@ -211,39 +212,11 @@ fn glob_scan_plans( } fn glob_scan_plan(pattern: &str, configured_max_depth: Option) -> GlobScanPlan { - // Start scanning at the deepest literal directory prefix before the first - // glob metacharacter. For example, `C:\repo\**\*.env` only scans `C:\repo` - // instead of the current directory or drive root. - let first_glob = pattern - .char_indices() - .find(|(_, ch)| matches!(ch, '*' | '?' | '[')) - .map(|(index, _)| index) - .unwrap_or(pattern.len()); - let literal_prefix = &pattern[..first_glob]; - let Some(separator_index) = literal_prefix.rfind(['/', '\\']) else { - return GlobScanPlan { - root: PathBuf::from("."), - max_depth: effective_glob_scan_max_depth(pattern, configured_max_depth), - globs: vec![ripgrep_glob(pattern)], - }; - }; - let pattern_suffix = &pattern[separator_index + 1..]; - let is_drive_root_separator = separator_index > 0 - && literal_prefix - .as_bytes() - .get(separator_index - 1) - .is_some_and(|ch| *ch == b':'); - if separator_index == 0 || is_drive_root_separator { - return GlobScanPlan { - root: PathBuf::from(&literal_prefix[..=separator_index]), - max_depth: effective_glob_scan_max_depth(pattern_suffix, configured_max_depth), - globs: vec![ripgrep_glob(pattern_suffix)], - }; - } + let scan = windows_deny_read_glob_scan(pattern, configured_max_depth); GlobScanPlan { - root: PathBuf::from(literal_prefix[..separator_index].to_string()), - max_depth: effective_glob_scan_max_depth(pattern_suffix, configured_max_depth), - globs: vec![ripgrep_glob(pattern_suffix)], + root: PathBuf::from(scan.root), + max_depth: scan.max_depth, + globs: vec![ripgrep_glob(scan.pattern_suffix)], } } @@ -285,22 +258,6 @@ fn ripgrep_glob(pattern: &str) -> String { } } -fn effective_glob_scan_max_depth( - pattern_suffix: &str, - configured_max_depth: Option, -) -> Option { - let components = pattern_suffix - .split(['/', '\\']) - .filter(|component| !component.is_empty()) - .collect::>(); - if components.contains(&"**") { - return configured_max_depth; - } - Some(configured_max_depth.map_or(components.len(), |max_depth| { - max_depth.min(components.len()) - })) -} - #[cfg(test)] #[path = "deny_read_resolver_access_tests.rs"] mod access_tests;