mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
a4711b88dd
## Why `fs/readFile` buffers the entire file in one response, which makes large remote reads expensive and prevents callers from applying backpressure. We need an opt-in streaming path with bounded block sizes while preserving the existing single-call API for small and sandboxed reads. ## What changed - Add `ExecServerClient::stream`, returning a named `FileReadStream` that implements `futures::Stream` and yields immutable 1 MiB byte blocks. - Add internal `fs/open`, `fs/readBlock`, and `fs/close` RPCs. `fs/readBlock` accepts an explicit offset and length. - Keep unsandboxed files open between block reads, cap open handles per connection, and clean them up on EOF, error, stream drop, explicit close, or connection shutdown. - Reject platform-sandboxed streaming opens instead of turning the one-shot sandbox helper into a persistent server. Existing `fs/readFile` behavior is unchanged. ## Testing - `just test -p codex-exec-server` - Integration coverage for 1 MiB chunking, exact block-boundary EOF, sandbox rejection, and continued reads from the opened file after path replacement. - Handle-manager coverage for non-sequential offsets, variable block lengths, the 128-handle limit, and capacity release after close.
635 lines
22 KiB
Rust
635 lines
22 KiB
Rust
use anyhow::Context;
|
|
use anyhow::Result;
|
|
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::ReadDirectoryEntry;
|
|
use codex_exec_server::RemoveOptions;
|
|
use codex_protocol::models::AdditionalPermissionProfile;
|
|
use codex_protocol::models::FileSystemPermissions;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
|
use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
|
|
use codex_utils_path_uri::PathUri;
|
|
use futures::TryStreamExt;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::Path;
|
|
use tempfile::TempDir;
|
|
use test_case::test_case;
|
|
|
|
use super::support::FileSystemImplementation;
|
|
use super::support::absolute_path;
|
|
use super::support::create_file_system_context;
|
|
use super::support::read_only_sandbox;
|
|
use super::support::workspace_write_sandbox;
|
|
|
|
#[test]
|
|
fn sandbox_context_from_profile_preserves_workspace_write_read_only_subpaths() -> Result<()> {
|
|
let tmp = TempDir::new()?;
|
|
let writable_dir = tmp.path().join("writable");
|
|
let git_dir = writable_dir.join(".git");
|
|
std::fs::create_dir_all(&git_dir)?;
|
|
|
|
let sandbox = workspace_write_sandbox(writable_dir.clone());
|
|
let permissions: PermissionProfile = sandbox.permissions.try_into()?;
|
|
let policy = permissions.file_system_sandbox_policy();
|
|
let cwd = absolute_path(writable_dir.clone());
|
|
let writable_roots = policy.get_writable_roots_with_cwd(cwd.as_path());
|
|
let writable_dir = absolute_path(std::fs::canonicalize(writable_dir)?);
|
|
let git_dir = absolute_path(std::fs::canonicalize(git_dir)?);
|
|
let Some(writable_root) = writable_roots
|
|
.iter()
|
|
.find(|writable_root| writable_root.root == writable_dir)
|
|
else {
|
|
panic!("writable root should be preserved");
|
|
};
|
|
|
|
assert!(writable_root.read_only_subpaths.contains(&git_dir));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_get_metadata_reports_files_and_directories(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("note.txt");
|
|
let directory_path = tmp.path().join("notes");
|
|
std::fs::write(&file_path, "hello")?;
|
|
std::fs::create_dir(&directory_path)?;
|
|
|
|
let file_metadata = file_system
|
|
.get_metadata(&PathUri::from_path(&file_path)?, /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
file_metadata,
|
|
FileMetadata {
|
|
is_directory: false,
|
|
is_file: true,
|
|
is_symlink: false,
|
|
size: 5,
|
|
created_at_ms: file_metadata.created_at_ms,
|
|
modified_at_ms: file_metadata.modified_at_ms,
|
|
}
|
|
);
|
|
assert!(file_metadata.modified_at_ms > 0);
|
|
|
|
let directory_metadata = file_system
|
|
.get_metadata(&PathUri::from_path(&directory_path)?, /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
directory_metadata,
|
|
FileMetadata {
|
|
is_directory: true,
|
|
is_file: false,
|
|
is_symlink: false,
|
|
size: std::fs::metadata(&directory_path)?.len(),
|
|
created_at_ms: directory_metadata.created_at_ms,
|
|
modified_at_ms: directory_metadata.modified_at_ms,
|
|
}
|
|
);
|
|
assert!(directory_metadata.modified_at_ms > 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_create_directory_creates_nested_directories(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let nested_dir = tmp.path().join("source").join("nested");
|
|
|
|
file_system
|
|
.create_directory(
|
|
&PathUri::from_path(&nested_dir)?,
|
|
CreateDirectoryOptions { recursive: true },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert!(nested_dir.is_dir());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_write_file_writes_bytes(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("note.txt");
|
|
file_system
|
|
.write_file(
|
|
&PathUri::from_path(&file_path)?,
|
|
b"hello from trait".to_vec(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(std::fs::read(file_path)?, b"hello from trait");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn path_uri_join_and_parent_preserve_lexical_paths() -> Result<()> {
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let source_dir_uri = PathUri::from_path(&source_dir)?;
|
|
let joined_nested = source_dir_uri.join("nested/note.txt")?;
|
|
assert_eq!(
|
|
joined_nested,
|
|
PathUri::from_path(source_dir.join("nested").join("note.txt"))?
|
|
);
|
|
let joined_parent = joined_nested.parent();
|
|
assert_eq!(
|
|
joined_parent,
|
|
Some(PathUri::from_path(source_dir.join("nested"))?)
|
|
);
|
|
let joined_parent_traversal = source_dir_uri.join("../outside")?;
|
|
assert_eq!(
|
|
joined_parent_traversal,
|
|
PathUri::from_path(source_dir.join("../outside"))?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_read_file_returns_bytes(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("note.txt");
|
|
std::fs::write(&file_path, "hello from trait")?;
|
|
|
|
let contents = file_system
|
|
.read_file(&PathUri::from_path(&file_path)?, /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(contents, b"hello from trait");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_read_file_stream_returns_bounded_chunks(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("blocks.bin");
|
|
let contents = (0..FILE_READ_CHUNK_SIZE * 2 + 17)
|
|
.map(|index| (index % 251) as u8)
|
|
.collect::<Vec<_>>();
|
|
std::fs::write(&file_path, &contents)?;
|
|
|
|
let chunks = file_system
|
|
.read_file_stream(&PathUri::from_path(file_path)?, /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?
|
|
.try_collect::<Vec<_>>()
|
|
.await?;
|
|
|
|
assert!(
|
|
chunks
|
|
.iter()
|
|
.all(|chunk| !chunk.is_empty() && chunk.len() <= FILE_READ_CHUNK_SIZE)
|
|
);
|
|
assert_eq!(
|
|
chunks
|
|
.iter()
|
|
.flat_map(|chunk| chunk.iter().copied())
|
|
.collect::<Vec<_>>(),
|
|
contents
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_read_file_text_returns_string(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("note.txt");
|
|
std::fs::write(&file_path, "hello from trait")?;
|
|
|
|
let contents = file_system
|
|
.read_file_text(&PathUri::from_path(&file_path)?, /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(contents, "hello from trait");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_copy_copies_file(implementation: FileSystemImplementation) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_file = tmp.path().join("source.txt");
|
|
let copied_file = tmp.path().join("copy.txt");
|
|
std::fs::write(&source_file, "hello from trait")?;
|
|
|
|
file_system
|
|
.copy(
|
|
&PathUri::from_path(&source_file)?,
|
|
&PathUri::from_path(&copied_file)?,
|
|
CopyOptions { recursive: false },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(std::fs::read_to_string(copied_file)?, "hello from trait");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_copy_copies_directory_recursively(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let nested_dir = source_dir.join("nested");
|
|
let nested_file = nested_dir.join("note.txt");
|
|
let copied_dir = tmp.path().join("copied");
|
|
std::fs::create_dir_all(&nested_dir)?;
|
|
std::fs::write(&nested_file, "hello from trait")?;
|
|
|
|
file_system
|
|
.copy(
|
|
&PathUri::from_path(&source_dir)?,
|
|
&PathUri::from_path(&copied_dir)?,
|
|
CopyOptions { recursive: true },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
std::fs::read_to_string(copied_dir.join("nested").join("note.txt"))?,
|
|
"hello from trait"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_read_directory_lists_entries(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
std::fs::create_dir_all(source_dir.join("nested"))?;
|
|
std::fs::write(source_dir.join("root.txt"), "hello")?;
|
|
|
|
let mut entries = file_system
|
|
.read_directory(&PathUri::from_path(&source_dir)?, /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
entries.sort_by(|left, right| left.file_name.cmp(&right.file_name));
|
|
assert_eq!(
|
|
entries,
|
|
vec![
|
|
ReadDirectoryEntry {
|
|
file_name: "nested".to_string(),
|
|
is_directory: true,
|
|
is_file: false,
|
|
},
|
|
ReadDirectoryEntry {
|
|
file_name: "root.txt".to_string(),
|
|
is_directory: false,
|
|
is_file: true,
|
|
},
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_remove_removes_directory(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let directory_path = tmp.path().join("remove-me");
|
|
std::fs::create_dir_all(directory_path.join("nested"))?;
|
|
|
|
file_system
|
|
.remove(
|
|
&PathUri::from_path(&directory_path)?,
|
|
RemoveOptions {
|
|
recursive: true,
|
|
force: true,
|
|
},
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert!(!directory_path.exists());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_write_file_reports_missing_parent(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let missing_parent_path = tmp.path().join("missing").join("note.txt");
|
|
|
|
let error = match file_system
|
|
.write_file(
|
|
&PathUri::from_path(&missing_parent_path)?,
|
|
b"hello from trait".to_vec(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
{
|
|
Ok(()) => anyhow::bail!("write should fail when parent directory is absent"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.kind(),
|
|
std::io::ErrorKind::NotFound,
|
|
"mode={implementation}"
|
|
);
|
|
assert!(!missing_parent_path.exists(), "mode={implementation}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_copy_rejects_directory_without_recursive(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
std::fs::create_dir_all(&source_dir)?;
|
|
|
|
let error = file_system
|
|
.copy(
|
|
&PathUri::from_path(&source_dir)?,
|
|
&PathUri::from_path(tmp.path().join("dest"))?,
|
|
CopyOptions { recursive: false },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await;
|
|
let error = error.expect_err("copying a directory without recursion should fail");
|
|
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"fs/copy requires recursive: true when sourcePath is a directory"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_sandboxed_metadata_and_read_allow_readable_root(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let allowed_dir = tmp.path().join("allowed");
|
|
let file_path = allowed_dir.join("note.txt");
|
|
std::fs::create_dir_all(&allowed_dir)?;
|
|
std::fs::write(&file_path, "sandboxed hello")?;
|
|
let sandbox = read_only_sandbox(allowed_dir);
|
|
|
|
let metadata = file_system
|
|
.get_metadata(&PathUri::from_path(&file_path)?, Some(&sandbox))
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
metadata,
|
|
FileMetadata {
|
|
is_directory: false,
|
|
is_file: true,
|
|
is_symlink: false,
|
|
size: 15,
|
|
created_at_ms: metadata.created_at_ms,
|
|
modified_at_ms: metadata.modified_at_ms,
|
|
}
|
|
);
|
|
|
|
let contents = file_system
|
|
.read_file(&PathUri::from_path(&file_path)?, Some(&sandbox))
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(contents, b"sandboxed hello");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn assert_canonicalize_resolves_directory_alias(
|
|
implementation: FileSystemImplementation,
|
|
create_directory_alias: impl FnOnce(&Path, &Path) -> Result<()>,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let nested_dir = source_dir.join("nested");
|
|
let file_path = nested_dir.join("note.txt");
|
|
let alias_dir = tmp.path().join("source-alias");
|
|
std::fs::create_dir_all(&nested_dir)?;
|
|
std::fs::write(&file_path, "canonical hello")?;
|
|
create_directory_alias(&source_dir, &alias_dir)?;
|
|
|
|
let requested_path = PathUri::from_path(alias_dir.join("nested").join("note.txt"))?;
|
|
let expected_path = PathUri::from_path(std::fs::canonicalize(&file_path)?)?;
|
|
assert_ne!(requested_path, expected_path);
|
|
|
|
let canonical_path = file_system
|
|
.canonicalize(&requested_path, /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(canonical_path, expected_path);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn assert_sandboxed_canonicalize_resolves_directory_alias(
|
|
implementation: FileSystemImplementation,
|
|
create_directory_alias: impl FnOnce(&Path, &Path) -> Result<()>,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let nested_dir = source_dir.join("nested");
|
|
let file_path = nested_dir.join("note.txt");
|
|
let alias_dir = tmp.path().join("source-alias");
|
|
std::fs::create_dir_all(&nested_dir)?;
|
|
std::fs::write(&file_path, "sandboxed canonical hello")?;
|
|
create_directory_alias(&source_dir, &alias_dir)?;
|
|
let sandbox = read_only_sandbox(tmp.path().to_path_buf());
|
|
|
|
let requested_path = PathUri::from_path(alias_dir.join("nested").join("note.txt"))?;
|
|
let expected_path = PathUri::from_path(std::fs::canonicalize(&file_path)?)?;
|
|
assert_ne!(requested_path, expected_path);
|
|
|
|
let canonical_path = file_system
|
|
.canonicalize(&requested_path, Some(&sandbox))
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(canonical_path, expected_path);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Verifies that effective additional permissions extend a read-only sandbox with a writable root.
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_sandboxed_write_allows_additional_write_root(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let readable_dir = tmp.path().join("readable");
|
|
let writable_dir = tmp.path().join("writable");
|
|
let file_path = writable_dir.join("note.txt");
|
|
std::fs::create_dir_all(&readable_dir)?;
|
|
std::fs::create_dir_all(&writable_dir)?;
|
|
|
|
let mut sandbox = read_only_sandbox(readable_dir);
|
|
let additional_permissions = AdditionalPermissionProfile {
|
|
network: None,
|
|
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
|
/*read*/ None,
|
|
Some(vec![absolute_path(writable_dir)]),
|
|
)),
|
|
};
|
|
let native_permissions: PermissionProfile = sandbox.permissions.clone().try_into()?;
|
|
let file_system_policy = effective_file_system_sandbox_policy(
|
|
&native_permissions.file_system_sandbox_policy(),
|
|
Some(&additional_permissions),
|
|
);
|
|
let network_policy = effective_network_sandbox_policy(
|
|
native_permissions.network_sandbox_policy(),
|
|
Some(&additional_permissions),
|
|
);
|
|
sandbox.permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
|
|
native_permissions.enforcement(),
|
|
&file_system_policy,
|
|
network_policy,
|
|
)
|
|
.into();
|
|
|
|
file_system
|
|
.write_file(
|
|
&PathUri::from_path(&file_path)?,
|
|
b"created".to_vec(),
|
|
Some(&sandbox),
|
|
)
|
|
.await
|
|
.with_context(|| format!("write file through additional root mode={implementation}"))?;
|
|
assert_eq!(std::fs::read(&file_path)?, b"created");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_copy_rejects_copying_directory_into_descendant(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
std::fs::create_dir_all(source_dir.join("nested"))?;
|
|
|
|
let error = file_system
|
|
.copy(
|
|
&PathUri::from_path(&source_dir)?,
|
|
&PathUri::from_path(source_dir.join("nested").join("copy"))?,
|
|
CopyOptions { recursive: true },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await;
|
|
let error = error.expect_err("copying a directory into itself should fail");
|
|
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"fs/copy cannot copy a directory to itself or one of its descendants"
|
|
);
|
|
|
|
Ok(())
|
|
}
|