mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Make filesystem policy matching URI-native (#41001)
## Why Filesystem policies can describe paths using a convention that differs from the host running Codex. Native path comparisons can therefore mis-handle cases such as case-variant Windows paths or ambiguous encoded components. ## What changed - Resolve policy entries and special roots as `PathUri` values using the executor's path convention. - Use validated URI components for containment, overlap, and precedence, and fail closed when component boundaries are ambiguous. - Restrict relative joins to descendants and apply the same matching rules to protected metadata paths and permission-profile intersections. ## Testing Added coverage for Windows case variants, encoded and opaque paths, repeated separators, descendant joins, special roots, metadata protection, and preserved deny entries. GitOrigin-RevId: fb09d44d11df25faaac806fe00457e6f6b0d8596
This commit is contained in:
1 parent
f3741880f6
commit
2926014075
5 files changed
+640
-113
No files matched your search
@@ -347,6 +347,37 @@ impl PathUri {
|
||||
native_path_segments_start_with(&path_segments, &base_segments, convention)
|
||||
}
|
||||
|
||||
/// Returns whether the lexical subtrees rooted at these URIs overlap.
|
||||
///
|
||||
/// Returns `None` when either URI does not expose unambiguous lexical
|
||||
/// components. Equal URIs are known to overlap even when they are opaque.
|
||||
pub fn overlaps(&self, other: &Self) -> Option<bool> {
|
||||
if self == other {
|
||||
return Some(true);
|
||||
}
|
||||
self.lexical_depth()?;
|
||||
other.lexical_depth()?;
|
||||
Some(self.starts_with(other) || other.starts_with(self))
|
||||
}
|
||||
|
||||
/// Returns true for a fallback URI that losslessly stores native path bytes.
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
self.opaque_fallback_bytes().is_some()
|
||||
}
|
||||
|
||||
/// Returns the number of non-empty path segments when this URI is safe for
|
||||
/// lexical containment.
|
||||
///
|
||||
/// Opaque fallback URIs and segments containing encoded native separators
|
||||
/// return `None` because they do not expose unambiguous component boundaries.
|
||||
pub fn lexical_depth(&self) -> Option<usize> {
|
||||
if decode_bad_path_uri(&self.0).is_some() {
|
||||
return None;
|
||||
}
|
||||
let convention = self.infer_path_convention()?;
|
||||
containment_path_segments(&self.0, convention).map(|segments| segments.len())
|
||||
}
|
||||
|
||||
/// Returns the decoded relative path from `base` to this URI.
|
||||
///
|
||||
/// The result uses the separators of the inferred path convention,
|
||||
@@ -493,6 +524,27 @@ impl PathUri {
|
||||
Self::try_from(url)
|
||||
}
|
||||
|
||||
/// Lexically resolves a relative native path that remains at or below this URI.
|
||||
///
|
||||
/// Absolute, Windows root- or drive-relative, and escaping paths are rejected.
|
||||
pub fn join_descendant(&self, path: &str) -> Result<Self, PathUriParseError> {
|
||||
let descendant = self.join(path)?;
|
||||
let windows = self.infer_path_convention() == Some(PathConvention::Windows);
|
||||
if path.starts_with('/')
|
||||
|| windows
|
||||
&& (path.starts_with('\\')
|
||||
|| PathConvention::Windows
|
||||
.path_segments(path)
|
||||
.any(|segment| segment.contains(':')))
|
||||
|| !descendant.starts_with(self)
|
||||
{
|
||||
return Err(PathUriParseError::JoinPathMustBeDescendant(
|
||||
path.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(descendant)
|
||||
}
|
||||
|
||||
/// Converts this file URI to a path using the current host's path rules.
|
||||
///
|
||||
/// The URI's inferred path convention must match the current host. Conversion should succeed
|
||||
@@ -730,7 +782,13 @@ fn native_path_segments_start_with(
|
||||
convention: PathConvention,
|
||||
) -> bool {
|
||||
match convention {
|
||||
PathConvention::Posix => path_segments.starts_with(base_segments),
|
||||
PathConvention::Posix => {
|
||||
path_segments.len() >= base_segments.len()
|
||||
&& path_segments.iter().zip(base_segments).all(|(path, base)| {
|
||||
urlencoding::decode_binary(path.as_bytes())
|
||||
== urlencoding::decode_binary(base.as_bytes())
|
||||
})
|
||||
}
|
||||
PathConvention::Windows => {
|
||||
path_segments.len() >= base_segments.len()
|
||||
&& path_segments.iter().zip(base_segments).all(|(path, base)| {
|
||||
@@ -901,8 +959,8 @@ pub enum PathUriParseError {
|
||||
QueryNotAllowed,
|
||||
#[error("fragments are not allowed in path URIs")]
|
||||
FragmentNotAllowed,
|
||||
#[error("path `{0}` must be relative when joining a path URI")]
|
||||
JoinPathMustBeRelative(String),
|
||||
#[error("path `{0}` must resolve to a relative descendant when joining a path URI")]
|
||||
JoinPathMustBeDescendant(String),
|
||||
}
|
||||
|
||||
/// Path syntax used to render a [`PathUri`] as an operating-system path.
|
||||
|
||||
Reference in new issue
Block a user