mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
apply-patch: carry paths as PathUri (#28854)
## Why Allows the model to edit files that are hosted on a different OS than where app-server is running. ## What * Use `PathUri` for apply_patch-internal data structures * Limit `PathUri` -> `AbsolutePathBuf` conversion to cases where the inferred path convention matches the host OS, allows requiring valid paths to pass to perms check * Adds `PathConvention::path_segments()` for iterating over path segments regardless of OS * Handle cross-platform relative paths in path filename parsing for sniffing a shell * Ensure we can apply patches in the wine e2e test
This commit is contained in:
committed by
GitHub
Unverified
parent
a52a3b5197
commit
0f89dd768c
@@ -1,9 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use tree_sitter::Parser;
|
||||
use tree_sitter::Query;
|
||||
use tree_sitter::QueryCursor;
|
||||
@@ -21,6 +19,7 @@ use crate::parser::Hunk;
|
||||
use crate::parser::ParseError;
|
||||
use crate::parser::parse_patch;
|
||||
use crate::unified_diff_from_chunks;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::str::Utf8Error;
|
||||
use tree_sitter::LanguageError;
|
||||
@@ -51,15 +50,17 @@ pub enum ExtractHeredocError {
|
||||
FailedToFindHeredocBody,
|
||||
}
|
||||
|
||||
fn classify_shell_name(shell: &str) -> Option<String> {
|
||||
std::path::Path::new(shell)
|
||||
.file_stem()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(str::to_ascii_lowercase)
|
||||
fn classify_shell_name(shell: &str, convention: PathConvention) -> Option<String> {
|
||||
let basename = convention.path_segments(shell).next_back()?;
|
||||
let stem = basename
|
||||
.rsplit_once('.')
|
||||
.and_then(|(stem, _extension)| (!stem.is_empty()).then_some(stem))
|
||||
.unwrap_or(basename);
|
||||
Some(stem.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn classify_shell(shell: &str, flag: &str) -> Option<ApplyPatchShell> {
|
||||
classify_shell_name(shell).and_then(|name| match name.as_str() {
|
||||
fn classify_shell(shell: &str, flag: &str, convention: PathConvention) -> Option<ApplyPatchShell> {
|
||||
classify_shell_name(shell, convention).and_then(|name| match name.as_str() {
|
||||
"bash" | "zsh" | "sh" if matches!(flag, "-lc" | "-c") => Some(ApplyPatchShell::Unix),
|
||||
"pwsh" | "powershell" if flag.eq_ignore_ascii_case("-command") => {
|
||||
Some(ApplyPatchShell::PowerShell)
|
||||
@@ -69,20 +70,24 @@ fn classify_shell(shell: &str, flag: &str) -> Option<ApplyPatchShell> {
|
||||
})
|
||||
}
|
||||
|
||||
fn can_skip_flag(shell: &str, flag: &str) -> bool {
|
||||
classify_shell_name(shell).is_some_and(|name| {
|
||||
fn can_skip_flag(shell: &str, flag: &str, convention: PathConvention) -> bool {
|
||||
classify_shell_name(shell, convention).is_some_and(|name| {
|
||||
matches!(name.as_str(), "pwsh" | "powershell") && flag.eq_ignore_ascii_case("-noprofile")
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_shell_script(argv: &[String]) -> Option<(ApplyPatchShell, &str)> {
|
||||
fn parse_shell_script<'a>(argv: &'a [String], cwd: &PathUri) -> Option<(ApplyPatchShell, &'a str)> {
|
||||
let convention = cwd.infer_path_convention()?;
|
||||
match argv {
|
||||
[shell, flag, script] => classify_shell(shell, flag).map(|shell_type| {
|
||||
[shell, flag, script] => classify_shell(shell, flag, convention).map(|shell_type| {
|
||||
let script = script.as_str();
|
||||
(shell_type, script)
|
||||
}),
|
||||
[shell, skip_flag, flag, script] if can_skip_flag(shell, skip_flag) => {
|
||||
classify_shell(shell, flag).map(|shell_type| {
|
||||
[shell, skip_flag, flag, script] => {
|
||||
if !can_skip_flag(shell, skip_flag, convention) {
|
||||
return None;
|
||||
}
|
||||
classify_shell(shell, flag, convention).map(|shell_type| {
|
||||
let script = script.as_str();
|
||||
(shell_type, script)
|
||||
})
|
||||
@@ -103,7 +108,8 @@ fn extract_apply_patch_from_shell(
|
||||
}
|
||||
|
||||
// TODO: make private once we remove tests in lib.rs
|
||||
pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
||||
/// `cwd` supplies the path convention used to interpret the shell executable in `argv`.
|
||||
pub fn maybe_parse_apply_patch(argv: &[String], cwd: &PathUri) -> MaybeApplyPatch {
|
||||
match argv {
|
||||
// Direct invocation: apply_patch <patch>
|
||||
[cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => match parse_patch(body) {
|
||||
@@ -111,7 +117,7 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||
},
|
||||
// Shell heredoc form: (optional `cd <path> &&`) apply_patch <<'EOF' ...
|
||||
_ => match parse_shell_script(argv) {
|
||||
_ => match parse_shell_script(argv, cwd) {
|
||||
Some((shell, script)) => match extract_apply_patch_from_shell(shell, script) {
|
||||
Ok((body, workdir)) => match parse_patch(&body) {
|
||||
Ok(mut source) => {
|
||||
@@ -130,11 +136,11 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
||||
}
|
||||
}
|
||||
|
||||
/// cwd must be an absolute path so that we can resolve relative paths in the
|
||||
/// patch.
|
||||
/// `cwd` must identify an absolute environment-native path so relative patch paths can be
|
||||
/// resolved without projecting them onto the app-server or exec-server host.
|
||||
pub async fn maybe_parse_apply_patch_verified(
|
||||
argv: &[String],
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&codex_exec_server::FileSystemSandboxContext>,
|
||||
) -> MaybeApplyPatchVerified {
|
||||
@@ -145,13 +151,13 @@ pub async fn maybe_parse_apply_patch_verified(
|
||||
{
|
||||
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
|
||||
}
|
||||
if let Some((_, script)) = parse_shell_script(argv)
|
||||
if let Some((_, script)) = parse_shell_script(argv, cwd)
|
||||
&& parse_patch(script).is_ok()
|
||||
{
|
||||
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
|
||||
}
|
||||
|
||||
match maybe_parse_apply_patch(argv) {
|
||||
match maybe_parse_apply_patch(argv, cwd) {
|
||||
MaybeApplyPatch::Body(args) => verify_apply_patch_args(args, cwd, fs, sandbox).await,
|
||||
MaybeApplyPatch::ShellParseError(e) => MaybeApplyPatchVerified::ShellParseError(e),
|
||||
MaybeApplyPatch::PatchParseError(e) => MaybeApplyPatchVerified::CorrectnessError(e.into()),
|
||||
@@ -161,10 +167,22 @@ pub async fn maybe_parse_apply_patch_verified(
|
||||
|
||||
pub async fn verify_apply_patch_args(
|
||||
args: ApplyPatchArgs,
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&codex_exec_server::FileSystemSandboxContext>,
|
||||
) -> MaybeApplyPatchVerified {
|
||||
match try_verify_apply_patch_args(args, cwd, fs, sandbox).await {
|
||||
Ok(action) => MaybeApplyPatchVerified::Body(action),
|
||||
Err(err) => MaybeApplyPatchVerified::CorrectnessError(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_verify_apply_patch_args(
|
||||
args: ApplyPatchArgs,
|
||||
cwd: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&codex_exec_server::FileSystemSandboxContext>,
|
||||
) -> Result<ApplyPatchAction, ApplyPatchError> {
|
||||
let ApplyPatchArgs {
|
||||
patch,
|
||||
hunks,
|
||||
@@ -173,35 +191,24 @@ pub async fn verify_apply_patch_args(
|
||||
} = args;
|
||||
let effective_cwd = workdir
|
||||
.as_ref()
|
||||
.map(|dir| cwd.join(Path::new(dir)))
|
||||
.map(|dir| cwd.join(dir))
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
let mut changes = HashMap::new();
|
||||
for hunk in hunks {
|
||||
let path = hunk.resolve_path(&effective_cwd);
|
||||
let path = hunk.resolve_path(&effective_cwd)?;
|
||||
match hunk {
|
||||
Hunk::AddFile { contents, .. } => {
|
||||
changes.insert(
|
||||
path.into_path_buf(),
|
||||
ApplyPatchFileChange::Add { content: contents },
|
||||
);
|
||||
changes.insert(path, ApplyPatchFileChange::Add { content: contents });
|
||||
}
|
||||
Hunk::DeleteFile { .. } => {
|
||||
let path_uri = PathUri::from_abs_path(&path);
|
||||
let content = match fs.read_file_text(&path_uri, sandbox).await {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
return MaybeApplyPatchVerified::CorrectnessError(
|
||||
ApplyPatchError::IoError(IoError {
|
||||
context: format!("Failed to read {}", path.display()),
|
||||
source: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
changes.insert(
|
||||
path.into_path_buf(),
|
||||
ApplyPatchFileChange::Delete { content },
|
||||
);
|
||||
let content = fs.read_file_text(&path, sandbox).await.map_err(|source| {
|
||||
ApplyPatchError::IoError(IoError {
|
||||
context: format!("Failed to read {}", path.inferred_native_path_string()),
|
||||
source,
|
||||
})
|
||||
})?;
|
||||
changes.insert(path, ApplyPatchFileChange::Delete { content });
|
||||
}
|
||||
Hunk::UpdateFile {
|
||||
move_path, chunks, ..
|
||||
@@ -210,27 +217,24 @@ pub async fn verify_apply_patch_args(
|
||||
unified_diff,
|
||||
content: contents,
|
||||
..
|
||||
} = match unified_diff_from_chunks(&path, &chunks, fs, sandbox).await {
|
||||
Ok(diff) => diff,
|
||||
Err(e) => {
|
||||
return MaybeApplyPatchVerified::CorrectnessError(e);
|
||||
}
|
||||
};
|
||||
} = unified_diff_from_chunks(&path, &chunks, fs, sandbox).await?;
|
||||
changes.insert(
|
||||
path.into_path_buf(),
|
||||
path,
|
||||
ApplyPatchFileChange::Update {
|
||||
unified_diff,
|
||||
move_path: move_path.map(|p| effective_cwd.join(p).into_path_buf()),
|
||||
move_path: move_path
|
||||
.map(|path| effective_cwd.join(&path.to_string_lossy()))
|
||||
.transpose()?,
|
||||
new_content: contents,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
MaybeApplyPatchVerified::Body(ApplyPatchAction {
|
||||
Ok(ApplyPatchAction {
|
||||
changes,
|
||||
patch,
|
||||
cwd: effective_cwd.into(),
|
||||
cwd: effective_cwd,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -392,7 +396,6 @@ mod tests {
|
||||
use crate::unified_diff_from_chunks;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -448,8 +451,22 @@ mod tests {
|
||||
}]
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_match_args(args: Vec<String>, expected_workdir: Option<&str>) {
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
assert_match_args_with_cwd(
|
||||
args,
|
||||
&PathUri::parse("file:///workspace").expect("valid POSIX test cwd"),
|
||||
expected_workdir,
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_match_args_with_cwd(
|
||||
args: Vec<String>,
|
||||
cwd: &PathUri,
|
||||
expected_workdir: Option<&str>,
|
||||
) {
|
||||
match maybe_parse_apply_patch(&args, cwd) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, workdir, .. }) => {
|
||||
assert_eq!(workdir.as_deref(), expected_workdir);
|
||||
assert_eq!(hunks, expected_single_add());
|
||||
@@ -458,6 +475,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_match(script: &str, expected_workdir: Option<&str>) {
|
||||
let args = args_bash(script);
|
||||
assert_match_args(args, expected_workdir);
|
||||
@@ -466,7 +484,10 @@ mod tests {
|
||||
fn assert_not_match(script: &str) {
|
||||
let args = args_bash(script);
|
||||
assert_matches!(
|
||||
maybe_parse_apply_patch(&args),
|
||||
maybe_parse_apply_patch(
|
||||
&args,
|
||||
&PathUri::parse("file:///workspace").expect("valid POSIX test cwd"),
|
||||
),
|
||||
MaybeApplyPatch::NotApplyPatch
|
||||
);
|
||||
}
|
||||
@@ -479,7 +500,7 @@ mod tests {
|
||||
assert_matches!(
|
||||
maybe_parse_apply_patch_verified(
|
||||
&args,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
@@ -496,7 +517,7 @@ mod tests {
|
||||
assert_matches!(
|
||||
maybe_parse_apply_patch_verified(
|
||||
&args,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
@@ -516,7 +537,10 @@ mod tests {
|
||||
"#,
|
||||
]);
|
||||
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
match maybe_parse_apply_patch(
|
||||
&args,
|
||||
&PathUri::parse("file:///workspace").expect("valid POSIX test cwd"),
|
||||
) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, .. }) => {
|
||||
assert_eq!(
|
||||
hunks,
|
||||
@@ -541,7 +565,10 @@ mod tests {
|
||||
"#,
|
||||
]);
|
||||
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
match maybe_parse_apply_patch(
|
||||
&args,
|
||||
&PathUri::parse("file:///workspace").expect("valid POSIX test cwd"),
|
||||
) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, .. }) => {
|
||||
assert_eq!(
|
||||
hunks,
|
||||
@@ -580,7 +607,10 @@ mod tests {
|
||||
PATCH"#,
|
||||
]);
|
||||
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
match maybe_parse_apply_patch(
|
||||
&args,
|
||||
&PathUri::parse("file:///workspace").expect("valid POSIX test cwd"),
|
||||
) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, workdir, .. }) => {
|
||||
assert_eq!(workdir, None);
|
||||
assert_eq!(
|
||||
@@ -614,6 +644,21 @@ PATCH"#,
|
||||
assert_match_args(args_pwsh(&script), /*expected_workdir*/ None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_apply_patch_interception_uses_cwd_convention_for_windows_pwsh_path() {
|
||||
let script = heredoc_script("");
|
||||
assert_match_args_with_cwd(
|
||||
strs_to_strings(&[
|
||||
r"C:\Program Files\PowerShell\7\pwsh.exe",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
&script,
|
||||
]),
|
||||
&PathUri::parse("file:///C:/windows").expect("valid Windows test cwd"),
|
||||
/*expected_workdir*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cmd_heredoc_with_cd() {
|
||||
let script = heredoc_script("cd foo && ");
|
||||
@@ -707,9 +752,9 @@ PATCH"#,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
};
|
||||
|
||||
let path_abs = path.as_path().abs();
|
||||
let path_uri = PathUri::from_path(&path).expect("absolute test path");
|
||||
let diff =
|
||||
unified_diff_from_chunks(&path_abs, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
unified_diff_from_chunks(&path_uri, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected_diff = r#"@@ -2,2 +2,2 @@
|
||||
@@ -747,9 +792,9 @@ PATCH"#,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
};
|
||||
|
||||
let path_abs = path.as_path().abs();
|
||||
let path_uri = PathUri::from_path(&path).expect("absolute test path");
|
||||
let diff =
|
||||
unified_diff_from_chunks(&path_abs, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
unified_diff_from_chunks(&path_uri, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected_diff = r#"@@ -3 +3,2 @@
|
||||
@@ -787,7 +832,7 @@ PATCH"#,
|
||||
|
||||
let result = maybe_parse_apply_patch_verified(
|
||||
&argv,
|
||||
&AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
|
||||
&PathUri::from_path(session_dir.path()).expect("absolute test path"),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
@@ -799,7 +844,8 @@ PATCH"#,
|
||||
result,
|
||||
MaybeApplyPatchVerified::Body(ApplyPatchAction {
|
||||
changes: HashMap::from([(
|
||||
session_dir.path().join(relative_path),
|
||||
PathUri::from_path(session_dir.path().join(relative_path))
|
||||
.expect("absolute test path"),
|
||||
ApplyPatchFileChange::Update {
|
||||
unified_diff: r#"@@ -1 +1 @@
|
||||
-session directory content
|
||||
@@ -811,9 +857,7 @@ PATCH"#,
|
||||
},
|
||||
)]),
|
||||
patch: argv[1].clone(),
|
||||
cwd: AbsolutePathBuf::from_absolute_path(session_dir.path())
|
||||
.unwrap()
|
||||
.into(),
|
||||
cwd: PathUri::from_path(session_dir.path()).expect("absolute test path"),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -843,7 +887,7 @@ PATCH"#,
|
||||
|
||||
let result = maybe_parse_apply_patch_verified(
|
||||
&argv,
|
||||
&AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
|
||||
&PathUri::from_path(session_dir.path()).expect("absolute test path"),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
@@ -858,18 +902,18 @@ PATCH"#,
|
||||
worktree_dir.as_path()
|
||||
);
|
||||
|
||||
let source_path = worktree_dir.join(source_name);
|
||||
let source_path =
|
||||
PathUri::from_path(worktree_dir.join(source_name)).expect("absolute test path");
|
||||
let change = action
|
||||
.changes()
|
||||
.get(source_path.as_path())
|
||||
.get(&source_path)
|
||||
.expect("source file change present");
|
||||
|
||||
match change {
|
||||
ApplyPatchFileChange::Update { move_path, .. } => {
|
||||
assert_eq!(
|
||||
move_path.as_deref(),
|
||||
Some(worktree_dir.join(dest_name).as_path())
|
||||
);
|
||||
let expected_move_path =
|
||||
PathUri::from_path(worktree_dir.join(dest_name)).expect("absolute test path");
|
||||
assert_eq!(move_path.as_ref(), Some(&expected_move_path));
|
||||
}
|
||||
other => panic!("expected update change, got {other:?}"),
|
||||
}
|
||||
@@ -879,7 +923,7 @@ PATCH"#,
|
||||
async fn test_unreadable_destinations_still_verify() {
|
||||
let session_dir = tempdir().unwrap();
|
||||
fs::write(session_dir.path().join("binary.dat"), [0xff, 0xfe, 0xfd]).unwrap();
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap();
|
||||
let cwd = PathUri::from_path(session_dir.path()).expect("absolute test path");
|
||||
let add_argv = vec![
|
||||
"apply_patch".to_string(),
|
||||
"*** Begin Patch\n*** Add File: binary.dat\n+text\n*** End Patch".to_string(),
|
||||
@@ -922,7 +966,7 @@ PATCH"#,
|
||||
|
||||
let result = maybe_parse_apply_patch_verified(
|
||||
&argv,
|
||||
&AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
|
||||
&PathUri::from_path(session_dir.path()).expect("absolute test path"),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
|
||||
+137
-116
@@ -6,7 +6,6 @@ mod streaming_parser;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -15,8 +14,8 @@ use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::FileSystemSandboxContext;
|
||||
use codex_exec_server::RemoveOptions;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use codex_utils_path_uri::PathUriParseError;
|
||||
pub use parser::Hunk;
|
||||
pub use parser::ParseError;
|
||||
use parser::ParseError::*;
|
||||
@@ -50,6 +49,9 @@ pub enum ApplyPatchError {
|
||||
/// Error that occurs while computing replacements when applying patch chunks
|
||||
#[error("{0}")]
|
||||
ComputeReplacements(String),
|
||||
/// A patch path could not be resolved as a path URI.
|
||||
#[error(transparent)]
|
||||
PathUri(#[from] PathUriParseError),
|
||||
/// A raw patch body was provided without an explicit `apply_patch` invocation.
|
||||
#[error(
|
||||
"patch detected without explicit call to apply_patch. Rerun as [\"apply_patch\", \"<patch>\"]"
|
||||
@@ -109,7 +111,7 @@ pub enum ApplyPatchFileChange {
|
||||
},
|
||||
Update {
|
||||
unified_diff: String,
|
||||
move_path: Option<PathBuf>,
|
||||
move_path: Option<PathUri>,
|
||||
/// new_content that will result after the unified_diff is applied.
|
||||
new_content: String,
|
||||
},
|
||||
@@ -134,7 +136,7 @@ pub enum MaybeApplyPatchVerified {
|
||||
/// construction, all paths should be absolute paths.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ApplyPatchAction {
|
||||
changes: HashMap<PathBuf, ApplyPatchFileChange>,
|
||||
changes: HashMap<PathUri, ApplyPatchFileChange>,
|
||||
|
||||
/// The raw patch argument that can be used to apply the patch. i.e., if the
|
||||
/// original arg was parsed in "lenient" mode with a
|
||||
@@ -151,18 +153,15 @@ impl ApplyPatchAction {
|
||||
}
|
||||
|
||||
/// Returns the changes that would be made by applying the patch.
|
||||
pub fn changes(&self) -> &HashMap<PathBuf, ApplyPatchFileChange> {
|
||||
pub fn changes(&self) -> &HashMap<PathUri, ApplyPatchFileChange> {
|
||||
&self.changes
|
||||
}
|
||||
|
||||
/// Should be used exclusively for testing. (Not worth the overhead of
|
||||
/// creating a feature flag for this.)
|
||||
pub fn new_add_for_test(path: &AbsolutePathBuf, content: String) -> Self {
|
||||
pub fn new_add_for_test(path: &PathUri, content: String) -> Self {
|
||||
#[expect(clippy::expect_used)]
|
||||
let filename = path
|
||||
.file_name()
|
||||
.expect("path should not be empty")
|
||||
.to_string_lossy();
|
||||
let filename = path.basename().expect("path should not be empty");
|
||||
let patch = format!(
|
||||
r#"*** Begin Patch
|
||||
*** Update File: {filename}
|
||||
@@ -170,11 +169,11 @@ impl ApplyPatchAction {
|
||||
+ {content}
|
||||
*** End Patch"#,
|
||||
);
|
||||
let changes = HashMap::from([(path.to_path_buf(), ApplyPatchFileChange::Add { content })]);
|
||||
let changes = HashMap::from([(path.clone(), ApplyPatchFileChange::Add { content })]);
|
||||
#[expect(clippy::expect_used)]
|
||||
Self {
|
||||
changes,
|
||||
cwd: path.parent().expect("path should have parent").into(),
|
||||
cwd: path.parent().expect("path should have parent"),
|
||||
patch,
|
||||
}
|
||||
}
|
||||
@@ -276,7 +275,7 @@ impl ApplyPatchFailure {
|
||||
/// Applies the patch and prints the result to stdout/stderr.
|
||||
pub async fn apply_patch(
|
||||
patch: &str,
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
stdout: &mut impl std::io::Write,
|
||||
stderr: &mut impl std::io::Write,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
@@ -315,7 +314,7 @@ pub async fn apply_patch(
|
||||
/// Applies hunks and continues to update stdout/stderr
|
||||
pub async fn apply_hunks(
|
||||
hunks: &[Hunk],
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
stdout: &mut impl std::io::Write,
|
||||
stderr: &mut impl std::io::Write,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
@@ -361,7 +360,7 @@ pub struct AffectedPaths {
|
||||
/// Returns an error if the patch could not be applied.
|
||||
async fn apply_hunks_to_files(
|
||||
hunks: &[Hunk],
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
delta: &mut AppliedPatchDelta,
|
||||
@@ -388,26 +387,26 @@ async fn apply_hunks_to_files(
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(anp): Carry PathUri through committed patch deltas and the turn diff tracker.
|
||||
for hunk in hunks {
|
||||
let affected_path = hunk.path().to_path_buf();
|
||||
let path_abs = hunk.resolve_path(cwd);
|
||||
let path_uri = PathUri::from_abs_path(&path_abs);
|
||||
let path_uri = hunk.resolve_path(cwd)?;
|
||||
match hunk {
|
||||
Hunk::AddFile { contents, .. } => {
|
||||
let overwritten_content =
|
||||
read_optional_file_text_for_delta(&path_abs, fs, sandbox, &mut delta.exact)
|
||||
read_optional_file_text_for_delta(&path_uri, fs, sandbox, &mut delta.exact)
|
||||
.await;
|
||||
try_write!(
|
||||
write_file_with_missing_parent_retry(
|
||||
fs,
|
||||
&path_abs,
|
||||
&path_uri,
|
||||
contents.clone().into_bytes(),
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
);
|
||||
delta.changes.push(AppliedPatchChange {
|
||||
path: path_abs.into_path_buf(),
|
||||
path: path_uri.to_path_buf(),
|
||||
change: AppliedPatchFileChange::Add {
|
||||
content: contents.clone(),
|
||||
overwritten_content,
|
||||
@@ -416,14 +415,19 @@ async fn apply_hunks_to_files(
|
||||
added.push(affected_path);
|
||||
}
|
||||
Hunk::DeleteFile { .. } => {
|
||||
note_existing_path_delta_support(&path_abs, fs, sandbox, &mut delta.exact).await;
|
||||
note_existing_path_delta_support(&path_uri, fs, sandbox, &mut delta.exact).await;
|
||||
let deleted_content = fs.read_file_text(&path_uri, sandbox).await.ok();
|
||||
if deleted_content.is_none() {
|
||||
delta.exact = false;
|
||||
}
|
||||
ensure_not_directory(&path_abs, fs, sandbox)
|
||||
ensure_not_directory(&path_uri, fs, sandbox)
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete file {}", path_abs.display()))?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to delete file {}",
|
||||
path_uri.inferred_native_path_string()
|
||||
)
|
||||
})?;
|
||||
if let Err(error) = fs
|
||||
.remove(
|
||||
&path_uri,
|
||||
@@ -434,10 +438,15 @@ async fn apply_hunks_to_files(
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete file {}", path_abs.display()))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to delete file {}",
|
||||
path_uri.inferred_native_path_string()
|
||||
)
|
||||
})
|
||||
{
|
||||
delta.exact &= remove_failure_was_side_effect_free(
|
||||
&path_abs,
|
||||
&path_uri,
|
||||
deleted_content.as_deref(),
|
||||
fs,
|
||||
sandbox,
|
||||
@@ -447,7 +456,7 @@ async fn apply_hunks_to_files(
|
||||
}
|
||||
if let Some(content) = deleted_content {
|
||||
delta.changes.push(AppliedPatchChange {
|
||||
path: path_abs.into_path_buf(),
|
||||
path: path_uri.to_path_buf(),
|
||||
change: AppliedPatchFileChange::Delete { content },
|
||||
});
|
||||
}
|
||||
@@ -456,20 +465,20 @@ async fn apply_hunks_to_files(
|
||||
Hunk::UpdateFile {
|
||||
move_path, chunks, ..
|
||||
} => {
|
||||
note_existing_path_delta_support(&path_abs, fs, sandbox, &mut delta.exact).await;
|
||||
note_existing_path_delta_support(&path_uri, fs, sandbox, &mut delta.exact).await;
|
||||
let AppliedPatch {
|
||||
original_contents,
|
||||
new_contents,
|
||||
} = derive_new_contents_from_chunks(&path_abs, chunks, fs, sandbox).await?;
|
||||
} = derive_new_contents_from_chunks(&path_uri, chunks, fs, sandbox).await?;
|
||||
if let Some(dest) = move_path {
|
||||
let dest_abs = AbsolutePathBuf::resolve_path_against_base(dest, cwd);
|
||||
let dest_uri = cwd.join(&dest.to_string_lossy())?;
|
||||
let overwritten_move_content =
|
||||
read_optional_file_text_for_delta(&dest_abs, fs, sandbox, &mut delta.exact)
|
||||
read_optional_file_text_for_delta(&dest_uri, fs, sandbox, &mut delta.exact)
|
||||
.await;
|
||||
try_write!(
|
||||
write_file_with_missing_parent_retry(
|
||||
fs,
|
||||
&dest_abs,
|
||||
&dest_uri,
|
||||
new_contents.clone().into_bytes(),
|
||||
sandbox,
|
||||
)
|
||||
@@ -477,16 +486,19 @@ async fn apply_hunks_to_files(
|
||||
);
|
||||
let dest_write_change_index = delta.changes.len();
|
||||
delta.changes.push(AppliedPatchChange {
|
||||
path: dest_abs.to_path_buf(),
|
||||
path: dest_uri.to_path_buf(),
|
||||
change: AppliedPatchFileChange::Add {
|
||||
content: new_contents.clone(),
|
||||
overwritten_content: overwritten_move_content.clone(),
|
||||
},
|
||||
});
|
||||
ensure_not_directory(&path_abs, fs, sandbox)
|
||||
ensure_not_directory(&path_uri, fs, sandbox)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to remove original {}", path_abs.display())
|
||||
format!(
|
||||
"Failed to remove original {}",
|
||||
path_uri.inferred_native_path_string()
|
||||
)
|
||||
})?;
|
||||
if let Err(error) = fs
|
||||
.remove(
|
||||
@@ -499,11 +511,14 @@ async fn apply_hunks_to_files(
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to remove original {}", path_abs.display())
|
||||
format!(
|
||||
"Failed to remove original {}",
|
||||
path_uri.inferred_native_path_string()
|
||||
)
|
||||
})
|
||||
{
|
||||
delta.exact &= remove_failure_was_side_effect_free(
|
||||
&path_abs,
|
||||
&path_uri,
|
||||
Some(&original_contents),
|
||||
fs,
|
||||
sandbox,
|
||||
@@ -512,9 +527,9 @@ async fn apply_hunks_to_files(
|
||||
return Err(error);
|
||||
}
|
||||
delta.changes[dest_write_change_index] = AppliedPatchChange {
|
||||
path: path_abs.into_path_buf(),
|
||||
path: path_uri.to_path_buf(),
|
||||
change: AppliedPatchFileChange::Update {
|
||||
move_path: Some(dest_abs.into_path_buf()),
|
||||
move_path: Some(dest_uri.to_path_buf()),
|
||||
old_content: original_contents,
|
||||
overwritten_move_content,
|
||||
new_content: new_contents,
|
||||
@@ -527,11 +542,11 @@ async fn apply_hunks_to_files(
|
||||
.await
|
||||
.with_context(|| format!(
|
||||
"Failed to write file {}",
|
||||
path_abs.display()
|
||||
path_uri.inferred_native_path_string()
|
||||
))
|
||||
);
|
||||
delta.changes.push(AppliedPatchChange {
|
||||
path: path_abs.into_path_buf(),
|
||||
path: path_uri.to_path_buf(),
|
||||
change: AppliedPatchFileChange::Update {
|
||||
move_path: None,
|
||||
old_content: original_contents,
|
||||
@@ -552,12 +567,11 @@ async fn apply_hunks_to_files(
|
||||
}
|
||||
|
||||
async fn ensure_not_directory(
|
||||
path: &AbsolutePathBuf,
|
||||
path: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> io::Result<()> {
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
let metadata = fs.get_metadata(&path_uri, sandbox).await?;
|
||||
let metadata = fs.get_metadata(path, sandbox).await?;
|
||||
if metadata.is_directory {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
@@ -568,15 +582,14 @@ async fn ensure_not_directory(
|
||||
}
|
||||
|
||||
async fn remove_failure_was_side_effect_free(
|
||||
path: &AbsolutePathBuf,
|
||||
path: &PathUri,
|
||||
expected_content: Option<&str>,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> bool {
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
match expected_content {
|
||||
Some(expected_content) => fs
|
||||
.read_file_text(&path_uri, sandbox)
|
||||
.read_file_text(path, sandbox)
|
||||
.await
|
||||
.is_ok_and(|content| content == expected_content),
|
||||
None => false,
|
||||
@@ -584,14 +597,13 @@ async fn remove_failure_was_side_effect_free(
|
||||
}
|
||||
|
||||
async fn read_optional_file_text_for_delta(
|
||||
path: &AbsolutePathBuf,
|
||||
path: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
exact: &mut bool,
|
||||
) -> Option<String> {
|
||||
note_existing_path_delta_support(path, fs, sandbox, exact).await;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
match fs.read_file_text(&path_uri, sandbox).await {
|
||||
match fs.read_file_text(path, sandbox).await {
|
||||
Ok(content) => Some(content),
|
||||
Err(source) if source.kind() == io::ErrorKind::NotFound => None,
|
||||
Err(_) => {
|
||||
@@ -602,13 +614,12 @@ async fn read_optional_file_text_for_delta(
|
||||
}
|
||||
|
||||
async fn note_existing_path_delta_support(
|
||||
path: &AbsolutePathBuf,
|
||||
path: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
exact: &mut bool,
|
||||
) {
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
match fs.get_metadata(&path_uri, sandbox).await {
|
||||
match fs.get_metadata(path, sandbox).await {
|
||||
Ok(metadata) if metadata.is_file && !metadata.is_symlink => {}
|
||||
Ok(_) => *exact = false,
|
||||
Err(source) if source.kind() == io::ErrorKind::NotFound => {}
|
||||
@@ -618,37 +629,39 @@ async fn note_existing_path_delta_support(
|
||||
|
||||
async fn write_file_with_missing_parent_retry(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
path_abs: &AbsolutePathBuf,
|
||||
path: &PathUri,
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> anyhow::Result<()> {
|
||||
let path_uri = PathUri::from_abs_path(path_abs);
|
||||
match fs.write_file(&path_uri, contents.clone(), sandbox).await {
|
||||
match fs.write_file(path, contents.clone(), sandbox).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
if let Some(parent_abs) = path_abs.parent() {
|
||||
let parent_uri = PathUri::from_abs_path(&parent_abs);
|
||||
fs.create_directory(
|
||||
&parent_uri,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
sandbox,
|
||||
)
|
||||
if let Some(parent) = path.parent() {
|
||||
fs.create_directory(&parent, CreateDirectoryOptions { recursive: true }, sandbox)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to create parent directories for {}",
|
||||
path.inferred_native_path_string()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
fs.write_file(path, contents, sandbox)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to create parent directories for {}",
|
||||
path_abs.display()
|
||||
"Failed to write file {}",
|
||||
path.inferred_native_path_string()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
fs.write_file(&path_uri, contents, sandbox)
|
||||
.await
|
||||
.with_context(|| format!("Failed to write file {}", path_abs.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Err(err).with_context(|| format!("Failed to write file {}", path_abs.display()))
|
||||
}
|
||||
Err(err) => Err(err).with_context(|| {
|
||||
format!(
|
||||
"Failed to write file {}",
|
||||
path.inferred_native_path_string()
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,15 +673,17 @@ struct AppliedPatch {
|
||||
/// Return *only* the new file contents (joined into a single `String`) after
|
||||
/// applying the chunks to the file at `path`.
|
||||
async fn derive_new_contents_from_chunks(
|
||||
path_abs: &AbsolutePathBuf,
|
||||
path: &PathUri,
|
||||
chunks: &[UpdateFileChunk],
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> std::result::Result<AppliedPatch, ApplyPatchError> {
|
||||
let path_uri = PathUri::from_abs_path(path_abs);
|
||||
let original_contents = fs.read_file_text(&path_uri, sandbox).await.map_err(|err| {
|
||||
let original_contents = fs.read_file_text(path, sandbox).await.map_err(|err| {
|
||||
ApplyPatchError::IoError(IoError {
|
||||
context: format!("Failed to read file to update {}", path_abs.display()),
|
||||
context: format!(
|
||||
"Failed to read file to update {}",
|
||||
path.inferred_native_path_string()
|
||||
),
|
||||
source: err,
|
||||
})
|
||||
})?;
|
||||
@@ -681,7 +696,8 @@ async fn derive_new_contents_from_chunks(
|
||||
original_lines.pop();
|
||||
}
|
||||
|
||||
let replacements = compute_replacements(&original_lines, path_abs.as_path(), chunks)?;
|
||||
let path_text = path.inferred_native_path_string();
|
||||
let replacements = compute_replacements(&original_lines, &path_text, chunks)?;
|
||||
let new_lines = apply_replacements(original_lines, &replacements);
|
||||
let mut new_lines = new_lines;
|
||||
if !new_lines.last().is_some_and(String::is_empty) {
|
||||
@@ -699,7 +715,7 @@ async fn derive_new_contents_from_chunks(
|
||||
/// `(start_index, old_len, new_lines)`.
|
||||
fn compute_replacements(
|
||||
original_lines: &[String],
|
||||
path: &Path,
|
||||
path: &str,
|
||||
chunks: &[UpdateFileChunk],
|
||||
) -> std::result::Result<Vec<(usize, usize, Vec<String>)>, ApplyPatchError> {
|
||||
let mut replacements: Vec<(usize, usize, Vec<String>)> = Vec::new();
|
||||
@@ -718,9 +734,7 @@ fn compute_replacements(
|
||||
line_index = idx + 1;
|
||||
} else {
|
||||
return Err(ApplyPatchError::ComputeReplacements(format!(
|
||||
"Failed to find context '{}' in {}",
|
||||
ctx_line,
|
||||
path.display()
|
||||
"Failed to find context '{ctx_line}' in {path}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -776,7 +790,7 @@ fn compute_replacements(
|
||||
} else {
|
||||
return Err(ApplyPatchError::ComputeReplacements(format!(
|
||||
"Failed to find expected lines in {}:\n{}",
|
||||
path.display(),
|
||||
path,
|
||||
chunk.old_lines.join("\n"),
|
||||
)));
|
||||
}
|
||||
@@ -824,16 +838,16 @@ pub struct ApplyPatchFileUpdate {
|
||||
}
|
||||
|
||||
pub async fn unified_diff_from_chunks(
|
||||
path_abs: &AbsolutePathBuf,
|
||||
path: &PathUri,
|
||||
chunks: &[UpdateFileChunk],
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> std::result::Result<ApplyPatchFileUpdate, ApplyPatchError> {
|
||||
unified_diff_from_chunks_with_context(path_abs, chunks, /*context*/ 1, fs, sandbox).await
|
||||
unified_diff_from_chunks_with_context(path, chunks, /*context*/ 1, fs, sandbox).await
|
||||
}
|
||||
|
||||
pub async fn unified_diff_from_chunks_with_context(
|
||||
path_abs: &AbsolutePathBuf,
|
||||
path: &PathUri,
|
||||
chunks: &[UpdateFileChunk],
|
||||
context: usize,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
@@ -842,7 +856,7 @@ pub async fn unified_diff_from_chunks_with_context(
|
||||
let AppliedPatch {
|
||||
original_contents,
|
||||
new_contents,
|
||||
} = derive_new_contents_from_chunks(path_abs, chunks, fs, sandbox).await?;
|
||||
} = derive_new_contents_from_chunks(path, chunks, fs, sandbox).await?;
|
||||
let text_diff = TextDiff::from_lines(&original_contents, &new_contents);
|
||||
let unified_diff = text_diff.unified_diff().context_radius(context).to_string();
|
||||
Ok(ApplyPatchFileUpdate {
|
||||
@@ -875,7 +889,6 @@ pub fn print_summary(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::string::ToString;
|
||||
@@ -900,7 +913,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -924,7 +937,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_apply_patch_hunks_accept_relative_and_absolute_paths() {
|
||||
let dir = tempdir().unwrap();
|
||||
let cwd = dir.path().abs();
|
||||
let cwd = PathUri::from_path(dir.path()).expect("absolute test path");
|
||||
let relative_add = dir.path().join("relative-add.txt");
|
||||
let absolute_add = dir.path().join("absolute-add.txt");
|
||||
let relative_delete = dir.path().join("relative-delete.txt");
|
||||
@@ -1003,7 +1016,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1039,7 +1052,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1079,7 +1092,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1123,7 +1136,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
let failure = apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1183,7 +1196,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1241,7 +1254,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1285,7 +1298,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1328,7 +1341,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1378,9 +1391,9 @@ mod tests {
|
||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
};
|
||||
let path_abs = path.as_path().abs();
|
||||
let path_uri = PathUri::from_path(&path).expect("absolute test path");
|
||||
let diff = unified_diff_from_chunks(
|
||||
&path_abs,
|
||||
&path_uri,
|
||||
update_file_chunks,
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
@@ -1426,11 +1439,15 @@ mod tests {
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
};
|
||||
|
||||
let path_abs = path.as_path().abs();
|
||||
let diff =
|
||||
unified_diff_from_chunks(&path_abs, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
let resolved_path = PathUri::from_path(&path).expect("absolute test path");
|
||||
let diff = unified_diff_from_chunks(
|
||||
&resolved_path,
|
||||
chunks,
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected_diff = r#"@@ -1,2 +1,2 @@
|
||||
-foo
|
||||
+FOO
|
||||
@@ -1468,11 +1485,15 @@ mod tests {
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
};
|
||||
|
||||
let path_abs = path.as_path().abs();
|
||||
let diff =
|
||||
unified_diff_from_chunks(&path_abs, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
let resolved_path = PathUri::from_path(&path).expect("absolute test path");
|
||||
let diff = unified_diff_from_chunks(
|
||||
&resolved_path,
|
||||
chunks,
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected_diff = r#"@@ -2,2 +2,2 @@
|
||||
bar
|
||||
-baz
|
||||
@@ -1508,9 +1529,9 @@ mod tests {
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
};
|
||||
|
||||
let path_abs = path.as_path().abs();
|
||||
let path_uri = PathUri::from_path(&path).expect("absolute test path");
|
||||
let diff =
|
||||
unified_diff_from_chunks(&path_abs, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
unified_diff_from_chunks(&path_uri, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected_diff = r#"@@ -3 +3,2 @@
|
||||
@@ -1559,9 +1580,9 @@ mod tests {
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
};
|
||||
|
||||
let path_abs = path.as_path().abs();
|
||||
let path_uri = PathUri::from_path(&path).expect("absolute test path");
|
||||
let diff =
|
||||
unified_diff_from_chunks(&path_abs, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
unified_diff_from_chunks(&path_uri, chunks, LOCAL_FS.as_ref(), /*sandbox*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1589,7 +1610,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1627,7 +1648,7 @@ g
|
||||
let mut stderr = Vec::new();
|
||||
let result = apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
@@ -1646,7 +1667,7 @@ g
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("binary.dat");
|
||||
fs::write(dir.path().join("source.txt"), "before\n").unwrap();
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(dir.path()).unwrap();
|
||||
let cwd = PathUri::from_path(dir.path()).expect("absolute test path");
|
||||
|
||||
for patch in [
|
||||
wrap_patch("*** Add File: binary.dat\n+text"),
|
||||
@@ -1684,7 +1705,7 @@ g
|
||||
let mut stderr = Vec::new();
|
||||
let delta = apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&PathUri::from_path(dir.path()).expect("absolute test path"),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
|
||||
@@ -25,9 +25,10 @@
|
||||
//! leading/trailing whitespace around patch markers.
|
||||
use crate::ApplyPatchArgs;
|
||||
use crate::streaming_parser::StreamingPatchParser;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
#[cfg(test)]
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use codex_utils_path_uri::PathUriParseError;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -81,12 +82,12 @@ pub enum Hunk {
|
||||
}
|
||||
|
||||
impl Hunk {
|
||||
pub fn resolve_path(&self, cwd: &AbsolutePathBuf) -> AbsolutePathBuf {
|
||||
pub fn resolve_path(&self, cwd: &PathUri) -> Result<PathUri, PathUriParseError> {
|
||||
let path = match self {
|
||||
Hunk::UpdateFile { path, .. } => path,
|
||||
Hunk::AddFile { .. } | Hunk::DeleteFile { .. } => self.path(),
|
||||
};
|
||||
AbsolutePathBuf::resolve_path_against_base(path, cwd)
|
||||
cwd.join(&path.to_string_lossy())
|
||||
}
|
||||
|
||||
/// Returns the path affected by this hunk, using the move destination for rename hunks.
|
||||
@@ -479,7 +480,7 @@ fn test_parse_patch_accepts_relative_and_absolute_hunk_paths() {
|
||||
#[test]
|
||||
fn test_hunk_resolve_path_accepts_relative_and_absolute_paths() {
|
||||
let cwd_dir = tempfile::tempdir().unwrap();
|
||||
let cwd = cwd_dir.path().to_path_buf().abs();
|
||||
let cwd = PathUri::from_path(cwd_dir.path()).unwrap();
|
||||
let absolute_dir = tempfile::tempdir().unwrap();
|
||||
let absolute_add = absolute_dir.path().join("absolute-add.py").abs();
|
||||
let absolute_delete = absolute_dir.path().join("absolute-delete.py").abs();
|
||||
@@ -491,13 +492,13 @@ fn test_hunk_resolve_path_accepts_relative_and_absolute_paths() {
|
||||
path: PathBuf::from("relative-add.py"),
|
||||
contents: String::new(),
|
||||
},
|
||||
cwd.join("relative-add.py"),
|
||||
cwd.join("relative-add.py").unwrap(),
|
||||
),
|
||||
(
|
||||
DeleteFile {
|
||||
path: PathBuf::from("relative-delete.py"),
|
||||
},
|
||||
cwd.join("relative-delete.py"),
|
||||
cwd.join("relative-delete.py").unwrap(),
|
||||
),
|
||||
(
|
||||
UpdateFile {
|
||||
@@ -505,20 +506,20 @@ fn test_hunk_resolve_path_accepts_relative_and_absolute_paths() {
|
||||
move_path: None,
|
||||
chunks: Vec::new(),
|
||||
},
|
||||
cwd.join("relative-update.py"),
|
||||
cwd.join("relative-update.py").unwrap(),
|
||||
),
|
||||
(
|
||||
AddFile {
|
||||
path: absolute_add.to_path_buf(),
|
||||
contents: String::new(),
|
||||
},
|
||||
absolute_add,
|
||||
PathUri::from_abs_path(&absolute_add),
|
||||
),
|
||||
(
|
||||
DeleteFile {
|
||||
path: absolute_delete.to_path_buf(),
|
||||
},
|
||||
absolute_delete,
|
||||
PathUri::from_abs_path(&absolute_delete),
|
||||
),
|
||||
(
|
||||
UpdateFile {
|
||||
@@ -526,10 +527,10 @@ fn test_hunk_resolve_path_accepts_relative_and_absolute_paths() {
|
||||
move_path: None,
|
||||
chunks: Vec::new(),
|
||||
},
|
||||
absolute_update,
|
||||
PathUri::from_abs_path(&absolute_update),
|
||||
),
|
||||
] {
|
||||
assert_eq!(hunk.resolve_path(&cwd), expected_path);
|
||||
assert_eq!(hunk.resolve_path(&cwd), Ok(expected_path));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ pub fn run_main() -> i32 {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
// TODO(anp): Discover the standalone executable cwd as PathUri directly.
|
||||
let cwd = codex_utils_path_uri::PathUri::from_abs_path(&cwd);
|
||||
match runtime.block_on(crate::apply_patch(
|
||||
&patch_arg,
|
||||
&cwd,
|
||||
|
||||
@@ -122,6 +122,7 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
|
||||
Ok(runtime) => runtime,
|
||||
Err(_) => std::process::exit(1),
|
||||
};
|
||||
let cwd = cwd.into();
|
||||
match runtime.block_on(codex_apply_patch::apply_patch(
|
||||
&patch_arg,
|
||||
&cwd,
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::ApplyPatchFileChange;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -35,24 +36,12 @@ pub(crate) async fn apply_patch(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
action: ApplyPatchAction,
|
||||
) -> InternalApplyPatchInvocation {
|
||||
// TODO(anp): Migrate patch safety checks to PathUri.
|
||||
let cwd = match action.cwd.to_abs_path() {
|
||||
Ok(cwd) => cwd,
|
||||
Err(err) => {
|
||||
return InternalApplyPatchInvocation::Output(Err(FunctionCallError::RespondToModel(
|
||||
format!(
|
||||
"patch cwd `{}` is not native to the Codex host: {err}",
|
||||
action.cwd
|
||||
),
|
||||
)));
|
||||
}
|
||||
};
|
||||
match assess_patch_safety(
|
||||
&action,
|
||||
turn_context.approval_policy.value(),
|
||||
&turn_context.permission_profile(),
|
||||
file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&action.cwd,
|
||||
turn_context.windows_sandbox_level,
|
||||
) {
|
||||
SafetyCheck::AutoApprove {
|
||||
@@ -103,9 +92,11 @@ pub(crate) fn convert_apply_patch_to_protocol(
|
||||
new_content: _new_content,
|
||||
} => FileChange::Update {
|
||||
unified_diff: unified_diff.clone(),
|
||||
move_path: move_path.clone(),
|
||||
move_path: move_path.as_ref().map(PathUri::to_path_buf),
|
||||
},
|
||||
};
|
||||
// TODO(anp): Carry PathUri through patch protocol events once app-server and rollout
|
||||
// compatibility no longer require path-flavored strings.
|
||||
result.insert(path.to_path_buf(), protocol_change);
|
||||
}
|
||||
result
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use core_test_support::PathBufExt;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use tempfile::tempdir;
|
||||
@@ -7,14 +7,14 @@ use tempfile::tempdir;
|
||||
#[test]
|
||||
fn convert_apply_patch_maps_add_variant() {
|
||||
let tmp = tempdir().expect("tmp");
|
||||
let p = tmp.path().join("a.txt").abs();
|
||||
// Create an action with a single Add change
|
||||
let action = ApplyPatchAction::new_add_for_test(&p, "hello".to_string());
|
||||
let path = tmp.path().join("a.txt");
|
||||
let path_uri = PathUri::from_path(&path).expect("absolute test path");
|
||||
let action = ApplyPatchAction::new_add_for_test(&path_uri, "hello".to_string());
|
||||
|
||||
let got = convert_apply_patch_to_protocol(&action);
|
||||
|
||||
assert_eq!(
|
||||
got.get(p.as_path()),
|
||||
got.get(path.as_path()),
|
||||
Some(&FileChange::Add {
|
||||
content: "hello".to_string()
|
||||
})
|
||||
|
||||
+26
-11
@@ -2,7 +2,6 @@ use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::util::resolve_path;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::ApplyPatchFileChange;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -11,7 +10,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::get_platform_sandbox;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
const PATCH_REJECTED_OUTSIDE_PROJECT_REASON: &str =
|
||||
"writing outside of the project; rejected by user approval settings";
|
||||
@@ -35,7 +34,7 @@ pub fn assess_patch_safety(
|
||||
policy: AskForApproval,
|
||||
permission_profile: &PermissionProfile,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
) -> SafetyCheck {
|
||||
if action.is_empty() {
|
||||
@@ -118,14 +117,17 @@ pub fn assess_patch_safety(
|
||||
fn patch_rejection_reason(
|
||||
permission_profile: &PermissionProfile,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
) -> &'static str {
|
||||
let has_no_writable_roots = cwd.to_abs_path().is_ok_and(|cwd| {
|
||||
file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(cwd.as_path())
|
||||
.is_empty()
|
||||
});
|
||||
match permission_profile {
|
||||
PermissionProfile::Managed { .. }
|
||||
if !file_system_sandbox_policy.has_full_disk_write_access()
|
||||
&& file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(cwd.as_path())
|
||||
.is_empty() =>
|
||||
&& has_no_writable_roots =>
|
||||
{
|
||||
PATCH_REJECTED_READ_ONLY_REASON
|
||||
}
|
||||
@@ -138,8 +140,17 @@ fn patch_rejection_reason(
|
||||
fn is_write_patch_constrained_to_writable_paths(
|
||||
action: &ApplyPatchAction,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
) -> bool {
|
||||
// A full-disk policy permits every patch target, so no per-path writable-root check can
|
||||
// further constrain the result.
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() {
|
||||
return true;
|
||||
}
|
||||
// TODO(anp): Make filesystem sandbox policies operate on PathUri.
|
||||
let Ok(native_cwd) = cwd.to_abs_path() else {
|
||||
return false;
|
||||
};
|
||||
// Normalize a path by removing `.` and resolving `..` without touching the
|
||||
// filesystem (works even if the file does not exist).
|
||||
fn normalize(path: &Path) -> Option<PathBuf> {
|
||||
@@ -159,14 +170,18 @@ fn is_write_patch_constrained_to_writable_paths(
|
||||
// Determine whether `path` is inside **any** writable root. Both `path`
|
||||
// and roots are converted to absolute, normalized forms before the
|
||||
// prefix check.
|
||||
let is_path_writable = |p: &Path| {
|
||||
let abs = resolve_path(cwd, &p.to_path_buf());
|
||||
let is_path_writable = |path: &PathUri| {
|
||||
// TODO(anp): Make sandbox policy path checks accept PathUri without host projection.
|
||||
let Ok(path) = path.to_abs_path() else {
|
||||
return false;
|
||||
};
|
||||
let abs = path.into_path_buf();
|
||||
let abs = match normalize(&abs) {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
file_system_sandbox_policy.can_write_path_with_cwd(&abs, cwd)
|
||||
file_system_sandbox_policy.can_write_path_with_cwd(&abs, &native_cwd)
|
||||
};
|
||||
|
||||
for (path, change) in action.changes() {
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_protocol::protocol::FileSystemSandboxEntry;
|
||||
use codex_protocol::protocol::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use core_test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
@@ -17,11 +18,13 @@ fn test_writable_roots_constraint() {
|
||||
// the real current working directory.
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let parent = cwd.parent().unwrap();
|
||||
|
||||
// Helper to build a single‑entry patch that adds a file at `p`.
|
||||
let make_add_change =
|
||||
|p: AbsolutePathBuf| ApplyPatchAction::new_add_for_test(&p, "".to_string());
|
||||
let make_add_change = |p: AbsolutePathBuf| {
|
||||
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&p), "".to_string())
|
||||
};
|
||||
|
||||
let add_inside = make_add_change(cwd.join("inner.txt"));
|
||||
let add_outside = make_add_change(parent.join("outside.txt"));
|
||||
@@ -37,13 +40,13 @@ fn test_writable_roots_constraint() {
|
||||
assert!(is_write_patch_constrained_to_writable_paths(
|
||||
&add_inside,
|
||||
&workspace_only_file_system_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
));
|
||||
|
||||
assert!(!is_write_patch_constrained_to_writable_paths(
|
||||
&add_outside,
|
||||
&workspace_only_file_system_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
));
|
||||
|
||||
// With the parent dir explicitly added as a writable root, the
|
||||
@@ -56,7 +59,7 @@ fn test_writable_roots_constraint() {
|
||||
assert!(is_write_patch_constrained_to_writable_paths(
|
||||
&add_outside,
|
||||
&file_system_policy_with_parent,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -64,8 +67,12 @@ fn test_writable_roots_constraint() {
|
||||
fn external_sandbox_auto_approves_in_on_request() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let add_inside_path = cwd.join("inner.txt");
|
||||
let add_inside = ApplyPatchAction::new_add_for_test(&add_inside_path, "".to_string());
|
||||
let add_inside = ApplyPatchAction::new_add_for_test(
|
||||
&PathUri::from_abs_path(&add_inside_path),
|
||||
"".to_string(),
|
||||
);
|
||||
|
||||
let permission_profile = PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Enabled,
|
||||
@@ -78,7 +85,7 @@ fn external_sandbox_auto_approves_in_on_request() {
|
||||
AskForApproval::OnRequest,
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
WindowsSandboxLevel::Disabled
|
||||
),
|
||||
SafetyCheck::AutoApprove {
|
||||
@@ -92,9 +99,11 @@ fn external_sandbox_auto_approves_in_on_request() {
|
||||
fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let parent = cwd.parent().unwrap();
|
||||
let outside_path = parent.join("outside.txt");
|
||||
let add_outside = ApplyPatchAction::new_add_for_test(&outside_path, "".to_string());
|
||||
let add_outside =
|
||||
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&outside_path), "".to_string());
|
||||
let permission_profile = PermissionProfile::workspace_write_with(
|
||||
&[],
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
@@ -109,7 +118,7 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
|
||||
AskForApproval::OnRequest,
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::AskUser,
|
||||
@@ -126,7 +135,7 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
|
||||
}),
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::AskUser,
|
||||
@@ -137,9 +146,11 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
|
||||
fn granular_sandbox_approval_false_rejects_out_of_root_patch() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let parent = cwd.parent().unwrap();
|
||||
let outside_path = parent.join("outside.txt");
|
||||
let add_outside = ApplyPatchAction::new_add_for_test(&outside_path, "".to_string());
|
||||
let add_outside =
|
||||
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&outside_path), "".to_string());
|
||||
let permission_profile = PermissionProfile::workspace_write_with(
|
||||
&[],
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
@@ -160,7 +171,7 @@ fn granular_sandbox_approval_false_rejects_out_of_root_patch() {
|
||||
}),
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::Reject {
|
||||
@@ -173,15 +184,17 @@ fn granular_sandbox_approval_false_rejects_out_of_root_patch() {
|
||||
fn read_only_policy_rejects_patch_with_read_only_reason() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let inside_path = cwd.join("inside.txt");
|
||||
let action = ApplyPatchAction::new_add_for_test(&inside_path, "".to_string());
|
||||
let action =
|
||||
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&inside_path), "".to_string());
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
|
||||
|
||||
assert!(!is_write_patch_constrained_to_writable_paths(
|
||||
&action,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
));
|
||||
assert_eq!(
|
||||
assess_patch_safety(
|
||||
@@ -189,7 +202,7 @@ fn read_only_policy_rejects_patch_with_read_only_reason() {
|
||||
AskForApproval::Never,
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::Reject {
|
||||
@@ -201,9 +214,13 @@ fn read_only_policy_rejects_patch_with_read_only_reason() {
|
||||
fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let blocked_path = cwd.join("blocked.txt");
|
||||
let blocked_absolute = blocked_path;
|
||||
let action = ApplyPatchAction::new_add_for_test(&blocked_absolute, "".to_string());
|
||||
let action = ApplyPatchAction::new_add_for_test(
|
||||
&PathUri::from_abs_path(&blocked_absolute),
|
||||
"".to_string(),
|
||||
);
|
||||
let permission_profile = PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
};
|
||||
@@ -225,7 +242,7 @@ fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
|
||||
assert!(!is_write_patch_constrained_to_writable_paths(
|
||||
&action,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
));
|
||||
assert_eq!(
|
||||
assess_patch_safety(
|
||||
@@ -233,7 +250,7 @@ fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
|
||||
AskForApproval::OnRequest,
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::AskUser,
|
||||
@@ -244,10 +261,14 @@ fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
|
||||
fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let blocked_path = cwd.join("docs").join("blocked.txt");
|
||||
let blocked_absolute = blocked_path;
|
||||
let docs_absolute = AbsolutePathBuf::resolve_path_against_base("docs", &cwd);
|
||||
let action = ApplyPatchAction::new_add_for_test(&blocked_absolute, "".to_string());
|
||||
let action = ApplyPatchAction::new_add_for_test(
|
||||
&PathUri::from_abs_path(&blocked_absolute),
|
||||
"".to_string(),
|
||||
);
|
||||
let permission_profile = PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
};
|
||||
@@ -269,7 +290,7 @@ fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
|
||||
assert!(!is_write_patch_constrained_to_writable_paths(
|
||||
&action,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
));
|
||||
assert_eq!(
|
||||
assess_patch_safety(
|
||||
@@ -277,7 +298,7 @@ fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
|
||||
AskForApproval::OnRequest,
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::AskUser,
|
||||
@@ -288,8 +309,10 @@ fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
|
||||
fn missing_project_dot_codex_config_requires_approval() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let config_path = cwd.join(".codex").join("config.toml");
|
||||
let action = ApplyPatchAction::new_add_for_test(&config_path, "".to_string());
|
||||
let action =
|
||||
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&config_path), "".to_string());
|
||||
let permission_profile = PermissionProfile::workspace_write_with(
|
||||
&[],
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
@@ -309,7 +332,7 @@ fn missing_project_dot_codex_config_requires_approval() {
|
||||
assert!(!is_write_patch_constrained_to_writable_paths(
|
||||
&action,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
));
|
||||
assert_eq!(
|
||||
assess_patch_safety(
|
||||
@@ -317,7 +340,7 @@ fn missing_project_dot_codex_config_requires_approval() {
|
||||
AskForApproval::OnRequest,
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
&cwd_uri,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::AskUser,
|
||||
|
||||
@@ -632,7 +632,7 @@ mod tests {
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::protocol::PatchApplyStatus;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -645,7 +645,7 @@ mod tests {
|
||||
make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;
|
||||
let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(dir.path()).expect("absolute cwd");
|
||||
let cwd = PathUri::from_path(dir.path()).expect("absolute cwd");
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let delta = codex_apply_patch::apply_patch(
|
||||
@@ -729,7 +729,7 @@ mod tests {
|
||||
make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;
|
||||
let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(dir.path()).expect("absolute cwd");
|
||||
let cwd = PathUri::from_path(dir.path()).expect("absolute cwd");
|
||||
|
||||
for patch in [
|
||||
"*** Begin Patch\n*** Add File: a.txt\n+one\n*** End Patch",
|
||||
@@ -782,7 +782,7 @@ mod tests {
|
||||
make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;
|
||||
let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(dir.path()).expect("absolute cwd");
|
||||
let cwd = PathUri::from_path(dir.path()).expect("absolute cwd");
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let delta = codex_apply_patch::apply_patch(
|
||||
|
||||
@@ -203,33 +203,21 @@ fn format_update_chunks_for_progress(chunks: &[codex_apply_patch::UpdateFileChun
|
||||
unified_diff
|
||||
}
|
||||
|
||||
fn file_paths_for_action(action: &ApplyPatchAction) -> Vec<AbsolutePathBuf> {
|
||||
fn file_paths_for_action(action: &ApplyPatchAction) -> Vec<PathUri> {
|
||||
let mut keys = Vec::new();
|
||||
// TODO(anp): Migrate permission path accounting to PathUri.
|
||||
let Ok(cwd) = action.cwd.to_abs_path() else {
|
||||
return keys;
|
||||
};
|
||||
|
||||
for (path, change) in action.changes() {
|
||||
if let Some(key) = to_abs_path(&cwd, path) {
|
||||
keys.push(key);
|
||||
}
|
||||
keys.push(path.clone());
|
||||
|
||||
if let ApplyPatchFileChange::Update { move_path, .. } = change
|
||||
&& let Some(dest) = move_path
|
||||
&& let Some(key) = to_abs_path(&cwd, dest)
|
||||
{
|
||||
keys.push(key);
|
||||
keys.push(dest.clone());
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
|
||||
fn to_abs_path(cwd: &AbsolutePathBuf, path: &Path) -> Option<AbsolutePathBuf> {
|
||||
Some(AbsolutePathBuf::resolve_path_against_base(path, cwd))
|
||||
}
|
||||
|
||||
fn write_permissions_for_paths(
|
||||
file_paths: &[AbsolutePathBuf],
|
||||
file_system_sandbox_policy: &codex_protocol::permissions::FileSystemSandboxPolicy,
|
||||
@@ -275,13 +263,14 @@ async fn effective_patch_permissions(
|
||||
turn: &TurnContext,
|
||||
environment_id: &str,
|
||||
action: &ApplyPatchAction,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> (
|
||||
Vec<AbsolutePathBuf>,
|
||||
cwd: &PathUri,
|
||||
) -> std::io::Result<(
|
||||
Vec<PathUri>,
|
||||
crate::tools::handlers::EffectiveAdditionalPermissions,
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy,
|
||||
) {
|
||||
)> {
|
||||
let file_paths = file_paths_for_action(action);
|
||||
let native_cwd = cwd.to_abs_path()?;
|
||||
let granted_permissions = merge_permission_profiles(
|
||||
session
|
||||
.granted_session_permissions(environment_id)
|
||||
@@ -297,19 +286,43 @@ async fn effective_patch_permissions(
|
||||
&base_file_system_sandbox_policy,
|
||||
granted_permissions.as_ref(),
|
||||
);
|
||||
let native_file_paths = file_paths
|
||||
.iter()
|
||||
.map(PathUri::to_abs_path)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let effective_additional_permissions = apply_granted_turn_permissions(
|
||||
session,
|
||||
environment_id,
|
||||
cwd.as_path(),
|
||||
native_cwd.as_path(),
|
||||
crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
write_permissions_for_paths(&file_paths, &file_system_sandbox_policy, cwd),
|
||||
write_permissions_for_paths(&native_file_paths, &file_system_sandbox_policy, &native_cwd),
|
||||
)
|
||||
.await;
|
||||
|
||||
(
|
||||
Ok((
|
||||
file_paths,
|
||||
effective_additional_permissions,
|
||||
file_system_sandbox_policy,
|
||||
))
|
||||
}
|
||||
|
||||
fn patch_permissions_without_path_matching(
|
||||
action: &ApplyPatchAction,
|
||||
) -> (
|
||||
Vec<PathUri>,
|
||||
crate::tools::handlers::EffectiveAdditionalPermissions,
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy,
|
||||
) {
|
||||
// TODO(anp): Make permission matching operate on PathUri. Until then, foreign paths skip
|
||||
// permission matching; a managed turn still fails closed at the platform sandbox boundary.
|
||||
(
|
||||
file_paths_for_action(action),
|
||||
crate::tools::handlers::EffectiveAdditionalPermissions {
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
permissions_preapproved: false,
|
||||
},
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy::unrestricted(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -366,21 +379,18 @@ impl ApplyPatchHandler {
|
||||
"apply_patch is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
// TODO(anp): Migrate apply-patch verification and permission accounting to PathUri so
|
||||
// patches can target environment-native foreign paths without host projection.
|
||||
let cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"apply_patch cwd `{}` is not native to the Codex host: {err}",
|
||||
turn_environment.cwd()
|
||||
))
|
||||
})?;
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
let sandbox = turn.file_system_sandbox_context(
|
||||
/*additional_permissions*/ None,
|
||||
turn_environment.cwd(),
|
||||
);
|
||||
match codex_apply_patch::verify_apply_patch_args(args, &cwd, fs.as_ref(), Some(&sandbox))
|
||||
.await
|
||||
match codex_apply_patch::verify_apply_patch_args(
|
||||
args,
|
||||
turn_environment.cwd(),
|
||||
fs.as_ref(),
|
||||
Some(&sandbox),
|
||||
)
|
||||
.await
|
||||
{
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
let (file_paths, effective_additional_permissions, file_system_sandbox_policy) =
|
||||
@@ -389,9 +399,10 @@ impl ApplyPatchHandler {
|
||||
turn.as_ref(),
|
||||
&turn_environment.environment_id,
|
||||
&changes,
|
||||
&cwd,
|
||||
turn_environment.cwd(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap_or_else(|_| patch_permissions_without_path_matching(&changes));
|
||||
match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)
|
||||
.await
|
||||
{
|
||||
@@ -531,7 +542,7 @@ impl CoreToolRuntime for ApplyPatchHandler {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn intercept_apply_patch(
|
||||
command: &[String],
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
turn_environment: TurnEnvironment,
|
||||
session: Arc<Session>,
|
||||
@@ -540,9 +551,7 @@ pub(crate) async fn intercept_apply_patch(
|
||||
call_id: &str,
|
||||
tool_name: &str,
|
||||
) -> Result<Option<FunctionToolOutput>, FunctionCallError> {
|
||||
let sandbox_cwd = PathUri::from_abs_path(cwd);
|
||||
let sandbox =
|
||||
turn.file_system_sandbox_context(/*additional_permissions*/ None, &sandbox_cwd);
|
||||
let sandbox = turn.file_system_sandbox_context(/*additional_permissions*/ None, cwd);
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, Some(&sandbox))
|
||||
.await
|
||||
{
|
||||
@@ -555,7 +564,8 @@ pub(crate) async fn intercept_apply_patch(
|
||||
&changes,
|
||||
cwd,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap_or_else(|_| patch_permissions_without_path_matching(&changes));
|
||||
match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -225,6 +225,8 @@ async fn approval_keys_include_move_destination() {
|
||||
+new content
|
||||
*** End Patch"#;
|
||||
let argv = vec!["apply_patch".to_string(), patch.to_string()];
|
||||
// TODO(anp): Keep apply_patch handler test cwd values as PathUri.
|
||||
let cwd = PathUri::from_abs_path(&cwd);
|
||||
let action = match codex_apply_patch::maybe_parse_apply_patch_verified(
|
||||
&argv,
|
||||
&cwd,
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::tools::sandboxing::ToolCtx;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_tools::ToolName;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
mod shell_command;
|
||||
|
||||
@@ -139,9 +140,10 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
}
|
||||
|
||||
// Intercept apply_patch if present.
|
||||
let apply_patch_cwd = PathUri::from_abs_path(&exec_params.cwd);
|
||||
if let Some(output) = intercept_apply_patch(
|
||||
&exec_params.command,
|
||||
&exec_params.cwd,
|
||||
&apply_patch_cwd,
|
||||
fs.as_ref(),
|
||||
turn_environment.clone(),
|
||||
session.clone(),
|
||||
|
||||
@@ -287,20 +287,18 @@ impl ExecCommandHandler {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(anp) intercept apply_patch properly when cwd is a foreign path
|
||||
if let Some(native_cwd) = native_cwd.as_ref()
|
||||
&& let Some(output) = intercept_apply_patch(
|
||||
&command,
|
||||
native_cwd,
|
||||
fs.as_ref(),
|
||||
turn_environment.clone(),
|
||||
context.session.clone(),
|
||||
context.turn.clone(),
|
||||
Some(&tracker),
|
||||
&context.call_id,
|
||||
"exec_command",
|
||||
)
|
||||
.await?
|
||||
if let Some(output) = intercept_apply_patch(
|
||||
&command,
|
||||
&cwd,
|
||||
fs.as_ref(),
|
||||
turn_environment.clone(),
|
||||
context.session.clone(),
|
||||
context.turn.clone(),
|
||||
Some(&tracker),
|
||||
&context.call_id,
|
||||
"exec_command",
|
||||
)
|
||||
.await?
|
||||
{
|
||||
manager.release_process_id(process_id).await;
|
||||
return Ok(boxed_tool_output(ExecCommandToolOutput {
|
||||
|
||||
@@ -32,7 +32,6 @@ use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_sandboxing::policy_transforms::effective_permission_profile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use futures::future::BoxFuture;
|
||||
use std::path::PathBuf;
|
||||
@@ -41,14 +40,14 @@ use std::time::Instant;
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize)]
|
||||
pub(crate) struct ApplyPatchApprovalKey {
|
||||
environment_id: String,
|
||||
path: AbsolutePathBuf,
|
||||
path: PathUri,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyPatchRequest {
|
||||
pub turn_environment: TurnEnvironment,
|
||||
pub action: ApplyPatchAction,
|
||||
pub file_paths: Vec<AbsolutePathBuf>,
|
||||
pub file_paths: Vec<PathUri>,
|
||||
pub changes: std::collections::HashMap<PathBuf, FileChange>,
|
||||
pub exec_approval_requirement: ExecApprovalRequirement,
|
||||
pub additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
@@ -81,10 +80,15 @@ impl ApplyPatchRuntime {
|
||||
) -> std::io::Result<GuardianApprovalRequest> {
|
||||
// TODO(anp): Remove this conversion once the guardian API supports PathUri.
|
||||
let cwd = req.action.cwd.to_abs_path()?;
|
||||
let files = req
|
||||
.file_paths
|
||||
.iter()
|
||||
.map(PathUri::to_abs_path)
|
||||
.collect::<std::io::Result<Vec<_>>>()?;
|
||||
Ok(GuardianApprovalRequest::ApplyPatch {
|
||||
id: call_id.to_string(),
|
||||
cwd,
|
||||
files: req.file_paths.clone(),
|
||||
files,
|
||||
patch: req.action.patch.clone(),
|
||||
})
|
||||
}
|
||||
@@ -245,15 +249,9 @@ impl ToolRuntime<ApplyPatchRequest, ApplyPatchRuntimeOutput> for ApplyPatchRunti
|
||||
let sandbox = Self::file_system_sandbox_context_for_attempt(req, attempt);
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
// TODO(anp): Teach apply_patch to operate on PathUri directly.
|
||||
let cwd = req
|
||||
.action
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| ToolError::Rejected(err.to_string()))?;
|
||||
let result = codex_apply_patch::apply_patch(
|
||||
&req.action.patch,
|
||||
&cwd,
|
||||
&req.action.cwd,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
fs.as_ref(),
|
||||
|
||||
@@ -53,13 +53,14 @@ async fn guardian_review_request_includes_patch_context() {
|
||||
let path = std::env::temp_dir()
|
||||
.join("guardian-apply-patch-test.txt")
|
||||
.abs();
|
||||
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
|
||||
let action =
|
||||
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&path), "hello".to_string());
|
||||
let expected_cwd = action.cwd.to_abs_path().expect("native patch cwd");
|
||||
let expected_patch = action.patch.clone();
|
||||
let request = ApplyPatchRequest {
|
||||
turn_environment: test_turn_environment(codex_exec_server::LOCAL_ENVIRONMENT_ID),
|
||||
action,
|
||||
file_paths: vec![path.clone()],
|
||||
file_paths: vec![PathUri::from_abs_path(&path)],
|
||||
changes: HashMap::from([(
|
||||
path.to_path_buf(),
|
||||
FileChange::Add {
|
||||
@@ -82,7 +83,7 @@ async fn guardian_review_request_includes_patch_context() {
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
id: "call-1".to_string(),
|
||||
cwd: expected_cwd,
|
||||
files: request.file_paths,
|
||||
files: vec![path],
|
||||
patch: expected_patch,
|
||||
}
|
||||
);
|
||||
@@ -94,12 +95,13 @@ async fn permission_request_payload_uses_apply_patch_hook_name_and_aliases() {
|
||||
let path = std::env::temp_dir()
|
||||
.join("apply-patch-permission-request-payload.txt")
|
||||
.abs();
|
||||
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
|
||||
let action =
|
||||
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&path), "hello".to_string());
|
||||
let expected_patch = action.patch.clone();
|
||||
let req = ApplyPatchRequest {
|
||||
turn_environment: test_turn_environment(codex_exec_server::LOCAL_ENVIRONMENT_ID),
|
||||
action,
|
||||
file_paths: vec![path],
|
||||
file_paths: vec![PathUri::from_abs_path(&path)],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
@@ -130,10 +132,11 @@ async fn approval_keys_include_environment_id() {
|
||||
let path = std::env::temp_dir()
|
||||
.join("apply-patch-approval-key.txt")
|
||||
.abs();
|
||||
let path_uri = PathUri::from_abs_path(&path);
|
||||
let req = ApplyPatchRequest {
|
||||
turn_environment: test_turn_environment("remote"),
|
||||
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
|
||||
file_paths: vec![path.clone()],
|
||||
action: ApplyPatchAction::new_add_for_test(&path_uri, "hello".to_string()),
|
||||
file_paths: vec![path_uri.clone()],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
@@ -150,7 +153,7 @@ async fn approval_keys_include_environment_id() {
|
||||
serde_json::json!([
|
||||
{
|
||||
"environment_id": "remote",
|
||||
"path": path,
|
||||
"path": path_uri,
|
||||
}
|
||||
])
|
||||
);
|
||||
@@ -164,8 +167,11 @@ async fn sandbox_cwd_uses_patch_action_cwd() {
|
||||
.abs();
|
||||
let req = ApplyPatchRequest {
|
||||
turn_environment: test_turn_environment(codex_exec_server::LOCAL_ENVIRONMENT_ID),
|
||||
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
|
||||
file_paths: vec![path.clone()],
|
||||
action: ApplyPatchAction::new_add_for_test(
|
||||
&PathUri::from_abs_path(&path),
|
||||
"hello".to_string(),
|
||||
),
|
||||
file_paths: vec![PathUri::from_abs_path(&path)],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
@@ -192,8 +198,11 @@ async fn file_system_sandbox_context_uses_active_attempt() {
|
||||
};
|
||||
let req = ApplyPatchRequest {
|
||||
turn_environment: test_turn_environment(codex_exec_server::LOCAL_ENVIRONMENT_ID),
|
||||
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
|
||||
file_paths: vec![path.clone()],
|
||||
action: ApplyPatchAction::new_add_for_test(
|
||||
&PathUri::from_abs_path(&path),
|
||||
"hello".to_string(),
|
||||
),
|
||||
file_paths: vec![PathUri::from_abs_path(&path)],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
@@ -259,8 +268,11 @@ async fn no_sandbox_attempt_has_no_file_system_context() {
|
||||
.abs();
|
||||
let req = ApplyPatchRequest {
|
||||
turn_environment: test_turn_environment(codex_exec_server::LOCAL_ENVIRONMENT_ID),
|
||||
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
|
||||
file_paths: vec![path.clone()],
|
||||
action: ApplyPatchAction::new_add_for_test(
|
||||
&PathUri::from_abs_path(&path),
|
||||
"hello".to_string(),
|
||||
),
|
||||
file_paths: vec![PathUri::from_abs_path(&path)],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
|
||||
@@ -4,7 +4,7 @@ use codex_apply_patch::MaybeApplyPatchVerified;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_git_utils::ApplyGitRequest;
|
||||
use codex_git_utils::apply_git_patch;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
@@ -18,7 +18,7 @@ fn git_blob_sha1_hex(data: &str) -> String {
|
||||
}
|
||||
|
||||
async fn apply_verified_patch(root: &Path, patch: &str) -> AppliedPatchDelta {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(root).expect("absolute tempdir path");
|
||||
let cwd = PathUri::from_path(root).expect("absolute tempdir path");
|
||||
let argv = vec!["apply_patch".to_string(), patch.to_string()];
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified(
|
||||
&argv,
|
||||
|
||||
@@ -50,7 +50,11 @@ const APP_SERVER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_s
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
const CALL_ID: &str = "wine-cmd-smoke";
|
||||
const PATCH_CALL_ID: &str = "wine-apply-patch";
|
||||
const VERIFY_CALL_ID: &str = "wine-verify-patch";
|
||||
const PATCH_FILE: &str = "codex-apply-patch-smoke.txt";
|
||||
const COMMAND: &str = r#"if ((Get-Location).Path -ne 'C:\windows') { exit 1 }"#;
|
||||
const VERIFY_COMMAND: &str = r#"$path = Join-Path (Get-Location) 'codex-apply-patch-smoke.txt'; if (-not (Test-Path $path)) { exit 1 }; if ([IO.File]::ReadAllText($path) -ne "patched through unified exec`n") { exit 2 }; Remove-Item $path; Write-Output 'PATCH_VERIFIED'"#;
|
||||
|
||||
WineExecServer
|
||||
.scope(|exec_server_url| async move {
|
||||
@@ -63,6 +67,22 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
"workdir": r"C:\windows",
|
||||
"yield_time_ms": 10_000,
|
||||
}))?;
|
||||
let patch = format!(
|
||||
"*** Begin Patch\n*** Add File: {PATCH_FILE}\n+patched through unified exec\n*** End Patch"
|
||||
);
|
||||
let patch_arguments = serde_json::to_string(&json!({
|
||||
"cmd": format!("apply_patch <<'EOF'\n{patch}\nEOF\n"),
|
||||
"login": false,
|
||||
// Resolve this relative workdir using the selected Windows environment cwd.
|
||||
"workdir": r"apply-patch-smoke\nested",
|
||||
"yield_time_ms": 10_000,
|
||||
}))?;
|
||||
let verify_arguments = serde_json::to_string(&json!({
|
||||
"cmd": VERIFY_COMMAND,
|
||||
"login": false,
|
||||
"workdir": r"apply-patch-smoke\nested",
|
||||
"yield_time_ms": 10_000,
|
||||
}))?;
|
||||
let response_mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
@@ -73,9 +93,19 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_function_call(PATCH_CALL_ID, "exec_command", &patch_arguments),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-3"),
|
||||
ev_function_call(VERIFY_CALL_ID, "exec_command", &verify_arguments),
|
||||
ev_completed("resp-3"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-4"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-4"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
@@ -97,7 +127,7 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
test.config.cwd.clone(),
|
||||
vec![TurnEnvironmentSelection {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::parse("file:///C:/windows")?,
|
||||
cwd: PathUri::parse("file:///C:/codex-home")?,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -130,6 +160,7 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
|
||||
let mut begin = None;
|
||||
let mut end = None;
|
||||
let mut patch_end = None;
|
||||
let mut turn_complete = false;
|
||||
loop {
|
||||
match wait_for_event(&test.codex, |_| true).await {
|
||||
@@ -139,6 +170,9 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
EventMsg::ExecCommandEnd(event) if event.call_id == CALL_ID => {
|
||||
end = Some(event)
|
||||
}
|
||||
EventMsg::PatchApplyEnd(event) if event.call_id == PATCH_CALL_ID => {
|
||||
patch_end = Some(event)
|
||||
}
|
||||
EventMsg::TurnComplete(_) => turn_complete = true,
|
||||
_ => {}
|
||||
}
|
||||
@@ -162,14 +196,48 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
assert_eq!((&begin.cwd, &end.cwd), (&expected_cwd, &expected_cwd));
|
||||
assert_eq!((end.exit_code, end.status), (0, ExecCommandStatus::Completed));
|
||||
|
||||
let patch_end = patch_end.context("intercepted apply_patch should emit an end event")?;
|
||||
assert!(
|
||||
patch_end.success,
|
||||
"intercepted apply_patch failed: stdout={:?} stderr={:?}",
|
||||
patch_end.stdout, patch_end.stderr
|
||||
);
|
||||
assert!(
|
||||
patch_end
|
||||
.changes
|
||||
.contains_key(&std::path::PathBuf::from(format!(
|
||||
r"C:\codex-home\apply-patch-smoke\nested\{PATCH_FILE}"
|
||||
))),
|
||||
"apply_patch should retain the Windows cwd: {:?}",
|
||||
patch_end.changes
|
||||
);
|
||||
let request = response_mock
|
||||
.last_request()
|
||||
.context("model should receive the command output")?;
|
||||
let (verify_output, verify_success) = request
|
||||
.function_call_output_content_and_success(VERIFY_CALL_ID)
|
||||
.context("verification output should be present")?;
|
||||
anyhow::ensure!(
|
||||
verify_success != Some(false),
|
||||
"verification command failed: {verify_output:?}"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
verify_output
|
||||
.as_deref()
|
||||
.is_some_and(|output| output.contains("PATCH_VERIFIED")),
|
||||
"verification command did not confirm the patched file: {verify_output:?}"
|
||||
);
|
||||
|
||||
let (_output, success) = request
|
||||
.function_call_output_content_and_success(CALL_ID)
|
||||
.context("command output should be present")?;
|
||||
assert_ne!(success, Some(false));
|
||||
|
||||
let (patch_output, patch_success) = request
|
||||
.function_call_output_content_and_success(PATCH_CALL_ID)
|
||||
.context("apply_patch output should be present")?;
|
||||
let patch_output = patch_output.context("apply_patch output should contain text")?;
|
||||
assert!(patch_output.contains(PATCH_FILE));
|
||||
assert_ne!(patch_success, Some(false));
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -12,6 +12,7 @@ use serde::Serializer;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
@@ -96,6 +97,7 @@ impl PathUri {
|
||||
Self::from_opaque_path_bytes(&path_bytes)
|
||||
}
|
||||
|
||||
/// Parses an absolute native path using the specified path convention.
|
||||
pub(crate) fn from_absolute_native_path(
|
||||
path: &str,
|
||||
convention: PathConvention,
|
||||
@@ -202,6 +204,11 @@ impl PathUri {
|
||||
.map(decode_uri_path)
|
||||
}
|
||||
|
||||
/// Renders this URI as a path-flavored string using its inferred convention.
|
||||
pub fn to_path_buf(&self) -> PathBuf {
|
||||
PathBuf::from(self.inferred_native_path_string())
|
||||
}
|
||||
|
||||
/// Returns the parent URI, or `None` for the URI root or an opaque fallback
|
||||
/// URI created by [`Self::from_abs_path`].
|
||||
pub fn parent(&self) -> Option<Self> {
|
||||
@@ -310,15 +317,19 @@ impl PathUri {
|
||||
|
||||
/// Converts this file URI to a path using the current host's path rules.
|
||||
///
|
||||
/// Conversion should succeed when the URI was created from an
|
||||
/// [`AbsolutePathBuf`] on the current host, including fallback URIs created
|
||||
/// by [`Self::from_abs_path`]. It may fail when the URI came from a different
|
||||
/// operating system and its `file:` URI form cannot be represented using
|
||||
/// the current host's path rules, such as a UNC authority on POSIX or a
|
||||
/// POSIX root on Windows. Because a `file:` URI does not record its source
|
||||
/// operating system, callers should only use this method when the URI is
|
||||
/// known to identify a path on the current host.
|
||||
/// The URI's inferred path convention must match the current host. Conversion should succeed
|
||||
/// when the URI was created from an [`AbsolutePathBuf`] on the current host, including fallback
|
||||
/// URIs created by [`Self::from_abs_path`]. Foreign conventions are rejected rather than being
|
||||
/// projected onto a syntactically valid but unrelated host path.
|
||||
pub fn to_abs_path(&self) -> io::Result<AbsolutePathBuf> {
|
||||
if self.infer_path_convention() != Some(PathConvention::native()) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
PathUriParseError::InvalidFileUriPath {
|
||||
path: self.to_string(),
|
||||
},
|
||||
));
|
||||
}
|
||||
if let Some(path_bytes) = decode_bad_path_uri(&self.0) {
|
||||
#[cfg(unix)]
|
||||
let decoded_path = {
|
||||
@@ -688,6 +699,17 @@ impl PathConvention {
|
||||
pub const fn native() -> Self {
|
||||
Self::Posix
|
||||
}
|
||||
|
||||
/// Splits absolute or relative native path text into lexical segments.
|
||||
///
|
||||
/// This does not validate the path or require it to be absolute. POSIX paths split on `/`,
|
||||
/// while Windows paths split on both `\\` and `/`. Empty segments are retained.
|
||||
pub fn path_segments(self, path: &str) -> impl DoubleEndedIterator<Item = &str> {
|
||||
path.split(move |character| match self {
|
||||
Self::Posix => character == '/',
|
||||
Self::Windows => matches!(character, '/' | '\\'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PathConvention {
|
||||
|
||||
@@ -34,21 +34,24 @@ fn file_uri_round_trips_an_absolute_path() {
|
||||
#[test]
|
||||
fn non_native_uri_io_conversion_is_invalid_input() {
|
||||
#[cfg(unix)]
|
||||
let uri = PathUri::parse("file://server/share/file.txt").expect("valid file URI");
|
||||
let uris = ["file://server/share/file.txt", "file:///C:/workspace"];
|
||||
#[cfg(windows)]
|
||||
let uri = PathUri::parse("file:///usr/local/file.txt").expect("valid file URI");
|
||||
let uris = ["file:///usr/local/file.txt"];
|
||||
|
||||
let error = uri
|
||||
.to_abs_path()
|
||||
.expect_err("URI should not be host-native");
|
||||
for uri in uris {
|
||||
let uri = PathUri::parse(uri).expect("valid file URI");
|
||||
let error = uri
|
||||
.to_abs_path()
|
||||
.expect_err("URI should not be host-native");
|
||||
|
||||
assert_eq!(
|
||||
(error.kind(), error.to_string()),
|
||||
(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("'{uri}' is invalid on '{}'", std::env::consts::OS),
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
(error.kind(), error.to_string()),
|
||||
(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("'{uri}' is invalid on '{}'", std::env::consts::OS),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -90,6 +93,35 @@ fn infers_path_conventions_from_uri_shape() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_convention_splits_absolute_relative_and_bare_path_text() {
|
||||
for (convention, path, expected) in [
|
||||
(
|
||||
PathConvention::Posix,
|
||||
"/usr/local/bin/bash",
|
||||
vec!["", "usr", "local", "bin", "bash"],
|
||||
),
|
||||
(
|
||||
PathConvention::Posix,
|
||||
r"tools\pwsh.exe",
|
||||
vec![r"tools\pwsh.exe"],
|
||||
),
|
||||
(
|
||||
PathConvention::Windows,
|
||||
r"C:\Program Files\PowerShell\7\pwsh.exe",
|
||||
vec!["C:", "Program Files", "PowerShell", "7", "pwsh.exe"],
|
||||
),
|
||||
(
|
||||
PathConvention::Windows,
|
||||
"tools/pwsh.exe",
|
||||
vec!["tools", "pwsh.exe"],
|
||||
),
|
||||
(PathConvention::Windows, "cmd.exe", vec!["cmd.exe"]),
|
||||
] {
|
||||
assert_eq!(convention.path_segments(path).collect::<Vec<_>>(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_shaped_posix_uri_is_intentionally_inferred_as_windows() {
|
||||
let path = PathUri::parse("file:///C:/actually/a/posix/path").expect("valid path URI");
|
||||
@@ -475,6 +507,20 @@ fn basename_uses_decoded_uri_segments() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_buf_uses_the_inferred_native_spelling() {
|
||||
let windows = PathUri::parse("file:///C:/Program%20Files/pwsh.exe").expect("Windows URI");
|
||||
let posix = PathUri::parse("file:///usr/local/bin/bash").expect("POSIX URI");
|
||||
|
||||
assert_eq!(
|
||||
(windows.to_path_buf(), posix.to_path_buf()),
|
||||
(
|
||||
PathBuf::from(r"C:\Program Files\pwsh.exe"),
|
||||
PathBuf::from("/usr/local/bin/bash"),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_uses_uri_hierarchy_and_preserves_authority() {
|
||||
for (input, expected) in [
|
||||
|
||||
Reference in New Issue
Block a user