mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
feat(exec-server): add filesystem find-up RPC
(cherry picked from commit d94f2e75d55ecc217f99783118fec769ad19048f)
This commit is contained in:
@@ -2,6 +2,10 @@ use std::collections::HashMap;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
pub use codex_file_system::FindUpErrorPolicy;
|
||||
pub use codex_file_system::FindUpMatchKind;
|
||||
pub use codex_file_system::FindUpOptions;
|
||||
pub use codex_file_system::FindUpOutcome;
|
||||
pub use codex_file_system::WalkOptions;
|
||||
pub use codex_file_system::WalkOutcome;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
@@ -33,6 +37,7 @@ pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile";
|
||||
pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory";
|
||||
pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata";
|
||||
pub const FS_GET_METADATA_BATCH_METHOD: &str = "fs/getMetadataBatch";
|
||||
pub const FS_FIND_UP_METHOD: &str = "fs/findUp";
|
||||
pub const FS_CANONICALIZE_METHOD: &str = "fs/canonicalize";
|
||||
pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory";
|
||||
pub const FS_WALK_METHOD: &str = "fs/walk";
|
||||
@@ -370,6 +375,17 @@ pub struct FsGetMetadataBatchResponse {
|
||||
pub results: Vec<FsGetMetadataBatchResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsFindUpParams {
|
||||
pub start: PathUri,
|
||||
#[serde(flatten)]
|
||||
pub options: FindUpOptions,
|
||||
pub sandbox: Option<FileSystemSandboxContext>,
|
||||
}
|
||||
|
||||
pub type FsFindUpResponse = FindUpOutcome;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsCanonicalizeParams {
|
||||
@@ -599,6 +615,9 @@ mod tests {
|
||||
use super::EnvironmentInfo;
|
||||
use super::ExecExitedNotification;
|
||||
use super::ExecParams;
|
||||
use super::FindUpErrorPolicy;
|
||||
use super::FindUpMatchKind;
|
||||
use super::FsFindUpParams;
|
||||
use super::FsGetMetadataBatchResponse;
|
||||
use super::FsReadFileParams;
|
||||
use super::HttpRequestParams;
|
||||
@@ -703,6 +722,14 @@ mod tests {
|
||||
"sandbox": null,
|
||||
}))
|
||||
.expect_err("native absolute path should not deserialize as a URI");
|
||||
serde_json::from_value::<FsFindUpParams>(serde_json::json!({
|
||||
"start": native_path.to_string_lossy(),
|
||||
"candidateRelativePaths": ["marker"],
|
||||
"matchKind": FindUpMatchKind::Any,
|
||||
"nonNotFoundErrorPolicy": FindUpErrorPolicy::Propagate,
|
||||
"sandbox": null,
|
||||
}))
|
||||
.expect_err("native absolute find-up start should not deserialize as a URI");
|
||||
|
||||
let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
|
||||
PermissionProfile::default(),
|
||||
|
||||
@@ -53,6 +53,7 @@ use crate::protocol::FS_CANONICALIZE_METHOD;
|
||||
use crate::protocol::FS_CLOSE_METHOD;
|
||||
use crate::protocol::FS_COPY_METHOD;
|
||||
use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
|
||||
use crate::protocol::FS_FIND_UP_METHOD;
|
||||
use crate::protocol::FS_GET_METADATA_BATCH_METHOD;
|
||||
use crate::protocol::FS_GET_METADATA_METHOD;
|
||||
use crate::protocol::FS_OPEN_METHOD;
|
||||
@@ -70,6 +71,8 @@ use crate::protocol::FsCopyParams;
|
||||
use crate::protocol::FsCopyResponse;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
use crate::protocol::FsCreateDirectoryResponse;
|
||||
use crate::protocol::FsFindUpParams;
|
||||
use crate::protocol::FsFindUpResponse;
|
||||
use crate::protocol::FsGetMetadataBatchParams;
|
||||
use crate::protocol::FsGetMetadataBatchResponse;
|
||||
use crate::protocol::FsGetMetadataParams;
|
||||
@@ -716,6 +719,13 @@ impl ExecServerClient {
|
||||
self.call(FS_GET_METADATA_BATCH_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_find_up(
|
||||
&self,
|
||||
params: FsFindUpParams,
|
||||
) -> Result<FsFindUpResponse, ExecServerError> {
|
||||
self.call(FS_FIND_UP_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_canonicalize(
|
||||
&self,
|
||||
params: FsCanonicalizeParams,
|
||||
|
||||
@@ -53,6 +53,14 @@ pub use codex_file_system::FileMetadata;
|
||||
pub use codex_file_system::FileSystemReadStream;
|
||||
pub use codex_file_system::FileSystemResult;
|
||||
pub use codex_file_system::FileSystemSandboxContext;
|
||||
pub use codex_file_system::FindUpErrorPolicy;
|
||||
pub use codex_file_system::FindUpMatch;
|
||||
pub use codex_file_system::FindUpMatchKind;
|
||||
pub use codex_file_system::FindUpOptions;
|
||||
pub use codex_file_system::FindUpOutcome;
|
||||
pub use codex_file_system::MAX_FIND_UP_CANDIDATE_BYTES;
|
||||
pub use codex_file_system::MAX_FIND_UP_CANDIDATES;
|
||||
pub use codex_file_system::MAX_FIND_UP_TOTAL_CANDIDATE_BYTES;
|
||||
pub use codex_file_system::ReadDirectoryEntry;
|
||||
pub use codex_file_system::RemoveOptions;
|
||||
pub use codex_file_system::WalkEntry;
|
||||
@@ -110,6 +118,8 @@ pub use protocol::FsCopyParams;
|
||||
pub use protocol::FsCopyResponse;
|
||||
pub use protocol::FsCreateDirectoryParams;
|
||||
pub use protocol::FsCreateDirectoryResponse;
|
||||
pub use protocol::FsFindUpParams;
|
||||
pub use protocol::FsFindUpResponse;
|
||||
pub use protocol::FsGetMetadataBatchParams;
|
||||
pub use protocol::FsGetMetadataBatchResponse;
|
||||
pub use protocol::FsGetMetadataBatchResult;
|
||||
|
||||
@@ -13,6 +13,8 @@ use crate::FileMetadata;
|
||||
use crate::FileSystemReadStream;
|
||||
use crate::FileSystemResult;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::FindUpOptions;
|
||||
use crate::FindUpOutcome;
|
||||
use crate::ReadDirectoryEntry;
|
||||
use crate::RemoveOptions;
|
||||
use crate::WalkOptions;
|
||||
@@ -21,6 +23,7 @@ use crate::client::LazyRemoteExecServerClient;
|
||||
use crate::protocol::FsCanonicalizeParams;
|
||||
use crate::protocol::FsCopyParams;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
use crate::protocol::FsFindUpParams;
|
||||
use crate::protocol::FsGetMetadataBatchParams;
|
||||
use crate::protocol::FsGetMetadataBatchResult;
|
||||
use crate::protocol::FsGetMetadataParams;
|
||||
@@ -211,6 +214,34 @@ impl RemoteFileSystem {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_up(
|
||||
&self,
|
||||
start: &PathUri,
|
||||
options: &FindUpOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FindUpOutcome> {
|
||||
trace!("remote fs find_up");
|
||||
let client = self.client.get().await.map_err(map_remote_error)?;
|
||||
match client
|
||||
.fs_find_up(FsFindUpParams {
|
||||
start: start.clone(),
|
||||
options: options.clone(),
|
||||
sandbox: remote_sandbox_context(sandbox),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(response) => Ok(response),
|
||||
Err(ExecServerError::Server {
|
||||
code: METHOD_NOT_FOUND_ERROR_CODE,
|
||||
..
|
||||
}) => {
|
||||
<Self as ExecutorFileSystem>::find_up_via_metadata(self, start, options, sandbox)
|
||||
.await
|
||||
}
|
||||
Err(error) => Err(map_remote_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &PathUri,
|
||||
@@ -370,6 +401,15 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
Box::pin(RemoteFileSystem::get_metadata_batch(self, paths, sandbox))
|
||||
}
|
||||
|
||||
fn find_up<'a>(
|
||||
&'a self,
|
||||
start: &'a PathUri,
|
||||
options: &'a FindUpOptions,
|
||||
sandbox: Option<&'a FileSystemSandboxContext>,
|
||||
) -> ExecutorFileSystemFuture<'a, FindUpOutcome> {
|
||||
Box::pin(RemoteFileSystem::find_up(self, start, options, sandbox))
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a PathUri,
|
||||
|
||||
@@ -21,6 +21,8 @@ use crate::protocol::FsCopyParams;
|
||||
use crate::protocol::FsCopyResponse;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
use crate::protocol::FsCreateDirectoryResponse;
|
||||
use crate::protocol::FsFindUpParams;
|
||||
use crate::protocol::FsFindUpResponse;
|
||||
use crate::protocol::FsGetMetadataBatchParams;
|
||||
use crate::protocol::FsGetMetadataBatchResponse;
|
||||
use crate::protocol::FsGetMetadataBatchResult;
|
||||
@@ -205,6 +207,16 @@ impl FileSystemHandler {
|
||||
Ok(FsGetMetadataBatchResponse { results })
|
||||
}
|
||||
|
||||
pub(crate) async fn find_up(
|
||||
&self,
|
||||
params: FsFindUpParams,
|
||||
) -> Result<FsFindUpResponse, JSONRPCErrorError> {
|
||||
self.file_system
|
||||
.find_up(¶ms.start, ¶ms.options, params.sandbox.as_ref())
|
||||
.await
|
||||
.map_err(map_fs_error)
|
||||
}
|
||||
|
||||
pub(crate) async fn canonicalize(
|
||||
&self,
|
||||
params: FsCanonicalizeParams,
|
||||
|
||||
@@ -25,6 +25,8 @@ use crate::protocol::FsCopyParams;
|
||||
use crate::protocol::FsCopyResponse;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
use crate::protocol::FsCreateDirectoryResponse;
|
||||
use crate::protocol::FsFindUpParams;
|
||||
use crate::protocol::FsFindUpResponse;
|
||||
use crate::protocol::FsGetMetadataBatchParams;
|
||||
use crate::protocol::FsGetMetadataBatchResponse;
|
||||
use crate::protocol::FsGetMetadataParams;
|
||||
@@ -310,6 +312,14 @@ impl ExecServerHandler {
|
||||
self.file_system.get_metadata_batch(params).await
|
||||
}
|
||||
|
||||
pub(crate) async fn fs_find_up(
|
||||
&self,
|
||||
params: FsFindUpParams,
|
||||
) -> Result<FsFindUpResponse, JSONRPCErrorError> {
|
||||
self.require_initialized_for("filesystem")?;
|
||||
self.file_system.find_up(params).await
|
||||
}
|
||||
|
||||
pub(crate) async fn fs_canonicalize(
|
||||
&self,
|
||||
params: FsCanonicalizeParams,
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::protocol::FS_CANONICALIZE_METHOD;
|
||||
use crate::protocol::FS_CLOSE_METHOD;
|
||||
use crate::protocol::FS_COPY_METHOD;
|
||||
use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
|
||||
use crate::protocol::FS_FIND_UP_METHOD;
|
||||
use crate::protocol::FS_GET_METADATA_BATCH_METHOD;
|
||||
use crate::protocol::FS_GET_METADATA_METHOD;
|
||||
use crate::protocol::FS_OPEN_METHOD;
|
||||
@@ -24,6 +25,7 @@ use crate::protocol::FsCanonicalizeParams;
|
||||
use crate::protocol::FsCloseParams;
|
||||
use crate::protocol::FsCopyParams;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
use crate::protocol::FsFindUpParams;
|
||||
use crate::protocol::FsGetMetadataBatchParams;
|
||||
use crate::protocol::FsGetMetadataParams;
|
||||
use crate::protocol::FsOpenParams;
|
||||
@@ -145,6 +147,12 @@ pub(crate) fn build_router() -> RpcRouter<ExecServerHandler> {
|
||||
handler.fs_get_metadata_batch(params).await
|
||||
},
|
||||
);
|
||||
router.request(
|
||||
FS_FIND_UP_METHOD,
|
||||
|handler: Arc<ExecServerHandler>, params: FsFindUpParams| async move {
|
||||
handler.fs_find_up(params).await
|
||||
},
|
||||
);
|
||||
router.request(
|
||||
FS_CANONICALIZE_METHOD,
|
||||
|handler: Arc<ExecServerHandler>, params: FsCanonicalizeParams| async move {
|
||||
|
||||
@@ -4,6 +4,14 @@ use codex_exec_server::CopyOptions;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::FILE_READ_CHUNK_SIZE;
|
||||
use codex_exec_server::FileMetadata;
|
||||
use codex_exec_server::FindUpErrorPolicy;
|
||||
use codex_exec_server::FindUpMatch;
|
||||
use codex_exec_server::FindUpMatchKind;
|
||||
use codex_exec_server::FindUpOptions;
|
||||
use codex_exec_server::FindUpOutcome;
|
||||
use codex_exec_server::MAX_FIND_UP_CANDIDATE_BYTES;
|
||||
use codex_exec_server::MAX_FIND_UP_CANDIDATES;
|
||||
use codex_exec_server::MAX_FIND_UP_TOTAL_CANDIDATE_BYTES;
|
||||
use codex_exec_server::ReadDirectoryEntry;
|
||||
use codex_exec_server::RemoveOptions;
|
||||
use codex_exec_server::WalkEntry;
|
||||
@@ -158,6 +166,115 @@ async fn file_system_get_metadata_batch_preserves_order_and_errors(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(FileSystemImplementation::Local ; "local")]
|
||||
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn file_system_find_up_preserves_ancestor_and_candidate_order(
|
||||
implementation: FileSystemImplementation,
|
||||
) -> Result<()> {
|
||||
let context = create_file_system_context(implementation).await?;
|
||||
let file_system = context.file_system;
|
||||
let tmp = TempDir::new()?;
|
||||
let parent = tmp.path().join("parent");
|
||||
let start = parent.join("child");
|
||||
std::fs::create_dir_all(&start)?;
|
||||
std::fs::write(parent.join("first.marker"), "parent")?;
|
||||
std::fs::write(start.join("second.marker"), "child")?;
|
||||
let options = FindUpOptions {
|
||||
candidate_relative_paths: vec!["first.marker".to_string(), "second.marker".to_string()],
|
||||
match_kind: FindUpMatchKind::File,
|
||||
non_not_found_error_policy: FindUpErrorPolicy::Propagate,
|
||||
};
|
||||
let start_uri = PathUri::from_host_native_path(&start)?;
|
||||
|
||||
let nearest = file_system
|
||||
.find_up(&start_uri, &options, /*sandbox*/ None)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
nearest,
|
||||
FindUpOutcome {
|
||||
matched: Some(FindUpMatch {
|
||||
ancestor: start_uri.clone(),
|
||||
path: PathUri::from_host_native_path(start.join("second.marker"))?,
|
||||
}),
|
||||
visited_ancestor_count: 1,
|
||||
metadata_probe_count: 2,
|
||||
ignored_error_count: 0,
|
||||
}
|
||||
);
|
||||
|
||||
std::fs::write(start.join("first.marker"), "first")?;
|
||||
let first = file_system
|
||||
.find_up(&start_uri, &options, /*sandbox*/ None)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
first,
|
||||
FindUpOutcome {
|
||||
matched: Some(FindUpMatch {
|
||||
ancestor: start_uri,
|
||||
path: PathUri::from_host_native_path(start.join("first.marker"))?,
|
||||
}),
|
||||
visited_ancestor_count: 1,
|
||||
metadata_probe_count: 1,
|
||||
ignored_error_count: 0,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(FileSystemImplementation::Local ; "local")]
|
||||
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn file_system_find_up_enforces_bounds_and_sandbox(
|
||||
implementation: FileSystemImplementation,
|
||||
) -> Result<()> {
|
||||
let context = create_file_system_context(implementation).await?;
|
||||
let file_system = context.file_system;
|
||||
let tmp = TempDir::new()?;
|
||||
let start = tmp.path().join("allowed/nested");
|
||||
std::fs::create_dir_all(&start)?;
|
||||
std::fs::write(start.join("marker"), "ok")?;
|
||||
let start_uri = PathUri::from_host_native_path(&start)?;
|
||||
let sandbox = read_only_sandbox(tmp.path().join("allowed"));
|
||||
let options = FindUpOptions {
|
||||
candidate_relative_paths: vec!["marker".to_string()],
|
||||
match_kind: FindUpMatchKind::Any,
|
||||
non_not_found_error_policy: FindUpErrorPolicy::Propagate,
|
||||
};
|
||||
|
||||
assert!(
|
||||
file_system
|
||||
.find_up(&start_uri, &options, Some(&sandbox))
|
||||
.await?
|
||||
.matched
|
||||
.is_some()
|
||||
);
|
||||
|
||||
for invalid_candidates in [
|
||||
vec!["marker".to_string(); MAX_FIND_UP_CANDIDATES + 1],
|
||||
vec!["x".repeat(MAX_FIND_UP_CANDIDATE_BYTES + 1)],
|
||||
vec![
|
||||
"x".repeat(MAX_FIND_UP_TOTAL_CANDIDATE_BYTES / MAX_FIND_UP_CANDIDATES + 1);
|
||||
MAX_FIND_UP_CANDIDATES
|
||||
],
|
||||
vec!["../marker".to_string()],
|
||||
] {
|
||||
let error = file_system
|
||||
.find_up(
|
||||
&start_uri,
|
||||
&FindUpOptions {
|
||||
candidate_relative_paths: invalid_candidates,
|
||||
..options.clone()
|
||||
},
|
||||
Some(&sandbox),
|
||||
)
|
||||
.await
|
||||
.expect_err("invalid candidates should fail closed");
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(FileSystemImplementation::Local ; "local")]
|
||||
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -7,9 +7,13 @@ use futures::StreamExt;
|
||||
use std::io;
|
||||
|
||||
const MAX_CONCURRENT_PROBES: usize = 8;
|
||||
pub const MAX_FIND_UP_CANDIDATES: usize = 16;
|
||||
pub const MAX_FIND_UP_CANDIDATE_BYTES: usize = 256;
|
||||
pub const MAX_FIND_UP_TOTAL_CANDIDATE_BYTES: usize = 2 * 1024;
|
||||
|
||||
/// Controls how an upward marker search handles metadata errors other than `NotFound`.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FindUpErrorPolicy {
|
||||
/// Return the first error in lexical search order.
|
||||
Propagate,
|
||||
@@ -17,6 +21,138 @@ pub enum FindUpErrorPolicy {
|
||||
Ignore,
|
||||
}
|
||||
|
||||
/// Filesystem entry kind that qualifies as an upward-search match.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FindUpMatchKind {
|
||||
Any,
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
impl FindUpMatchKind {
|
||||
fn matches(self, metadata: &crate::FileMetadata) -> bool {
|
||||
match self {
|
||||
Self::Any => true,
|
||||
Self::File => metadata.is_file,
|
||||
Self::Directory => metadata.is_directory,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded options for an upward filesystem search.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FindUpOptions {
|
||||
/// Relative paths tested in order beneath each ancestor.
|
||||
pub candidate_relative_paths: Vec<String>,
|
||||
pub match_kind: FindUpMatchKind,
|
||||
pub non_not_found_error_policy: FindUpErrorPolicy,
|
||||
}
|
||||
|
||||
/// First qualifying path found by an upward filesystem search.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FindUpMatch {
|
||||
pub ancestor: PathUri,
|
||||
pub path: PathUri,
|
||||
}
|
||||
|
||||
/// Result and generic work counters from an upward filesystem search.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FindUpOutcome {
|
||||
pub matched: Option<FindUpMatch>,
|
||||
pub visited_ancestor_count: usize,
|
||||
pub metadata_probe_count: usize,
|
||||
pub ignored_error_count: usize,
|
||||
}
|
||||
|
||||
pub(super) async fn find_up_via_metadata(
|
||||
file_system: &(impl ExecutorFileSystem + ?Sized),
|
||||
start: &PathUri,
|
||||
options: &FindUpOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FindUpOutcome> {
|
||||
validate_find_up_options(start, options)?;
|
||||
|
||||
let mut outcome = FindUpOutcome::default();
|
||||
for ancestor in start.ancestors() {
|
||||
outcome.visited_ancestor_count += 1;
|
||||
for candidate in &options.candidate_relative_paths {
|
||||
let path = checked_candidate_path(&ancestor, candidate)?;
|
||||
outcome.metadata_probe_count += 1;
|
||||
match file_system.get_metadata(&path, sandbox).await {
|
||||
Ok(metadata) if options.match_kind.matches(&metadata) => {
|
||||
outcome.matched = Some(FindUpMatch { ancestor, path });
|
||||
return Ok(outcome);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(err) => match options.non_not_found_error_policy {
|
||||
FindUpErrorPolicy::Propagate => return Err(err),
|
||||
FindUpErrorPolicy::Ignore => outcome.ignored_error_count += 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
fn validate_find_up_options(start: &PathUri, options: &FindUpOptions) -> FileSystemResult<()> {
|
||||
let candidate_count = options.candidate_relative_paths.len();
|
||||
if candidate_count == 0 || candidate_count > MAX_FIND_UP_CANDIDATES {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"filesystem find-up candidate count must be between 1 and {MAX_FIND_UP_CANDIDATES}, got {candidate_count}"
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let mut total_bytes = 0usize;
|
||||
for candidate in &options.candidate_relative_paths {
|
||||
let candidate_bytes = candidate.len();
|
||||
if candidate_bytes == 0 || candidate_bytes > MAX_FIND_UP_CANDIDATE_BYTES {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"filesystem find-up candidates must be between 1 and {MAX_FIND_UP_CANDIDATE_BYTES} bytes"
|
||||
),
|
||||
));
|
||||
}
|
||||
total_bytes = total_bytes.checked_add(candidate_bytes).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"filesystem find-up candidate size overflow",
|
||||
)
|
||||
})?;
|
||||
checked_candidate_path(start, candidate)?;
|
||||
}
|
||||
if total_bytes > MAX_FIND_UP_TOTAL_CANDIDATE_BYTES {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"filesystem find-up candidates must not exceed {MAX_FIND_UP_TOTAL_CANDIDATE_BYTES} bytes total"
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn checked_candidate_path(ancestor: &PathUri, candidate: &str) -> FileSystemResult<PathUri> {
|
||||
let path = ancestor
|
||||
.join(candidate)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||
if path == *ancestor || !path.starts_with(ancestor) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("filesystem find-up candidate {candidate:?} escapes ancestor {ancestor}"),
|
||||
));
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Finds the nearest ancestor containing one of the provided marker names.
|
||||
///
|
||||
/// Marker paths are probed in lexical order from `start` toward the filesystem root. A bounded
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
mod find_up;
|
||||
|
||||
pub use find_up::FindUpErrorPolicy;
|
||||
pub use find_up::FindUpMatch;
|
||||
pub use find_up::FindUpMatchKind;
|
||||
pub use find_up::FindUpOptions;
|
||||
pub use find_up::FindUpOutcome;
|
||||
pub use find_up::MAX_FIND_UP_CANDIDATE_BYTES;
|
||||
pub use find_up::MAX_FIND_UP_CANDIDATES;
|
||||
pub use find_up::MAX_FIND_UP_TOTAL_CANDIDATE_BYTES;
|
||||
pub use find_up::find_nearest_ancestor_with_markers;
|
||||
pub use find_up::find_nearest_native_ancestor_with_markers;
|
||||
|
||||
@@ -324,6 +331,28 @@ pub trait ExecutorFileSystem: Send + Sync {
|
||||
})
|
||||
}
|
||||
|
||||
/// Finds the nearest ancestor containing a qualifying candidate path.
|
||||
fn find_up<'a>(
|
||||
&'a self,
|
||||
start: &'a PathUri,
|
||||
options: &'a FindUpOptions,
|
||||
sandbox: Option<&'a FileSystemSandboxContext>,
|
||||
) -> ExecutorFileSystemFuture<'a, FindUpOutcome> {
|
||||
self.find_up_via_metadata(start, options, sandbox)
|
||||
}
|
||||
|
||||
/// Performs an ordered upward search using primitive metadata operations.
|
||||
///
|
||||
/// Implementations with an optimized transport can use this as a compatibility fallback.
|
||||
fn find_up_via_metadata<'a>(
|
||||
&'a self,
|
||||
start: &'a PathUri,
|
||||
options: &'a FindUpOptions,
|
||||
sandbox: Option<&'a FileSystemSandboxContext>,
|
||||
) -> ExecutorFileSystemFuture<'a, FindUpOutcome> {
|
||||
Box::pin(find_up::find_up_via_metadata(self, start, options, sandbox))
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a PathUri,
|
||||
|
||||
Reference in New Issue
Block a user