mirror of
https://github.com/openai/codex.git
synced 2026-08-30 14:19:08 +00:00
## What changed - Make `ExecutorFileSystem::walk` a required backend operation instead of providing a fallback built from directory reads and metadata requests. - Implement bounded local walks on a blocking task with cancellation, symlink cycle detection, deterministic ordering, error collection, and response-size limits. - Have remote filesystems use the server's walk operation directly. ## Testing - Cover local and remote handling of invalid roots and limits, directory symlinks, non-UTF-8 names, cancellation, sandbox contexts, and response budgets. GitOrigin-RevId: 7499bf05080c3f9965a5eb7ffd593de604d62c2a
1424 lines
45 KiB
Rust
1424 lines
45 KiB
Rust
use codex_file_system::MAX_WALK_DEPTH;
|
|
use codex_file_system::MAX_WALK_DIRECTORIES;
|
|
use codex_file_system::MAX_WALK_ENTRIES;
|
|
use codex_file_system::MAX_WALK_RESPONSE_BYTES;
|
|
use codex_file_system::WALK_RESPONSE_ITEM_OVERHEAD_BYTES;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_path_uri::PathUri;
|
|
use std::collections::HashSet;
|
|
use std::collections::VecDeque;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::sync::LazyLock;
|
|
use std::time::SystemTime;
|
|
use std::time::UNIX_EPOCH;
|
|
use tokio::io;
|
|
use tokio::io::AsyncReadExt;
|
|
use tokio_util::io::ReaderStream;
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
use crate::CopyOptions;
|
|
use crate::CreateDirectoryOptions;
|
|
use crate::ExecServerRuntimePaths;
|
|
use crate::ExecutorFileSystem;
|
|
use crate::ExecutorFileSystemFuture;
|
|
use crate::FILE_READ_CHUNK_SIZE;
|
|
use crate::FileMetadata;
|
|
use crate::FileSystemReadStream;
|
|
use crate::FileSystemResult;
|
|
use crate::FileSystemSandboxContext;
|
|
use crate::GetMetadataOptions;
|
|
use crate::ReadDirectoryEntry;
|
|
use crate::ReadFileOptions;
|
|
use crate::RemoveOptions;
|
|
use crate::WalkEntry;
|
|
use crate::WalkEntryKind;
|
|
use crate::WalkError;
|
|
use crate::WalkOptions;
|
|
use crate::WalkOutcome;
|
|
use crate::WriteFileOptions;
|
|
use crate::no_follow;
|
|
use crate::regular_file;
|
|
use crate::sandboxed_file_system::SandboxedFileSystem;
|
|
|
|
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
|
|
|
|
fn file_too_large_error() -> io::Error {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("file is too large to read: limit is {MAX_READ_FILE_BYTES} bytes"),
|
|
)
|
|
}
|
|
|
|
pub static LOCAL_FS: LazyLock<Arc<dyn ExecutorFileSystem>> =
|
|
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { Arc::new(LocalFileSystem::unsandboxed()) });
|
|
|
|
#[derive(Clone, Default)]
|
|
pub(crate) struct DirectFileSystem;
|
|
|
|
#[derive(Clone, Default)]
|
|
pub(crate) struct UnsandboxedFileSystem {
|
|
file_system: DirectFileSystem,
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct LocalFileSystem {
|
|
unsandboxed: UnsandboxedFileSystem,
|
|
sandboxed: Option<SandboxedFileSystem>,
|
|
}
|
|
|
|
impl LocalFileSystem {
|
|
pub fn unsandboxed() -> Self {
|
|
Self {
|
|
unsandboxed: UnsandboxedFileSystem::default(),
|
|
sandboxed: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_runtime_paths(runtime_paths: ExecServerRuntimePaths) -> Self {
|
|
Self {
|
|
unsandboxed: UnsandboxedFileSystem::default(),
|
|
sandboxed: Some(SandboxedFileSystem::new(runtime_paths)),
|
|
}
|
|
}
|
|
|
|
fn sandboxed(&self) -> io::Result<&SandboxedFileSystem> {
|
|
self.sandboxed.as_ref().ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"sandboxed filesystem operations require configured runtime paths",
|
|
)
|
|
})
|
|
}
|
|
|
|
fn file_system_for<'a>(
|
|
&'a self,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> io::Result<(
|
|
&'a dyn ExecutorFileSystem,
|
|
Option<&'a FileSystemSandboxContext>,
|
|
)> {
|
|
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
|
|
Ok((self.sandboxed()?, sandbox))
|
|
} else {
|
|
Ok((&self.unsandboxed, sandbox))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LocalFileSystem {
|
|
pub(crate) async fn open_file_for_read(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<tokio::fs::File> {
|
|
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
|
|
return self.sandboxed()?.open_file_for_read(path, sandbox).await;
|
|
}
|
|
self.unsandboxed.open_file_for_read(path, sandbox).await
|
|
}
|
|
|
|
async fn canonicalize(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<PathUri> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system.canonicalize(path, sandbox).await
|
|
}
|
|
|
|
async fn read_file(
|
|
&self,
|
|
path: &PathUri,
|
|
options: ReadFileOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<Vec<u8>> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system.read_file(path, options, sandbox).await
|
|
}
|
|
|
|
async fn read_file_stream(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<FileSystemReadStream> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system.read_file_stream(path, sandbox).await
|
|
}
|
|
|
|
async fn write_file(
|
|
&self,
|
|
path: &PathUri,
|
|
contents: Vec<u8>,
|
|
options: WriteFileOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system
|
|
.write_file(path, contents, options, sandbox)
|
|
.await
|
|
}
|
|
|
|
async fn create_directory(
|
|
&self,
|
|
path: &PathUri,
|
|
options: CreateDirectoryOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system.create_directory(path, options, sandbox).await
|
|
}
|
|
|
|
async fn get_metadata(
|
|
&self,
|
|
path: &PathUri,
|
|
options: GetMetadataOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<FileMetadata> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system.get_metadata(path, options, sandbox).await
|
|
}
|
|
|
|
async fn read_directory(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system.read_directory(path, sandbox).await
|
|
}
|
|
|
|
async fn walk(
|
|
&self,
|
|
path: &PathUri,
|
|
options: WalkOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<WalkOutcome> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system.walk(path, options, sandbox).await
|
|
}
|
|
|
|
async fn remove(
|
|
&self,
|
|
path: &PathUri,
|
|
options: RemoveOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system.remove(path, options, sandbox).await
|
|
}
|
|
|
|
async fn copy(
|
|
&self,
|
|
source_path: &PathUri,
|
|
destination_path: &PathUri,
|
|
options: CopyOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
|
file_system
|
|
.copy(source_path, destination_path, options, sandbox)
|
|
.await
|
|
}
|
|
}
|
|
|
|
impl ExecutorFileSystem for LocalFileSystem {
|
|
fn canonicalize<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, PathUri> {
|
|
Box::pin(LocalFileSystem::canonicalize(self, path, sandbox))
|
|
}
|
|
|
|
fn read_file<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: ReadFileOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, Vec<u8>> {
|
|
Box::pin(LocalFileSystem::read_file(self, path, options, sandbox))
|
|
}
|
|
|
|
fn read_file_stream<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, FileSystemReadStream> {
|
|
Box::pin(LocalFileSystem::read_file_stream(self, path, sandbox))
|
|
}
|
|
|
|
fn write_file<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
contents: Vec<u8>,
|
|
options: WriteFileOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(LocalFileSystem::write_file(
|
|
self, path, contents, options, sandbox,
|
|
))
|
|
}
|
|
|
|
fn create_directory<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: CreateDirectoryOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(LocalFileSystem::create_directory(
|
|
self, path, options, sandbox,
|
|
))
|
|
}
|
|
|
|
fn get_metadata<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: GetMetadataOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
|
|
Box::pin(LocalFileSystem::get_metadata(self, path, options, sandbox))
|
|
}
|
|
|
|
fn read_directory<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
|
|
Box::pin(LocalFileSystem::read_directory(self, path, sandbox))
|
|
}
|
|
|
|
fn walk<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: WalkOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, WalkOutcome> {
|
|
Box::pin(LocalFileSystem::walk(self, path, options, sandbox))
|
|
}
|
|
|
|
fn remove<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: RemoveOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(LocalFileSystem::remove(self, path, options, sandbox))
|
|
}
|
|
|
|
fn copy<'a>(
|
|
&'a self,
|
|
source_path: &'a PathUri,
|
|
destination_path: &'a PathUri,
|
|
options: CopyOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(LocalFileSystem::copy(
|
|
self,
|
|
source_path,
|
|
destination_path,
|
|
options,
|
|
sandbox,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl UnsandboxedFileSystem {
|
|
async fn open_file_for_read(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<tokio::fs::File> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.open_file_for_read(path, /*sandbox*/ None)
|
|
.await
|
|
}
|
|
|
|
async fn canonicalize(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<PathUri> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system.canonicalize(path, /*sandbox*/ None).await
|
|
}
|
|
|
|
async fn read_file(
|
|
&self,
|
|
path: &PathUri,
|
|
options: ReadFileOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<Vec<u8>> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.read_file(path, options, /*sandbox*/ None)
|
|
.await
|
|
}
|
|
|
|
async fn read_file_stream(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<FileSystemReadStream> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.read_file_stream(path, /*sandbox*/ None)
|
|
.await
|
|
}
|
|
|
|
async fn write_file(
|
|
&self,
|
|
path: &PathUri,
|
|
contents: Vec<u8>,
|
|
options: WriteFileOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.write_file(path, contents, options, /*sandbox*/ None)
|
|
.await
|
|
}
|
|
|
|
async fn create_directory(
|
|
&self,
|
|
path: &PathUri,
|
|
options: CreateDirectoryOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.create_directory(path, options, /*sandbox*/ None)
|
|
.await
|
|
}
|
|
|
|
async fn get_metadata(
|
|
&self,
|
|
path: &PathUri,
|
|
options: GetMetadataOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<FileMetadata> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.get_metadata(path, options, /*sandbox*/ None)
|
|
.await
|
|
}
|
|
|
|
async fn read_directory(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.read_directory(path, /*sandbox*/ None)
|
|
.await
|
|
}
|
|
|
|
async fn remove(
|
|
&self,
|
|
path: &PathUri,
|
|
options: RemoveOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.remove(path, options, /*sandbox*/ None)
|
|
.await
|
|
}
|
|
|
|
async fn copy(
|
|
&self,
|
|
source_path: &PathUri,
|
|
destination_path: &PathUri,
|
|
options: CopyOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system
|
|
.copy(
|
|
source_path,
|
|
destination_path,
|
|
options,
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
|
|
impl ExecutorFileSystem for UnsandboxedFileSystem {
|
|
fn canonicalize<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, PathUri> {
|
|
Box::pin(UnsandboxedFileSystem::canonicalize(self, path, sandbox))
|
|
}
|
|
|
|
fn read_file<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: ReadFileOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, Vec<u8>> {
|
|
Box::pin(UnsandboxedFileSystem::read_file(
|
|
self, path, options, sandbox,
|
|
))
|
|
}
|
|
|
|
fn read_file_stream<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, FileSystemReadStream> {
|
|
Box::pin(UnsandboxedFileSystem::read_file_stream(self, path, sandbox))
|
|
}
|
|
|
|
fn write_file<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
contents: Vec<u8>,
|
|
options: WriteFileOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(UnsandboxedFileSystem::write_file(
|
|
self, path, contents, options, sandbox,
|
|
))
|
|
}
|
|
|
|
fn create_directory<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: CreateDirectoryOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(UnsandboxedFileSystem::create_directory(
|
|
self, path, options, sandbox,
|
|
))
|
|
}
|
|
|
|
fn get_metadata<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: GetMetadataOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
|
|
Box::pin(UnsandboxedFileSystem::get_metadata(
|
|
self, path, options, sandbox,
|
|
))
|
|
}
|
|
|
|
fn read_directory<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
|
|
Box::pin(UnsandboxedFileSystem::read_directory(self, path, sandbox))
|
|
}
|
|
|
|
fn walk<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: WalkOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, WalkOutcome> {
|
|
Box::pin(async move {
|
|
reject_platform_sandbox_context(sandbox)?;
|
|
self.file_system.walk(path, options, /*sandbox*/ None).await
|
|
})
|
|
}
|
|
|
|
fn remove<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: RemoveOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(UnsandboxedFileSystem::remove(self, path, options, sandbox))
|
|
}
|
|
|
|
fn copy<'a>(
|
|
&'a self,
|
|
source_path: &'a PathUri,
|
|
destination_path: &'a PathUri,
|
|
options: CopyOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(UnsandboxedFileSystem::copy(
|
|
self,
|
|
source_path,
|
|
destination_path,
|
|
options,
|
|
sandbox,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl DirectFileSystem {
|
|
async fn open_file_for_read(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<tokio::fs::File> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let path = path.to_abs_path()?;
|
|
regular_file::open(path.as_path()).await
|
|
}
|
|
|
|
async fn canonicalize(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<PathUri> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let path = path.to_abs_path()?;
|
|
let canonicalized =
|
|
AbsolutePathBuf::from_absolute_path(tokio::fs::canonicalize(path.as_path()).await?)?;
|
|
Ok(PathUri::from_abs_path(&canonicalized))
|
|
}
|
|
|
|
async fn read_file(
|
|
&self,
|
|
path: &PathUri,
|
|
options: ReadFileOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<Vec<u8>> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let file = if options.follow_symlinks {
|
|
self.open_file_for_read(path, /*sandbox*/ None).await?
|
|
} else {
|
|
no_follow::open_file(path.to_abs_path()?.as_path()).await?
|
|
};
|
|
let metadata = file.metadata().await?;
|
|
if metadata.len() > MAX_READ_FILE_BYTES {
|
|
return Err(file_too_large_error());
|
|
}
|
|
let mut bytes = Vec::with_capacity(metadata.len() as usize);
|
|
file.take(MAX_READ_FILE_BYTES + 1)
|
|
.read_to_end(&mut bytes)
|
|
.await?;
|
|
if bytes.len() as u64 > MAX_READ_FILE_BYTES {
|
|
return Err(file_too_large_error());
|
|
}
|
|
Ok(bytes)
|
|
}
|
|
|
|
async fn read_file_stream(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<FileSystemReadStream> {
|
|
let file = self.open_file_for_read(path, sandbox).await?;
|
|
Ok(FileSystemReadStream::new(ReaderStream::with_capacity(
|
|
file,
|
|
FILE_READ_CHUNK_SIZE,
|
|
)))
|
|
}
|
|
|
|
async fn write_file(
|
|
&self,
|
|
path: &PathUri,
|
|
contents: Vec<u8>,
|
|
options: WriteFileOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let path = path.to_abs_path()?;
|
|
if options.follow_symlinks {
|
|
tokio::fs::write(path.as_path(), contents).await
|
|
} else {
|
|
no_follow::write_file(path.as_path(), contents).await
|
|
}
|
|
}
|
|
|
|
async fn create_directory(
|
|
&self,
|
|
path: &PathUri,
|
|
options: CreateDirectoryOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let path = path.to_abs_path()?;
|
|
if !options.follow_symlinks {
|
|
return no_follow::create_directory(path.as_path(), options.recursive).await;
|
|
}
|
|
if options.recursive {
|
|
tokio::fs::create_dir_all(path.as_path()).await?;
|
|
} else {
|
|
tokio::fs::create_dir(path.as_path()).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_metadata(
|
|
&self,
|
|
path: &PathUri,
|
|
options: GetMetadataOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<FileMetadata> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let path = path.to_abs_path()?;
|
|
if !options.follow_symlinks {
|
|
return no_follow::metadata(path.as_path()).await;
|
|
}
|
|
let symlink_metadata = tokio::fs::symlink_metadata(path.as_path()).await?;
|
|
let is_symlink = symlink_metadata.is_symlink();
|
|
let metadata = if is_symlink {
|
|
tokio::fs::metadata(path.as_path()).await?
|
|
} else {
|
|
symlink_metadata
|
|
};
|
|
Ok(file_metadata(metadata, is_symlink))
|
|
}
|
|
|
|
async fn read_directory(
|
|
&self,
|
|
path: &PathUri,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let path = path.to_abs_path()?;
|
|
let mut entries = Vec::new();
|
|
let mut read_dir = tokio::fs::read_dir(path.as_path()).await?;
|
|
while let Some(entry) = read_dir.next_entry().await? {
|
|
let Ok(mut file_type) = entry.file_type().await else {
|
|
continue;
|
|
};
|
|
if file_type.is_symlink() {
|
|
let Ok(metadata) = tokio::fs::metadata(entry.path()).await else {
|
|
continue;
|
|
};
|
|
file_type = metadata.file_type();
|
|
}
|
|
entries.push(ReadDirectoryEntry {
|
|
file_name: entry.file_name().to_string_lossy().into_owned(),
|
|
is_directory: file_type.is_dir(),
|
|
is_file: file_type.is_file(),
|
|
});
|
|
}
|
|
Ok(entries)
|
|
}
|
|
|
|
fn sync_walk(
|
|
root: &PathUri,
|
|
options: WalkOptions,
|
|
cancelled: &CancellationToken,
|
|
) -> io::Result<WalkOutcome> {
|
|
if options.max_directories == 0 || options.max_entries == 0 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"filesystem walk limits must be greater than zero",
|
|
));
|
|
}
|
|
if options.max_depth > MAX_WALK_DEPTH
|
|
|| options.max_directories > MAX_WALK_DIRECTORIES
|
|
|| options.max_entries > MAX_WALK_ENTRIES
|
|
{
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!(
|
|
"filesystem walk limits exceed maximums: depth={MAX_WALK_DEPTH}, directories={MAX_WALK_DIRECTORIES}, entries={MAX_WALK_ENTRIES}"
|
|
),
|
|
));
|
|
}
|
|
|
|
check_walk_cancelled(cancelled)?;
|
|
let (root_metadata, root_is_symlink) = walk_metadata(root)?;
|
|
if !root_metadata.is_dir() || (root_is_symlink && !options.follow_directory_symlinks) {
|
|
return Ok(WalkOutcome::default());
|
|
}
|
|
|
|
let root_identity = if options.follow_directory_symlinks {
|
|
check_walk_cancelled(cancelled)?;
|
|
walk_canonicalize(root)?
|
|
} else {
|
|
root.clone()
|
|
};
|
|
let mut outcome = WalkOutcome::default();
|
|
let mut queue = VecDeque::from([(root.clone(), 0usize)]);
|
|
let mut visited_directories = HashSet::from([root_identity]);
|
|
let mut directory_count = 1usize;
|
|
let mut entry_count = 0usize;
|
|
let mut response_bytes = 0usize;
|
|
|
|
while let Some((directory, depth)) = queue.pop_front() {
|
|
let entries = walk_read_directory(&directory, cancelled);
|
|
check_walk_cancelled(cancelled)?;
|
|
let mut entries = match entries {
|
|
Ok(entries) => entries,
|
|
Err(error) => {
|
|
if !push_walk_error(
|
|
&mut outcome,
|
|
&mut response_bytes,
|
|
directory,
|
|
error.to_string(),
|
|
) {
|
|
return Ok(outcome);
|
|
}
|
|
continue;
|
|
}
|
|
};
|
|
entries.sort();
|
|
|
|
for file_name in entries {
|
|
check_walk_cancelled(cancelled)?;
|
|
if entry_count == options.max_entries {
|
|
outcome.truncated = true;
|
|
return Ok(outcome);
|
|
}
|
|
entry_count += 1;
|
|
|
|
let path = match directory.join(&file_name) {
|
|
Ok(path) => path,
|
|
Err(error) => {
|
|
if !push_walk_error(
|
|
&mut outcome,
|
|
&mut response_bytes,
|
|
directory.clone(),
|
|
error.to_string(),
|
|
) {
|
|
return Ok(outcome);
|
|
}
|
|
continue;
|
|
}
|
|
};
|
|
let (metadata, is_symlink) = match walk_metadata(&path) {
|
|
Ok(metadata) => metadata,
|
|
Err(error) => {
|
|
if !push_walk_error(
|
|
&mut outcome,
|
|
&mut response_bytes,
|
|
path,
|
|
error.to_string(),
|
|
) {
|
|
return Ok(outcome);
|
|
}
|
|
continue;
|
|
}
|
|
};
|
|
if is_symlink && (!options.follow_directory_symlinks || !metadata.is_dir()) {
|
|
continue;
|
|
}
|
|
|
|
let kind = if metadata.is_dir() {
|
|
WalkEntryKind::Directory
|
|
} else if metadata.is_file() {
|
|
WalkEntryKind::File
|
|
} else {
|
|
continue;
|
|
};
|
|
if !reserve_walk_response_bytes(
|
|
&mut outcome,
|
|
&mut response_bytes,
|
|
path.to_string().len(),
|
|
) {
|
|
return Ok(outcome);
|
|
}
|
|
outcome.entries.push(WalkEntry {
|
|
path: path.clone(),
|
|
kind,
|
|
});
|
|
|
|
if kind == WalkEntryKind::Directory && depth < options.max_depth {
|
|
if options.prune_hidden_directories && file_name.starts_with('.') {
|
|
continue;
|
|
}
|
|
let directory_identity = if options.follow_directory_symlinks {
|
|
check_walk_cancelled(cancelled)?;
|
|
match walk_canonicalize(&path) {
|
|
Ok(path) => path,
|
|
Err(error) => {
|
|
if !push_walk_error(
|
|
&mut outcome,
|
|
&mut response_bytes,
|
|
path,
|
|
error.to_string(),
|
|
) {
|
|
return Ok(outcome);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
path.clone()
|
|
};
|
|
if !visited_directories.insert(directory_identity) {
|
|
continue;
|
|
}
|
|
if directory_count == options.max_directories {
|
|
outcome.truncated = true;
|
|
} else {
|
|
directory_count += 1;
|
|
queue.push_back((path, depth + 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(outcome)
|
|
}
|
|
|
|
async fn remove(
|
|
&self,
|
|
path: &PathUri,
|
|
options: RemoveOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let path = path.to_abs_path()?;
|
|
if !options.follow_symlinks {
|
|
return no_follow::remove(path.as_path(), options.recursive, options.force).await;
|
|
}
|
|
match tokio::fs::symlink_metadata(path.as_path()).await {
|
|
Ok(metadata) => {
|
|
let file_type = metadata.file_type();
|
|
if file_type.is_dir() {
|
|
if options.recursive {
|
|
tokio::fs::remove_dir_all(path.as_path()).await?;
|
|
} else {
|
|
tokio::fs::remove_dir(path.as_path()).await?;
|
|
}
|
|
} else {
|
|
tokio::fs::remove_file(path.as_path()).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(err) if err.kind() == io::ErrorKind::NotFound && options.force => Ok(()),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
async fn copy(
|
|
&self,
|
|
source_path: &PathUri,
|
|
destination_path: &PathUri,
|
|
options: CopyOptions,
|
|
sandbox: Option<&FileSystemSandboxContext>,
|
|
) -> FileSystemResult<()> {
|
|
reject_sandbox_context(sandbox)?;
|
|
let source_path = source_path.to_abs_path()?.into_path_buf();
|
|
let destination_path = destination_path.to_abs_path()?.into_path_buf();
|
|
tokio::task::spawn_blocking(move || -> FileSystemResult<()> {
|
|
let metadata = std::fs::symlink_metadata(source_path.as_path())?;
|
|
let file_type = metadata.file_type();
|
|
|
|
if file_type.is_dir() {
|
|
if !options.recursive {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"fs/copy requires recursive: true when sourcePath is a directory",
|
|
));
|
|
}
|
|
if destination_is_same_or_descendant_of_source(
|
|
source_path.as_path(),
|
|
destination_path.as_path(),
|
|
)? {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"fs/copy cannot copy a directory to itself or one of its descendants",
|
|
));
|
|
}
|
|
copy_dir_recursive(source_path.as_path(), destination_path.as_path())?;
|
|
return Ok(());
|
|
}
|
|
|
|
if file_type.is_symlink() {
|
|
copy_symlink(source_path.as_path(), destination_path.as_path())?;
|
|
return Ok(());
|
|
}
|
|
|
|
if file_type.is_file() {
|
|
std::fs::copy(source_path.as_path(), destination_path.as_path())?;
|
|
return Ok(());
|
|
}
|
|
|
|
Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"fs/copy only supports regular files, directories, and symlinks",
|
|
))
|
|
})
|
|
.await
|
|
.map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))?
|
|
}
|
|
}
|
|
|
|
impl ExecutorFileSystem for DirectFileSystem {
|
|
fn canonicalize<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, PathUri> {
|
|
Box::pin(DirectFileSystem::canonicalize(self, path, sandbox))
|
|
}
|
|
|
|
fn read_file<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: ReadFileOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, Vec<u8>> {
|
|
Box::pin(DirectFileSystem::read_file(self, path, options, sandbox))
|
|
}
|
|
|
|
fn read_file_stream<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, FileSystemReadStream> {
|
|
Box::pin(DirectFileSystem::read_file_stream(self, path, sandbox))
|
|
}
|
|
|
|
fn write_file<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
contents: Vec<u8>,
|
|
options: WriteFileOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(DirectFileSystem::write_file(
|
|
self, path, contents, options, sandbox,
|
|
))
|
|
}
|
|
|
|
fn create_directory<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: CreateDirectoryOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(DirectFileSystem::create_directory(
|
|
self, path, options, sandbox,
|
|
))
|
|
}
|
|
|
|
fn get_metadata<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: GetMetadataOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
|
|
Box::pin(DirectFileSystem::get_metadata(self, path, options, sandbox))
|
|
}
|
|
|
|
fn read_directory<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
|
|
Box::pin(DirectFileSystem::read_directory(self, path, sandbox))
|
|
}
|
|
|
|
fn walk<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: WalkOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, WalkOutcome> {
|
|
Box::pin(async move {
|
|
reject_sandbox_context(sandbox)?;
|
|
let path = path.clone();
|
|
let cancelled = CancellationToken::new();
|
|
let _cancel_on_drop = cancelled.clone().drop_guard();
|
|
tokio::task::spawn_blocking(move || Self::sync_walk(&path, options, &cancelled))
|
|
.await
|
|
.map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))?
|
|
})
|
|
}
|
|
|
|
fn remove<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
options: RemoveOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(DirectFileSystem::remove(self, path, options, sandbox))
|
|
}
|
|
|
|
fn copy<'a>(
|
|
&'a self,
|
|
source_path: &'a PathUri,
|
|
destination_path: &'a PathUri,
|
|
options: CopyOptions,
|
|
sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(DirectFileSystem::copy(
|
|
self,
|
|
source_path,
|
|
destination_path,
|
|
options,
|
|
sandbox,
|
|
))
|
|
}
|
|
}
|
|
|
|
fn check_walk_cancelled(cancelled: &CancellationToken) -> io::Result<()> {
|
|
if cancelled.is_cancelled() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::Interrupted,
|
|
"filesystem walk cancelled",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn walk_metadata(path: &PathUri) -> io::Result<(std::fs::Metadata, bool)> {
|
|
let path = path.to_abs_path()?;
|
|
let metadata = std::fs::symlink_metadata(path.as_path())?;
|
|
let is_symlink = metadata.is_symlink();
|
|
let metadata = if is_symlink {
|
|
std::fs::metadata(path.as_path())?
|
|
} else {
|
|
metadata
|
|
};
|
|
Ok((metadata, is_symlink))
|
|
}
|
|
|
|
fn walk_canonicalize(path: &PathUri) -> io::Result<PathUri> {
|
|
let path = path.to_abs_path()?;
|
|
let canonicalized =
|
|
AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(path.as_path())?)?;
|
|
Ok(PathUri::from_abs_path(&canonicalized))
|
|
}
|
|
|
|
fn walk_read_directory(path: &PathUri, cancelled: &CancellationToken) -> io::Result<Vec<String>> {
|
|
check_walk_cancelled(cancelled)?;
|
|
let path = path.to_abs_path()?;
|
|
let mut entries = Vec::new();
|
|
for entry in std::fs::read_dir(path.as_path())? {
|
|
check_walk_cancelled(cancelled)?;
|
|
let entry = entry?;
|
|
let Ok(file_type) = entry.file_type() else {
|
|
continue;
|
|
};
|
|
// Match DirectFileSystem::read_directory: omit broken or inaccessible links.
|
|
if file_type.is_symlink() {
|
|
check_walk_cancelled(cancelled)?;
|
|
if std::fs::metadata(entry.path()).is_err() {
|
|
continue;
|
|
}
|
|
}
|
|
entries.push(entry.file_name().to_string_lossy().into_owned());
|
|
}
|
|
Ok(entries)
|
|
}
|
|
|
|
fn push_walk_error(
|
|
outcome: &mut WalkOutcome,
|
|
response_bytes: &mut usize,
|
|
path: PathUri,
|
|
message: String,
|
|
) -> bool {
|
|
let item_bytes = path.to_string().len().saturating_add(message.len());
|
|
if !reserve_walk_response_bytes(outcome, response_bytes, item_bytes) {
|
|
return false;
|
|
}
|
|
outcome.errors.push(WalkError { path, message });
|
|
true
|
|
}
|
|
|
|
fn reserve_walk_response_bytes(
|
|
outcome: &mut WalkOutcome,
|
|
response_bytes: &mut usize,
|
|
content_bytes: usize,
|
|
) -> bool {
|
|
let item_bytes = content_bytes.saturating_add(WALK_RESPONSE_ITEM_OVERHEAD_BYTES);
|
|
let Some(total_bytes) = response_bytes.checked_add(item_bytes) else {
|
|
outcome.truncated = true;
|
|
return false;
|
|
};
|
|
if total_bytes > MAX_WALK_RESPONSE_BYTES {
|
|
outcome.truncated = true;
|
|
return false;
|
|
}
|
|
*response_bytes = total_bytes;
|
|
true
|
|
}
|
|
|
|
fn reject_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
|
|
if sandbox.is_some() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"direct filesystem operations do not accept sandbox context",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn file_metadata(metadata: std::fs::Metadata, is_symlink: bool) -> FileMetadata {
|
|
FileMetadata {
|
|
is_directory: metadata.is_dir(),
|
|
is_file: metadata.is_file(),
|
|
is_symlink,
|
|
size: metadata.len(),
|
|
created_at_ms: metadata.created().ok().map_or(0, system_time_to_unix_ms),
|
|
modified_at_ms: metadata.modified().ok().map_or(0, system_time_to_unix_ms),
|
|
}
|
|
}
|
|
|
|
fn reject_platform_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
|
|
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"sandboxed filesystem operations require configured runtime paths",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> {
|
|
std::fs::create_dir_all(target)?;
|
|
for entry in std::fs::read_dir(source)? {
|
|
let entry = entry?;
|
|
let source_path = entry.path();
|
|
let target_path = target.join(entry.file_name());
|
|
let file_type = entry.file_type()?;
|
|
|
|
if file_type.is_dir() {
|
|
copy_dir_recursive(&source_path, &target_path)?;
|
|
} else if file_type.is_file() {
|
|
std::fs::copy(&source_path, &target_path)?;
|
|
} else if file_type.is_symlink() {
|
|
copy_symlink(&source_path, &target_path)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn destination_is_same_or_descendant_of_source(
|
|
source: &Path,
|
|
destination: &Path,
|
|
) -> io::Result<bool> {
|
|
let source = std::fs::canonicalize(source)?;
|
|
let destination = resolve_existing_path(destination)?;
|
|
Ok(destination.starts_with(&source))
|
|
}
|
|
|
|
pub(crate) fn resolve_existing_path(path: &Path) -> io::Result<PathBuf> {
|
|
let mut unresolved_suffix = Vec::new();
|
|
let mut existing_path = path;
|
|
while !existing_path.exists() {
|
|
let Some(file_name) = existing_path.file_name() else {
|
|
break;
|
|
};
|
|
unresolved_suffix.push(file_name.to_os_string());
|
|
let Some(parent) = existing_path.parent() else {
|
|
break;
|
|
};
|
|
existing_path = parent;
|
|
}
|
|
|
|
let mut resolved = std::fs::canonicalize(existing_path)?;
|
|
for file_name in unresolved_suffix.iter().rev() {
|
|
resolved.push(file_name);
|
|
}
|
|
Ok(resolved)
|
|
}
|
|
|
|
pub(crate) fn current_sandbox_cwd() -> io::Result<PathBuf> {
|
|
let cwd = std::env::current_dir()
|
|
.map_err(|err| io::Error::other(format!("failed to read current dir: {err}")))?;
|
|
resolve_existing_path(cwd.as_path())
|
|
}
|
|
|
|
fn copy_symlink(source: &Path, target: &Path) -> io::Result<()> {
|
|
let link_target = std::fs::read_link(source)?;
|
|
#[cfg(unix)]
|
|
{
|
|
std::os::unix::fs::symlink(&link_target, target)
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
if symlink_points_to_directory(source)? {
|
|
std::os::windows::fs::symlink_dir(&link_target, target)
|
|
} else {
|
|
std::os::windows::fs::symlink_file(&link_target, target)
|
|
}
|
|
}
|
|
#[cfg(not(any(unix, windows)))]
|
|
{
|
|
let _ = link_target;
|
|
let _ = target;
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Unsupported,
|
|
"copying symlinks is unsupported on this platform",
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn symlink_points_to_directory(source: &Path) -> io::Result<bool> {
|
|
use std::os::windows::fs::FileTypeExt;
|
|
|
|
Ok(std::fs::symlink_metadata(source)?
|
|
.file_type()
|
|
.is_symlink_dir())
|
|
}
|
|
|
|
fn system_time_to_unix_ms(time: SystemTime) -> i64 {
|
|
time.duration_since(UNIX_EPOCH)
|
|
.ok()
|
|
.and_then(|duration| i64::try_from(duration.as_millis()).ok())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
#[cfg(all(test, any(unix, windows)))]
|
|
#[path = "local_file_system_path_uri_tests.rs"]
|
|
mod path_uri_tests;
|
|
|
|
#[cfg(all(test, unix))]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use std::os::unix::fs::symlink;
|
|
|
|
#[test]
|
|
fn resolve_existing_path_handles_symlink_parent_dotdot_escape() -> io::Result<()> {
|
|
let temp_dir = tempfile::TempDir::new()?;
|
|
let allowed_dir = temp_dir.path().join("allowed");
|
|
let outside_dir = temp_dir.path().join("outside");
|
|
std::fs::create_dir_all(&allowed_dir)?;
|
|
std::fs::create_dir_all(&outside_dir)?;
|
|
symlink(&outside_dir, allowed_dir.join("link"))?;
|
|
|
|
let resolved = resolve_existing_path(
|
|
allowed_dir
|
|
.join("link")
|
|
.join("..")
|
|
.join("secret.txt")
|
|
.as_path(),
|
|
)?;
|
|
|
|
assert_eq!(
|
|
resolved,
|
|
resolve_existing_path(temp_dir.path())?.join("secret.txt")
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(all(test, windows))]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn symlink_points_to_directory_handles_dangling_directory_symlinks() -> io::Result<()> {
|
|
use std::os::windows::fs::symlink_dir;
|
|
|
|
let temp_dir = tempfile::TempDir::new()?;
|
|
let source_dir = temp_dir.path().join("source");
|
|
let link_path = temp_dir.path().join("source-link");
|
|
std::fs::create_dir(&source_dir)?;
|
|
|
|
if symlink_dir(&source_dir, &link_path).is_err() {
|
|
return Ok(());
|
|
}
|
|
|
|
std::fs::remove_dir(&source_dir)?;
|
|
|
|
assert_eq!(symlink_points_to_directory(&link_path)?, true);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod walk_tests {
|
|
use super::*;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[tokio::test]
|
|
async fn sync_walk_rejects_sandbox_context() -> io::Result<()> {
|
|
let temp = tempfile::tempdir()?;
|
|
let root = PathUri::from_host_native_path(temp.path())?;
|
|
let sandbox = FileSystemSandboxContext::from_permission_profile(
|
|
PermissionProfile::from_runtime_permissions(
|
|
&FileSystemSandboxPolicy::restricted(Vec::new()),
|
|
NetworkSandboxPolicy::Restricted,
|
|
),
|
|
);
|
|
let options = WalkOptions {
|
|
max_depth: 1,
|
|
max_directories: 1,
|
|
max_entries: 1,
|
|
follow_directory_symlinks: false,
|
|
prune_hidden_directories: false,
|
|
};
|
|
let direct_error = DirectFileSystem
|
|
.walk(&root, options, Some(&sandbox))
|
|
.await
|
|
.expect_err("direct walk must reject sandbox contexts");
|
|
let wrapper_error = UnsandboxedFileSystem::default()
|
|
.walk(&root, options, Some(&sandbox))
|
|
.await
|
|
.expect_err("unsandboxed walk must reject restricted contexts");
|
|
assert_eq!(direct_error.kind(), io::ErrorKind::InvalidInput);
|
|
assert_eq!(wrapper_error.kind(), io::ErrorKind::InvalidInput);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn sync_walk_cancellation_stops_before_io() -> io::Result<()> {
|
|
let temp = tempfile::tempdir()?;
|
|
let missing = PathUri::from_host_native_path(temp.path().join("missing"))?;
|
|
let options = WalkOptions {
|
|
max_depth: 1,
|
|
max_directories: 1,
|
|
max_entries: 1,
|
|
follow_directory_symlinks: true,
|
|
prune_hidden_directories: false,
|
|
};
|
|
let cancelled = CancellationToken::new();
|
|
let cancel_on_drop = cancelled.clone().drop_guard();
|
|
drop(cancel_on_drop);
|
|
|
|
for result in [
|
|
DirectFileSystem::sync_walk(&missing, options, &cancelled).map(|_| ()),
|
|
walk_read_directory(&missing, &cancelled).map(|_| ()),
|
|
] {
|
|
assert_eq!(
|
|
result
|
|
.expect_err("cancelled walks must stop before I/O")
|
|
.kind(),
|
|
io::ErrorKind::Interrupted,
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn sync_walk_response_budget_counts_entries_and_errors() -> io::Result<()> {
|
|
let temp = tempfile::tempdir()?;
|
|
let root = PathUri::from_host_native_path(temp.path())?;
|
|
let mut outcome = WalkOutcome::default();
|
|
let mut response_bytes =
|
|
MAX_WALK_RESPONSE_BYTES - WALK_RESPONSE_ITEM_OVERHEAD_BYTES - root.to_string().len();
|
|
assert!(push_walk_error(
|
|
&mut outcome,
|
|
&mut response_bytes,
|
|
root.clone(),
|
|
String::new()
|
|
));
|
|
assert!(!reserve_walk_response_bytes(
|
|
&mut outcome,
|
|
&mut response_bytes,
|
|
/*content_bytes*/ 0
|
|
));
|
|
assert_eq!(
|
|
outcome,
|
|
WalkOutcome {
|
|
entries: Vec::new(),
|
|
errors: vec![WalkError {
|
|
path: root,
|
|
message: String::new()
|
|
}],
|
|
truncated: true,
|
|
},
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|