mirror of
https://github.com/openai/codex.git
synced 2026-03-20 04:46:31 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e9ec8983c |
10
codex-rs/Cargo.lock
generated
10
codex-rs/Cargo.lock
generated
@@ -1557,6 +1557,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-apply-patch",
|
||||
"codex-fs-ops",
|
||||
"codex-linux-sandbox",
|
||||
"codex-shell-escalation",
|
||||
"codex-utils-home-dir",
|
||||
@@ -1846,6 +1847,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-file-search",
|
||||
"codex-fs-ops",
|
||||
"codex-git",
|
||||
"codex-hooks",
|
||||
"codex-keyring-store",
|
||||
@@ -2087,6 +2089,14 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-fs-ops"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-git"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -11,6 +11,7 @@ members = [
|
||||
"apply-patch",
|
||||
"arg0",
|
||||
"feedback",
|
||||
"fs-ops",
|
||||
"codex-backend-openapi-models",
|
||||
"cloud-requirements",
|
||||
"cloud-tasks",
|
||||
@@ -109,6 +110,7 @@ codex-exec-server = { path = "exec-server" }
|
||||
codex-execpolicy = { path = "execpolicy" }
|
||||
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
|
||||
codex-feedback = { path = "feedback" }
|
||||
codex-fs-ops = { path = "fs-ops" }
|
||||
codex-file-search = { path = "file-search" }
|
||||
codex-git = { path = "utils/git" }
|
||||
codex-hooks = { path = "hooks" }
|
||||
|
||||
@@ -14,6 +14,7 @@ workspace = true
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-apply-patch = { workspace = true }
|
||||
codex-fs-ops = { workspace = true }
|
||||
codex-linux-sandbox = { workspace = true }
|
||||
codex-shell-escalation = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
|
||||
use codex_fs_ops::CODEX_CORE_FS_OPS_ARG1;
|
||||
use codex_utils_home_dir::find_codex_home;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::symlink;
|
||||
@@ -105,6 +106,17 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
|
||||
};
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
if argv1 == CODEX_CORE_FS_OPS_ARG1 {
|
||||
let mut stdin = std::io::stdin();
|
||||
let mut stdout = std::io::stdout();
|
||||
let mut stderr = std::io::stderr();
|
||||
let exit_code =
|
||||
match codex_fs_ops::run_from_args(args, &mut stdin, &mut stdout, &mut stderr) {
|
||||
Ok(()) => 0,
|
||||
Err(_) => 1,
|
||||
};
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
// This modifies the environment, which is not thread-safe, so do this
|
||||
// before creating any threads/the Tokio runtime.
|
||||
|
||||
@@ -38,6 +38,7 @@ codex-exec-server = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-skills = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-fs-ops = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
codex-git = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
|
||||
@@ -332,6 +332,58 @@ pub(crate) async fn execute_exec_request(
|
||||
finalize_exec_result(raw_output_result, sandbox, duration)
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_exec_request_raw_output(
|
||||
exec_request: ExecRequest,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
stdout_stream: Option<StdoutStream>,
|
||||
after_spawn: Option<Box<dyn FnOnce() + Send>>,
|
||||
) -> Result<ExecToolCallRawOutput> {
|
||||
let ExecRequest {
|
||||
command,
|
||||
cwd,
|
||||
env,
|
||||
network,
|
||||
expiration,
|
||||
sandbox,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
sandbox_permissions,
|
||||
sandbox_policy: _sandbox_policy_from_env,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
justification,
|
||||
arg0,
|
||||
} = exec_request;
|
||||
let _ = _sandbox_policy_from_env;
|
||||
|
||||
let params = ExecParams {
|
||||
command,
|
||||
cwd,
|
||||
expiration,
|
||||
env,
|
||||
network: network.clone(),
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
justification,
|
||||
arg0,
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
let raw_output_result = exec(
|
||||
params,
|
||||
sandbox,
|
||||
sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
stdout_stream,
|
||||
after_spawn,
|
||||
)
|
||||
.await;
|
||||
let duration = start.elapsed();
|
||||
finalize_exec_result_raw_output(raw_output_result, sandbox, duration)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn extract_create_process_as_user_error_code(err: &str) -> Option<String> {
|
||||
let marker = "CreateProcessAsUserW failed: ";
|
||||
@@ -574,6 +626,64 @@ fn finalize_exec_result(
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_exec_result_raw_output(
|
||||
raw_output_result: std::result::Result<RawExecToolCallOutput, CodexErr>,
|
||||
sandbox_type: SandboxType,
|
||||
duration: Duration,
|
||||
) -> Result<ExecToolCallRawOutput> {
|
||||
match raw_output_result {
|
||||
Ok(raw_output) => {
|
||||
#[allow(unused_mut)]
|
||||
let mut timed_out = raw_output.timed_out;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
if let Some(signal) = raw_output.exit_status.signal() {
|
||||
if signal == TIMEOUT_CODE {
|
||||
timed_out = true;
|
||||
} else {
|
||||
return Err(CodexErr::Sandbox(SandboxErr::Signal(signal)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut exit_code = raw_output.exit_status.code().unwrap_or(-1);
|
||||
if timed_out {
|
||||
exit_code = EXEC_TIMEOUT_EXIT_CODE;
|
||||
}
|
||||
|
||||
let exec_output = ExecToolCallRawOutput {
|
||||
exit_code,
|
||||
stdout: raw_output.stdout,
|
||||
stderr: raw_output.stderr,
|
||||
aggregated_output: raw_output.aggregated_output,
|
||||
duration,
|
||||
timed_out,
|
||||
};
|
||||
|
||||
if timed_out {
|
||||
return Err(CodexErr::Sandbox(SandboxErr::Timeout {
|
||||
output: Box::new(exec_output.to_utf8_lossy_output()),
|
||||
}));
|
||||
}
|
||||
|
||||
let string_output = exec_output.to_utf8_lossy_output();
|
||||
if is_likely_sandbox_denied(sandbox_type, &string_output) {
|
||||
return Err(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output: Box::new(string_output),
|
||||
network_policy_decision: None,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(exec_output)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("exec error: {err}");
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod errors {
|
||||
use super::CodexErr;
|
||||
use crate::sandboxing::SandboxTransformError;
|
||||
@@ -741,6 +851,16 @@ pub struct ExecToolCallOutput {
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ExecToolCallRawOutput {
|
||||
pub exit_code: i32,
|
||||
pub stdout: StreamOutput<Vec<u8>>,
|
||||
pub stderr: StreamOutput<Vec<u8>>,
|
||||
pub aggregated_output: StreamOutput<Vec<u8>>,
|
||||
pub duration: Duration,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
impl Default for ExecToolCallOutput {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -754,6 +874,19 @@ impl Default for ExecToolCallOutput {
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecToolCallRawOutput {
|
||||
fn to_utf8_lossy_output(&self) -> ExecToolCallOutput {
|
||||
ExecToolCallOutput {
|
||||
exit_code: self.exit_code,
|
||||
stdout: self.stdout.from_utf8_lossy(),
|
||||
stderr: self.stderr.from_utf8_lossy(),
|
||||
aggregated_output: self.aggregated_output.from_utf8_lossy(),
|
||||
duration: self.duration,
|
||||
timed_out: self.timed_out,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
|
||||
async fn exec(
|
||||
params: ExecParams,
|
||||
|
||||
@@ -114,6 +114,7 @@ pub mod default_client;
|
||||
pub mod project_doc;
|
||||
mod rollout;
|
||||
pub(crate) mod safety;
|
||||
mod sandboxed_fs;
|
||||
pub mod seatbelt;
|
||||
pub mod shell;
|
||||
pub mod shell_snapshot;
|
||||
|
||||
143
codex-rs/core/src/sandboxed_fs.rs
Normal file
143
codex-rs/core/src/sandboxed_fs.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::exec::ExecToolCallRawOutput;
|
||||
use crate::sandboxing::CommandSpec;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::sandboxing::execute_env_raw_output;
|
||||
use crate::sandboxing::merge_permission_profiles;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use crate::tools::sandboxing::SandboxablePreference;
|
||||
use codex_fs_ops::CODEX_CORE_FS_OPS_ARG1;
|
||||
use codex_fs_ops::READ_FILE_OPERATION_ARG;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// An [crate::sandboxing::ExecRequest] must have an `expiration` set, so we
|
||||
/// specify a generous timeout for the operations specified by
|
||||
/// [SandboxedFsOperation].
|
||||
const SANDBOXED_FS_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Read the contents of the specified file subject to the sandbox constraints
|
||||
/// imposed by the provided session and turn context.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn read_file(
|
||||
path: AbsolutePathBuf,
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
) -> Result<Vec<u8>, SandboxedFsError> {
|
||||
let output = perform_operation(SandboxedFsOperation::Read { path }, session, turn).await?;
|
||||
Ok(output.stdout.text)
|
||||
}
|
||||
|
||||
/// Operations supported by the [CODEX_CORE_FS_OPS_ARG1] sandbox helper.
|
||||
enum SandboxedFsOperation {
|
||||
Read { path: AbsolutePathBuf },
|
||||
}
|
||||
|
||||
async fn perform_operation(
|
||||
operation: SandboxedFsOperation,
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
) -> Result<ExecToolCallRawOutput, SandboxedFsError> {
|
||||
let exe = std::env::current_exe().map_err(|error| SandboxedFsError::ResolveExe {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let additional_permissions = effective_granted_permissions(session).await;
|
||||
let sandbox_manager = crate::sandboxing::SandboxManager::new();
|
||||
let attempt = SandboxAttempt {
|
||||
sandbox: sandbox_manager.select_initial(
|
||||
&turn.file_system_sandbox_policy,
|
||||
turn.network_sandbox_policy,
|
||||
SandboxablePreference::Auto,
|
||||
turn.windows_sandbox_level,
|
||||
/*has_managed_network_requirements*/ false,
|
||||
),
|
||||
policy: &turn.sandbox_policy,
|
||||
file_system_policy: &turn.file_system_sandbox_policy,
|
||||
network_policy: turn.network_sandbox_policy,
|
||||
enforce_managed_network: false,
|
||||
manager: &sandbox_manager,
|
||||
sandbox_cwd: &turn.cwd,
|
||||
codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(),
|
||||
use_legacy_landlock: turn.features.use_legacy_landlock(),
|
||||
windows_sandbox_level: turn.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn.config.permissions.windows_sandbox_private_desktop,
|
||||
};
|
||||
|
||||
let args = match operation {
|
||||
SandboxedFsOperation::Read { ref path } => vec![
|
||||
CODEX_CORE_FS_OPS_ARG1.to_string(),
|
||||
READ_FILE_OPERATION_ARG.to_string(),
|
||||
path.to_string_lossy().to_string(),
|
||||
],
|
||||
};
|
||||
let exec_request = attempt
|
||||
.env_for(
|
||||
CommandSpec {
|
||||
program: exe.to_string_lossy().to_string(),
|
||||
args,
|
||||
cwd: turn.cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
expiration: ExecExpiration::Timeout(SANDBOXED_FS_TIMEOUT),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
additional_permissions,
|
||||
justification: None,
|
||||
},
|
||||
/*network*/ None,
|
||||
)
|
||||
.map_err(|error| SandboxedFsError::ProcessFailed {
|
||||
exit_code: -1,
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
|
||||
let output = execute_env_raw_output(exec_request, /*stdout_stream*/ None)
|
||||
.await
|
||||
.map_err(|error| SandboxedFsError::ProcessFailed {
|
||||
exit_code: 1,
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
if output.exit_code == 0 {
|
||||
Ok(output)
|
||||
} else {
|
||||
Err(parse_helper_failure(
|
||||
output.exit_code,
|
||||
&output.stderr.text,
|
||||
&output.stdout.text,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn effective_granted_permissions(session: &Session) -> Option<PermissionProfile> {
|
||||
let granted_session_permissions = session.granted_session_permissions().await;
|
||||
let granted_turn_permissions = session.granted_turn_permissions().await;
|
||||
merge_permission_profiles(
|
||||
granted_session_permissions.as_ref(),
|
||||
granted_turn_permissions.as_ref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_helper_failure(exit_code: i32, stderr: &[u8], stdout: &[u8]) -> SandboxedFsError {
|
||||
let stderr = String::from_utf8_lossy(stderr);
|
||||
let stdout = String::from_utf8_lossy(stdout);
|
||||
let message = if !stderr.trim().is_empty() {
|
||||
stderr.trim().to_string()
|
||||
} else if !stdout.trim().is_empty() {
|
||||
stdout.trim().to_string()
|
||||
} else {
|
||||
"no error details emitted".to_string()
|
||||
};
|
||||
|
||||
SandboxedFsError::ProcessFailed { exit_code, message }
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum SandboxedFsError {
|
||||
#[error("failed to determine codex executable: {message}")]
|
||||
ResolveExe { message: String },
|
||||
#[error("sandboxed fs helper exited with code {exit_code}: {message}")]
|
||||
ProcessFailed { exit_code: i32, message: String },
|
||||
}
|
||||
@@ -10,9 +10,11 @@ pub(crate) mod macos_permissions;
|
||||
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::exec::ExecToolCallRawOutput;
|
||||
use crate::exec::SandboxType;
|
||||
use crate::exec::StdoutStream;
|
||||
use crate::exec::execute_exec_request;
|
||||
use crate::exec::execute_exec_request_raw_output;
|
||||
use crate::landlock::allow_network_for_proxy;
|
||||
use crate::landlock::create_linux_sandbox_command_args_for_policies;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
@@ -738,6 +740,20 @@ pub async fn execute_env(
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_env_raw_output(
|
||||
exec_request: ExecRequest,
|
||||
stdout_stream: Option<StdoutStream>,
|
||||
) -> crate::error::Result<ExecToolCallRawOutput> {
|
||||
let effective_policy = exec_request.sandbox_policy.clone();
|
||||
execute_exec_request_raw_output(
|
||||
exec_request,
|
||||
&effective_policy,
|
||||
stdout_stream,
|
||||
/*after_spawn*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn execute_exec_request_with_after_spawn(
|
||||
exec_request: ExecRequest,
|
||||
stdout_stream: Option<StdoutStream>,
|
||||
|
||||
6
codex-rs/fs-ops/BUILD.bazel
Normal file
6
codex-rs/fs-ops/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "fs-ops",
|
||||
crate_name = "codex_fs_ops",
|
||||
)
|
||||
16
codex-rs/fs-ops/Cargo.toml
Normal file
16
codex-rs/fs-ops/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "codex-fs-ops"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_fs_ops"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
38
codex-rs/fs-ops/src/command.rs
Normal file
38
codex-rs/fs-ops/src/command.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const READ_FILE_OPERATION_ARG: &str = "read";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FsCommand {
|
||||
ReadFile { path: PathBuf },
|
||||
}
|
||||
|
||||
pub fn parse_command_from_args(
|
||||
mut args: impl Iterator<Item = OsString>,
|
||||
) -> Result<FsCommand, String> {
|
||||
let Some(operation) = args.next() else {
|
||||
return Err("missing operation".to_string());
|
||||
};
|
||||
let Some(operation) = operation.to_str() else {
|
||||
return Err("operation must be valid UTF-8".to_string());
|
||||
};
|
||||
let Some(path) = args.next() else {
|
||||
return Err(format!("missing path for operation `{operation}`"));
|
||||
};
|
||||
if args.next().is_some() {
|
||||
return Err(format!(
|
||||
"unexpected extra arguments for operation `{operation}`"
|
||||
));
|
||||
}
|
||||
|
||||
let path = PathBuf::from(path);
|
||||
match operation {
|
||||
READ_FILE_OPERATION_ARG => Ok(FsCommand::ReadFile { path }),
|
||||
_ => Err(format!("unsupported filesystem operation `{operation}`")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "command_tests.rs"]
|
||||
mod tests;
|
||||
21
codex-rs/fs-ops/src/command_tests.rs
Normal file
21
codex-rs/fs-ops/src/command_tests.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use super::FsCommand;
|
||||
use super::READ_FILE_OPERATION_ARG;
|
||||
use super::parse_command_from_args;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse_read_command() {
|
||||
let command = parse_command_from_args(
|
||||
[READ_FILE_OPERATION_ARG, "/tmp/example.png"]
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
)
|
||||
.expect("command should parse");
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
FsCommand::ReadFile {
|
||||
path: "/tmp/example.png".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
3
codex-rs/fs-ops/src/constants.rs
Normal file
3
codex-rs/fs-ops/src/constants.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
/// Special argv[1] flag used when the Codex executable self-invokes to run the
|
||||
/// internal sandbox-backed filesystem helper path.
|
||||
pub const CODEX_CORE_FS_OPS_ARG1: &str = "--codex-run-as-fs-ops";
|
||||
7
codex-rs/fs-ops/src/lib.rs
Normal file
7
codex-rs/fs-ops/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod command;
|
||||
mod constants;
|
||||
mod runner;
|
||||
|
||||
pub use command::READ_FILE_OPERATION_ARG;
|
||||
pub use constants::CODEX_CORE_FS_OPS_ARG1;
|
||||
pub use runner::run_from_args;
|
||||
37
codex-rs/fs-ops/src/runner.rs
Normal file
37
codex-rs/fs-ops/src/runner.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::command::FsCommand;
|
||||
use crate::command::parse_command_from_args;
|
||||
use std::ffi::OsString;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
|
||||
pub fn run_from_args(
|
||||
args: impl Iterator<Item = OsString>,
|
||||
stdin: &mut impl Read,
|
||||
stdout: &mut impl Write,
|
||||
stderr: &mut impl Write,
|
||||
) -> std::io::Result<()> {
|
||||
let command = parse_command_from_args(args)
|
||||
.inspect_err(|error| {
|
||||
writeln!(stderr, "{error}").ok();
|
||||
})
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
execute(command, stdin, stdout)
|
||||
}
|
||||
|
||||
fn execute(
|
||||
command: FsCommand,
|
||||
_stdin: &mut impl Read,
|
||||
stdout: &mut impl Write,
|
||||
) -> std::io::Result<()> {
|
||||
match command {
|
||||
FsCommand::ReadFile { path } => {
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
std::io::copy(&mut file, stdout).map(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "runner_tests.rs"]
|
||||
mod tests;
|
||||
78
codex-rs/fs-ops/src/runner_tests.rs
Normal file
78
codex-rs/fs-ops/src/runner_tests.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use super::FsCommand;
|
||||
use super::execute;
|
||||
use crate::READ_FILE_OPERATION_ARG;
|
||||
use crate::run_from_args;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn run_from_args_streams_file_bytes_to_stdout() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
let path = tempdir.path().join("image.bin");
|
||||
let expected = b"hello\x00world".to_vec();
|
||||
std::fs::write(&path, &expected).expect("write test file");
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let mut stdin = std::io::empty();
|
||||
run_from_args(
|
||||
[
|
||||
READ_FILE_OPERATION_ARG,
|
||||
path.to_str().expect("utf-8 test path"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
&mut stdin,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
)
|
||||
.expect("read should succeed");
|
||||
|
||||
assert_eq!(stdout, expected);
|
||||
assert_eq!(stderr, Vec::<u8>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_reports_directory_error() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
let mut stdout = Vec::new();
|
||||
let mut stdin = std::io::empty();
|
||||
|
||||
let error = execute(
|
||||
FsCommand::ReadFile {
|
||||
path: tempdir.path().to_path_buf(),
|
||||
},
|
||||
&mut stdin,
|
||||
&mut stdout,
|
||||
)
|
||||
.expect_err("reading a directory should fail");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::IsADirectory);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_from_args_serializes_errors_to_stderr() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
let missing = tempdir.path().join("missing.txt");
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let mut stdin = std::io::empty();
|
||||
|
||||
let result = run_from_args(
|
||||
[
|
||||
READ_FILE_OPERATION_ARG,
|
||||
missing.to_str().expect("utf-8 test path"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
&mut stdin,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
);
|
||||
|
||||
assert!(result.is_err(), "missing file should fail");
|
||||
assert_eq!(stdout, Vec::<u8>::new());
|
||||
}
|
||||
Reference in New Issue
Block a user