mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
## Why `PathUri::from_abs_path` can fail for absolute paths that do not have a normal `file:` URI representation, forcing filesystem call sites to handle a conversion error even though the original path can be preserved losslessly. ## What Make `from_abs_path` infallible and migrate its callers. Unrepresentable paths use `file:///%00/bad/path/<base64>`, encoding Unix bytes or Windows UTF-16LE; `to_abs_path` validates and decodes that fallback. The leading encoded null reserves a namespace that cannot collide with a real Unix or Windows path, and fallback URIs remain opaque to lexical path operations. ## Validation Added path-URI coverage for Unix null and non-UTF-8 paths, Windows device/verbatim and non-Unicode paths, serialization, malformed fallbacks, opaque lexical operations, invalid native payloads, and literal `/bad/path` collision resistance.
243 lines
7.3 KiB
Rust
243 lines
7.3 KiB
Rust
use super::*;
|
|
use codex_file_system::CopyOptions;
|
|
use codex_file_system::CreateDirectoryOptions;
|
|
use codex_file_system::ExecutorFileSystemFuture;
|
|
use codex_file_system::FileMetadata;
|
|
use codex_file_system::FileSystemSandboxContext;
|
|
use codex_file_system::ReadDirectoryEntry;
|
|
use codex_file_system::RemoveOptions;
|
|
use codex_utils_path_uri::PathUri;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::tempdir;
|
|
|
|
struct TestFileSystem;
|
|
|
|
impl ExecutorFileSystem for TestFileSystem {
|
|
fn canonicalize<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
_sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, PathUri> {
|
|
Box::pin(async move {
|
|
let path = path.to_abs_path()?;
|
|
let canonicalized = path.canonicalize()?;
|
|
Ok(PathUri::from_abs_path(&canonicalized))
|
|
})
|
|
}
|
|
|
|
fn read_file<'a>(
|
|
&'a self,
|
|
path: &'a PathUri,
|
|
_sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, Vec<u8>> {
|
|
Box::pin(async move {
|
|
let path = path.to_abs_path()?;
|
|
tokio::fs::read(path.as_path()).await
|
|
})
|
|
}
|
|
|
|
fn write_file<'a>(
|
|
&'a self,
|
|
_path: &'a PathUri,
|
|
_contents: Vec<u8>,
|
|
_sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(async move { unimplemented!("test filesystem only supports reads") })
|
|
}
|
|
|
|
fn create_directory<'a>(
|
|
&'a self,
|
|
_path: &'a PathUri,
|
|
_create_directory_options: CreateDirectoryOptions,
|
|
_sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(async move { unimplemented!("test filesystem only supports reads") })
|
|
}
|
|
|
|
fn get_metadata<'a>(
|
|
&'a self,
|
|
_path: &'a PathUri,
|
|
_sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
|
|
Box::pin(async move { unimplemented!("test filesystem only supports reads") })
|
|
}
|
|
|
|
fn read_directory<'a>(
|
|
&'a self,
|
|
_path: &'a PathUri,
|
|
_sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
|
|
Box::pin(async move { unimplemented!("test filesystem only supports reads") })
|
|
}
|
|
|
|
fn remove<'a>(
|
|
&'a self,
|
|
_path: &'a PathUri,
|
|
_remove_options: RemoveOptions,
|
|
_sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(async move { unimplemented!("test filesystem only supports reads") })
|
|
}
|
|
|
|
fn copy<'a>(
|
|
&'a self,
|
|
_source_path: &'a PathUri,
|
|
_destination_path: &'a PathUri,
|
|
_copy_options: CopyOptions,
|
|
_sandbox: Option<&'a FileSystemSandboxContext>,
|
|
) -> ExecutorFileSystemFuture<'a, ()> {
|
|
Box::pin(async move { unimplemented!("test filesystem only supports reads") })
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn profile_v2_rejects_matching_legacy_profile_in_base_user_config() {
|
|
let tmp = tempdir().expect("tempdir");
|
|
let selected_config = tmp.path().join("work.config.toml");
|
|
|
|
std::fs::write(
|
|
tmp.path().join(CONFIG_TOML_FILE),
|
|
r#"
|
|
model = "gpt-main"
|
|
|
|
[profiles.work]
|
|
model = "gpt-work"
|
|
"#,
|
|
)
|
|
.expect("write default user config");
|
|
std::fs::write(&selected_config, r#"model = "gpt-work-v2""#)
|
|
.expect("write selected user config");
|
|
|
|
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
|
overrides.user_config_path = Some(AbsolutePathBuf::resolve_path_against_base(
|
|
"work.config.toml",
|
|
tmp.path(),
|
|
));
|
|
overrides.user_config_profile = Some("work".parse().expect("profile-v2 name"));
|
|
|
|
let err = load_config_layers_state(
|
|
&TestFileSystem,
|
|
tmp.path(),
|
|
/*cwd*/ None,
|
|
&[],
|
|
overrides,
|
|
&crate::NoopThreadConfigLoader,
|
|
)
|
|
.await
|
|
.expect_err("profile-v2 should reject a matching legacy profile in base user config");
|
|
|
|
assert_eq!(
|
|
err.kind(),
|
|
io::ErrorKind::InvalidData,
|
|
"a matching legacy profile should be a hard config error"
|
|
);
|
|
let message = err.to_string();
|
|
assert!(
|
|
message.contains("--profile `work` cannot be used"),
|
|
"unexpected error message: {message}"
|
|
);
|
|
assert!(
|
|
message.contains("config.toml"),
|
|
"unexpected error message: {message}"
|
|
);
|
|
assert!(
|
|
message.contains("[profiles.work]"),
|
|
"unexpected error message: {message}"
|
|
);
|
|
assert!(
|
|
message.contains("https://developers.openai.com/codex/config-advanced#profiles"),
|
|
"unexpected error message: {message}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn profile_v2_rejects_matching_legacy_profile_selector_in_base_user_config() {
|
|
let tmp = tempdir().expect("tempdir");
|
|
let selected_config = tmp.path().join("work.config.toml");
|
|
|
|
std::fs::write(
|
|
tmp.path().join(CONFIG_TOML_FILE),
|
|
r#"
|
|
profile = "work"
|
|
model = "gpt-main"
|
|
"#,
|
|
)
|
|
.expect("write default user config");
|
|
std::fs::write(&selected_config, r#"model = "gpt-work-v2""#)
|
|
.expect("write selected user config");
|
|
|
|
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
|
overrides.user_config_path = Some(AbsolutePathBuf::resolve_path_against_base(
|
|
"work.config.toml",
|
|
tmp.path(),
|
|
));
|
|
overrides.user_config_profile = Some("work".parse().expect("profile-v2 name"));
|
|
|
|
let err = load_config_layers_state(
|
|
&TestFileSystem,
|
|
tmp.path(),
|
|
/*cwd*/ None,
|
|
&[],
|
|
overrides,
|
|
&crate::NoopThreadConfigLoader,
|
|
)
|
|
.await
|
|
.expect_err("profile-v2 should reject a matching legacy profile selector");
|
|
|
|
assert_eq!(
|
|
err.kind(),
|
|
io::ErrorKind::InvalidData,
|
|
"a matching legacy profile selector should be a hard config error"
|
|
);
|
|
let message = err.to_string();
|
|
assert!(
|
|
message.contains("--profile `work` cannot be used"),
|
|
"unexpected error message: {message}"
|
|
);
|
|
assert!(
|
|
message.contains("profile = \"work\""),
|
|
"unexpected error message: {message}"
|
|
);
|
|
assert!(
|
|
message.contains("work.config.toml"),
|
|
"unexpected error message: {message}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn profile_v2_allows_unrelated_legacy_profiles_in_base_user_config() {
|
|
let tmp = tempdir().expect("tempdir");
|
|
let selected_config = tmp.path().join("work.config.toml");
|
|
|
|
std::fs::write(
|
|
tmp.path().join(CONFIG_TOML_FILE),
|
|
r#"
|
|
model = "gpt-main"
|
|
|
|
[profiles.dev]
|
|
model = "gpt-dev"
|
|
"#,
|
|
)
|
|
.expect("write default user config");
|
|
std::fs::write(&selected_config, r#"model = "gpt-work-v2""#)
|
|
.expect("write selected user config");
|
|
|
|
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
|
overrides.user_config_path = Some(AbsolutePathBuf::resolve_path_against_base(
|
|
"work.config.toml",
|
|
tmp.path(),
|
|
));
|
|
overrides.user_config_profile = Some("work".parse().expect("profile-v2 name"));
|
|
|
|
load_config_layers_state(
|
|
&TestFileSystem,
|
|
tmp.path(),
|
|
/*cwd*/ None,
|
|
&[],
|
|
overrides,
|
|
&crate::NoopThreadConfigLoader,
|
|
)
|
|
.await
|
|
.expect("profile-v2 should allow unrelated legacy profiles in base user config");
|
|
}
|