mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] make PathUri::from_abs_path infallible (#27976)
## 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.
This commit is contained in:
committed by
GitHub
Unverified
parent
eb46984aaa
commit
968a3ac9c1
Generated
+1
@@ -4193,6 +4193,7 @@ dependencies = [
|
||||
name = "codex-utils-path-uri"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
|
||||
@@ -65,7 +65,7 @@ impl FsRequestProcessor {
|
||||
&self,
|
||||
params: FsReadFileParams,
|
||||
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
|
||||
let path = PathUri::from_abs_path(¶ms.path).map_err(map_fs_error)?;
|
||||
let path = PathUri::from_abs_path(¶ms.path);
|
||||
let bytes = self
|
||||
.file_system()?
|
||||
.read_file(&path, /*sandbox*/ None)
|
||||
@@ -85,7 +85,7 @@ impl FsRequestProcessor {
|
||||
"fs/writeFile requires valid base64 dataBase64: {err}"
|
||||
))
|
||||
})?;
|
||||
let path = PathUri::from_abs_path(¶ms.path).map_err(map_fs_error)?;
|
||||
let path = PathUri::from_abs_path(¶ms.path);
|
||||
self.file_system()?
|
||||
.write_file(&path, bytes, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -97,7 +97,7 @@ impl FsRequestProcessor {
|
||||
&self,
|
||||
params: FsCreateDirectoryParams,
|
||||
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
|
||||
let path = PathUri::from_abs_path(¶ms.path).map_err(map_fs_error)?;
|
||||
let path = PathUri::from_abs_path(¶ms.path);
|
||||
self.file_system()?
|
||||
.create_directory(
|
||||
&path,
|
||||
@@ -115,7 +115,7 @@ impl FsRequestProcessor {
|
||||
&self,
|
||||
params: FsGetMetadataParams,
|
||||
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
|
||||
let path = PathUri::from_abs_path(¶ms.path).map_err(map_fs_error)?;
|
||||
let path = PathUri::from_abs_path(¶ms.path);
|
||||
let metadata = self
|
||||
.file_system()?
|
||||
.get_metadata(&path, /*sandbox*/ None)
|
||||
@@ -134,7 +134,7 @@ impl FsRequestProcessor {
|
||||
&self,
|
||||
params: FsReadDirectoryParams,
|
||||
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
|
||||
let path = PathUri::from_abs_path(¶ms.path).map_err(map_fs_error)?;
|
||||
let path = PathUri::from_abs_path(¶ms.path);
|
||||
let entries = self
|
||||
.file_system()?
|
||||
.read_directory(&path, /*sandbox*/ None)
|
||||
@@ -156,7 +156,7 @@ impl FsRequestProcessor {
|
||||
&self,
|
||||
params: FsRemoveParams,
|
||||
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
|
||||
let path = PathUri::from_abs_path(¶ms.path).map_err(map_fs_error)?;
|
||||
let path = PathUri::from_abs_path(¶ms.path);
|
||||
self.file_system()?
|
||||
.remove(
|
||||
&path,
|
||||
@@ -175,9 +175,8 @@ impl FsRequestProcessor {
|
||||
&self,
|
||||
params: FsCopyParams,
|
||||
) -> Result<FsCopyResponse, JSONRPCErrorError> {
|
||||
let source_path = PathUri::from_abs_path(¶ms.source_path).map_err(map_fs_error)?;
|
||||
let destination_path =
|
||||
PathUri::from_abs_path(¶ms.destination_path).map_err(map_fs_error)?;
|
||||
let source_path = PathUri::from_abs_path(¶ms.source_path);
|
||||
let destination_path = PathUri::from_abs_path(¶ms.destination_path);
|
||||
self.file_system()?
|
||||
.copy(
|
||||
&source_path,
|
||||
|
||||
@@ -186,17 +186,7 @@ pub async fn verify_apply_patch_args(
|
||||
);
|
||||
}
|
||||
Hunk::DeleteFile { .. } => {
|
||||
let path_uri = match PathUri::from_abs_path(&path) {
|
||||
Ok(path_uri) => path_uri,
|
||||
Err(e) => {
|
||||
return MaybeApplyPatchVerified::CorrectnessError(
|
||||
ApplyPatchError::IoError(IoError {
|
||||
context: format!("Failed to read {}", path.display()),
|
||||
source: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
let path_uri = PathUri::from_abs_path(&path);
|
||||
let content = match fs.read_file_text(&path_uri, sandbox).await {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
|
||||
@@ -391,7 +391,7 @@ async fn apply_hunks_to_files(
|
||||
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 = PathUri::from_abs_path(&path_abs);
|
||||
match hunk {
|
||||
Hunk::AddFile { contents, .. } => {
|
||||
let overwritten_content =
|
||||
@@ -556,7 +556,7 @@ async fn ensure_not_directory(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> io::Result<()> {
|
||||
let path_uri = PathUri::from_abs_path(path)?;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
let metadata = fs.get_metadata(&path_uri, sandbox).await?;
|
||||
if metadata.is_directory {
|
||||
return Err(io::Error::new(
|
||||
@@ -573,9 +573,7 @@ async fn remove_failure_was_side_effect_free(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> bool {
|
||||
let Ok(path_uri) = PathUri::from_abs_path(path) else {
|
||||
return false;
|
||||
};
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
match expected_content {
|
||||
Some(expected_content) => fs
|
||||
.read_file_text(&path_uri, sandbox)
|
||||
@@ -592,13 +590,7 @@ async fn read_optional_file_text_for_delta(
|
||||
exact: &mut bool,
|
||||
) -> Option<String> {
|
||||
note_existing_path_delta_support(path, fs, sandbox, exact).await;
|
||||
let path_uri = match PathUri::from_abs_path(path) {
|
||||
Ok(path_uri) => path_uri,
|
||||
Err(_) => {
|
||||
*exact = false;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
match fs.read_file_text(&path_uri, sandbox).await {
|
||||
Ok(content) => Some(content),
|
||||
Err(source) if source.kind() == io::ErrorKind::NotFound => None,
|
||||
@@ -615,10 +607,7 @@ async fn note_existing_path_delta_support(
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
exact: &mut bool,
|
||||
) {
|
||||
let Ok(path_uri) = PathUri::from_abs_path(path) else {
|
||||
*exact = false;
|
||||
return;
|
||||
};
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
match fs.get_metadata(&path_uri, sandbox).await {
|
||||
Ok(metadata) if metadata.is_file && !metadata.is_symlink => {}
|
||||
Ok(_) => *exact = false,
|
||||
@@ -633,12 +622,12 @@ async fn write_file_with_missing_parent_retry(
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> anyhow::Result<()> {
|
||||
let path_uri = PathUri::from_abs_path(path_abs)?;
|
||||
let path_uri = PathUri::from_abs_path(path_abs);
|
||||
match fs.write_file(&path_uri, 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)?;
|
||||
let parent_uri = PathUri::from_abs_path(&parent_abs);
|
||||
fs.create_directory(
|
||||
&parent_uri,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
@@ -676,7 +665,7 @@ async fn derive_new_contents_from_chunks(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> std::result::Result<AppliedPatch, ApplyPatchError> {
|
||||
let path_uri = PathUri::from_abs_path(path_abs)?;
|
||||
let path_uri = PathUri::from_abs_path(path_abs);
|
||||
let original_contents = fs.read_file_text(&path_uri, sandbox).await.map_err(|err| {
|
||||
ApplyPatchError::IoError(IoError {
|
||||
context: format!("Failed to read file to update {}", path_abs.display()),
|
||||
|
||||
@@ -107,7 +107,7 @@ pub(super) async fn read_config_from_path(
|
||||
log_missing_as_info: bool,
|
||||
strict_config: bool,
|
||||
) -> io::Result<Option<TomlValue>> {
|
||||
let path_uri = PathUri::from_abs_path(path)?;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
match fs.read_file_text(&path_uri, /*sandbox*/ None).await {
|
||||
Ok(contents) => match toml::from_str::<TomlValue>(&contents) {
|
||||
Ok(value) => {
|
||||
|
||||
@@ -473,7 +473,7 @@ async fn load_config_toml_for_required_layer(
|
||||
strict_config: bool,
|
||||
create_entry: impl FnOnce(TomlValue) -> ConfigLayerEntry,
|
||||
) -> io::Result<ConfigLayerEntry> {
|
||||
let toml_file_uri = PathUri::from_abs_path(toml_file)?;
|
||||
let toml_file_uri = PathUri::from_abs_path(toml_file);
|
||||
let toml_value = match fs.read_file_text(&toml_file_uri, /*sandbox*/ None).await {
|
||||
Ok(contents) => {
|
||||
let config_parent = toml_file.as_path().parent().ok_or_else(|| {
|
||||
@@ -569,7 +569,7 @@ pub async fn load_requirements_toml(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
requirements_toml_file: &AbsolutePathBuf,
|
||||
) -> io::Result<Option<RequirementsLayerEntry>> {
|
||||
let requirements_toml_file_uri = PathUri::from_abs_path(requirements_toml_file)?;
|
||||
let requirements_toml_file_uri = PathUri::from_abs_path(requirements_toml_file);
|
||||
match fs
|
||||
.read_file_text(&requirements_toml_file_uri, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -1139,7 +1139,7 @@ async fn find_project_root(
|
||||
for ancestor in cwd.ancestors() {
|
||||
for marker in project_root_markers {
|
||||
let marker_path = ancestor.join(marker);
|
||||
let marker_path_uri = PathUri::from_abs_path(&marker_path)?;
|
||||
let marker_path_uri = PathUri::from_abs_path(&marker_path);
|
||||
if fs
|
||||
.get_metadata(&marker_path_uri, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -1156,7 +1156,7 @@ async fn find_git_checkout_root(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
let cwd_uri = PathUri::from_abs_path(cwd).ok()?;
|
||||
let cwd_uri = PathUri::from_abs_path(cwd);
|
||||
let base = match fs.get_metadata(&cwd_uri, /*sandbox*/ None).await {
|
||||
Ok(metadata) if metadata.is_directory => cwd.clone(),
|
||||
_ => cwd.parent()?,
|
||||
@@ -1164,7 +1164,7 @@ async fn find_git_checkout_root(
|
||||
|
||||
for dir in base.ancestors() {
|
||||
let dot_git = dir.join(".git");
|
||||
let dot_git_uri = PathUri::from_abs_path(&dot_git).ok()?;
|
||||
let dot_git_uri = PathUri::from_abs_path(&dot_git);
|
||||
if fs
|
||||
.get_metadata(&dot_git_uri, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -1217,7 +1217,7 @@ async fn load_project_layers(
|
||||
let mut startup_warnings = Vec::new();
|
||||
for dir in dirs {
|
||||
let dot_codex_abs = dir.join(".codex");
|
||||
let dot_codex_uri = PathUri::from_abs_path(&dot_codex_abs)?;
|
||||
let dot_codex_uri = PathUri::from_abs_path(&dot_codex_abs);
|
||||
if !fs
|
||||
.get_metadata(&dot_codex_uri, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -1236,7 +1236,7 @@ async fn load_project_layers(
|
||||
continue;
|
||||
}
|
||||
let config_file = dot_codex_abs.join(CONFIG_TOML_FILE);
|
||||
let config_file_uri = PathUri::from_abs_path(&config_file)?;
|
||||
let config_file_uri = PathUri::from_abs_path(&config_file);
|
||||
match fs.read_file_text(&config_file_uri, /*sandbox*/ None).await {
|
||||
Ok(contents) => {
|
||||
let config: TomlValue = match toml::from_str(&contents) {
|
||||
@@ -1340,7 +1340,7 @@ async fn merge_root_checkout_project_hooks(
|
||||
return Ok(config);
|
||||
};
|
||||
let hooks_config_file = hooks_config_folder.join(CONFIG_TOML_FILE);
|
||||
let hooks_config_file_uri = PathUri::from_abs_path(&hooks_config_file)?;
|
||||
let hooks_config_file_uri = PathUri::from_abs_path(&hooks_config_file);
|
||||
let root_config = match fs
|
||||
.read_file_text(&hooks_config_file_uri, /*sandbox*/ None)
|
||||
.await
|
||||
|
||||
@@ -21,7 +21,7 @@ impl ExecutorFileSystem for TestFileSystem {
|
||||
Box::pin(async move {
|
||||
let path = path.to_abs_path()?;
|
||||
let canonicalized = path.canonicalize()?;
|
||||
PathUri::from_abs_path(&canonicalized)
|
||||
Ok(PathUri::from_abs_path(&canonicalized))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -137,17 +137,8 @@ async fn resolve_plugin_root(
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> Result<Option<ResolvedPlugin>, ExecutorPluginProviderError> {
|
||||
let root_id = &selected_root.id;
|
||||
let CapabilityRootLocation::Environment {
|
||||
environment_id,
|
||||
path,
|
||||
} = &selected_root.location;
|
||||
let root_uri = PathUri::from_abs_path(&plugin_root).map_err(|err| {
|
||||
ExecutorPluginProviderError::InvalidRootPath {
|
||||
root_id: root_id.clone(),
|
||||
path: path.clone(),
|
||||
message: err.to_string(),
|
||||
}
|
||||
})?;
|
||||
let CapabilityRootLocation::Environment { environment_id, .. } = &selected_root.location;
|
||||
let root_uri = PathUri::from_abs_path(&plugin_root);
|
||||
let root_metadata = file_system
|
||||
.get_metadata(&root_uri, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -166,13 +157,7 @@ async fn resolve_plugin_root(
|
||||
let mut manifest_path = None;
|
||||
for relative_path in DISCOVERABLE_PLUGIN_MANIFEST_PATHS {
|
||||
let candidate = plugin_root.join(relative_path);
|
||||
let candidate_uri = PathUri::from_abs_path(&candidate).map_err(|err| {
|
||||
ExecutorPluginProviderError::InvalidRootPath {
|
||||
root_id: root_id.clone(),
|
||||
path: candidate.as_path().to_string_lossy().into_owned(),
|
||||
message: err.to_string(),
|
||||
}
|
||||
})?;
|
||||
let candidate_uri = PathUri::from_abs_path(&candidate);
|
||||
match file_system
|
||||
.get_metadata(&candidate_uri, /*sandbox*/ None)
|
||||
.await
|
||||
|
||||
@@ -76,20 +76,7 @@ pub async fn build_skill_injections(
|
||||
let fs = loaded_skills
|
||||
.and_then(|outcome| outcome.file_system_for_skill(skill))
|
||||
.unwrap_or_else(|| Arc::clone(&LOCAL_FS));
|
||||
// Skill metadata may point at a file that is absent, but a path the host
|
||||
// cannot represent means the configured skill cannot be loaded correctly.
|
||||
let path = match PathUri::from_abs_path(&skill.path_to_skills_md) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
emit_skill_injected_metric(otel, skill, "error");
|
||||
result.warnings.push(format!(
|
||||
"Failed to load skill {name} at {path}: {err:#}",
|
||||
name = skill.name,
|
||||
path = skill.path_to_skills_md.display()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let path = PathUri::from_abs_path(&skill.path_to_skills_md);
|
||||
match fs.read_file_text(&path, /*sandbox*/ None).await {
|
||||
Ok(contents) => {
|
||||
emit_skill_injected_metric(otel, skill, "ok");
|
||||
|
||||
@@ -375,16 +375,7 @@ async fn repo_agents_skill_roots(
|
||||
let mut roots = Vec::new();
|
||||
for dir in dirs {
|
||||
let agents_skills = dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME);
|
||||
let agents_skills_uri = match PathUri::from_abs_path(&agents_skills) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"failed to convert repo skills root {} to URI: {err:#}",
|
||||
agents_skills.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let agents_skills_uri = PathUri::from_abs_path(&agents_skills);
|
||||
match fs.get_metadata(&agents_skills_uri, /*sandbox*/ None).await {
|
||||
Ok(metadata) if metadata.is_directory => roots.push(SkillRoot {
|
||||
path: agents_skills,
|
||||
@@ -440,16 +431,7 @@ async fn find_project_root(
|
||||
for ancestor in cwd.ancestors() {
|
||||
for marker in project_root_markers {
|
||||
let marker_path = ancestor.join(marker);
|
||||
let marker_path_uri = match PathUri::from_abs_path(&marker_path) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"failed to convert project root marker {} to URI: {err:#}",
|
||||
marker_path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let marker_path_uri = PathUri::from_abs_path(&marker_path);
|
||||
match fs.get_metadata(&marker_path_uri, /*sandbox*/ None).await {
|
||||
Ok(_) => return ancestor,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||
@@ -496,9 +478,7 @@ async fn canonicalize_for_skill_identity(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
path: &AbsolutePathBuf,
|
||||
) -> AbsolutePathBuf {
|
||||
let Ok(path_uri) = PathUri::from_abs_path(path) else {
|
||||
return path.clone();
|
||||
};
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
fs.canonicalize(&path_uri, /*sandbox*/ None)
|
||||
.await
|
||||
.and_then(|path| path.to_abs_path())
|
||||
@@ -519,16 +499,7 @@ async fn discover_skills_under_root(
|
||||
None => None,
|
||||
};
|
||||
|
||||
let root_uri = match PathUri::from_abs_path(&root) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"failed to convert skills root {} to URI: {err:#}",
|
||||
root.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let root_uri = PathUri::from_abs_path(&root);
|
||||
match fs.get_metadata(&root_uri, /*sandbox*/ None).await {
|
||||
Ok(metadata) if metadata.is_directory => {}
|
||||
Ok(_) => return,
|
||||
@@ -571,16 +542,7 @@ async fn discover_skills_under_root(
|
||||
let mut truncated_by_dir_limit = false;
|
||||
|
||||
while let Some((dir, depth)) = queue.pop_front() {
|
||||
let dir_uri = match PathUri::from_abs_path(&dir) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"failed to convert skills dir {} to URI: {e:#}",
|
||||
dir.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let dir_uri = PathUri::from_abs_path(&dir);
|
||||
let entries = match fs.read_directory(&dir_uri, /*sandbox*/ None).await {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
@@ -596,16 +558,7 @@ async fn discover_skills_under_root(
|
||||
}
|
||||
|
||||
let path = dir.join(&file_name);
|
||||
let path_uri = match PathUri::from_abs_path(&path) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"failed to convert skills path {} to URI: {e:#}",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let path_uri = PathUri::from_abs_path(&path);
|
||||
let metadata = match fs.get_metadata(&path_uri, /*sandbox*/ None).await {
|
||||
Ok(metadata) => metadata,
|
||||
Err(e) => {
|
||||
@@ -690,7 +643,7 @@ async fn parse_skill_file(
|
||||
plugin_id: Option<&str>,
|
||||
plugin_root: Option<&AbsolutePathBuf>,
|
||||
) -> Result<SkillMetadata, SkillParseError> {
|
||||
let path_uri = PathUri::from_abs_path(path).map_err(SkillParseError::Read)?;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
let contents = fs
|
||||
.read_file_text(&path_uri, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -786,17 +739,7 @@ async fn load_skill_metadata(
|
||||
let metadata_path = skill_dir
|
||||
.join(SKILLS_METADATA_DIR)
|
||||
.join(SKILLS_METADATA_FILENAME);
|
||||
let metadata_path_uri = match PathUri::from_abs_path(&metadata_path) {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"ignoring {path}: failed to convert {label} path to URI: {error}",
|
||||
path = metadata_path.display(),
|
||||
label = SKILLS_METADATA_FILENAME
|
||||
);
|
||||
return LoadedSkillMetadata::default();
|
||||
}
|
||||
};
|
||||
let metadata_path_uri = PathUri::from_abs_path(&metadata_path);
|
||||
match fs.get_metadata(&metadata_path_uri, /*sandbox*/ None).await {
|
||||
Ok(metadata) if metadata.is_file => {}
|
||||
Ok(_) => return LoadedSkillMetadata::default(),
|
||||
|
||||
@@ -153,7 +153,7 @@ impl HostLoadedSkills {
|
||||
.outcome
|
||||
.file_system_for_skill(skill)
|
||||
.unwrap_or_else(|| Arc::clone(&LOCAL_FS));
|
||||
let path = PathUri::from_abs_path(&skill.path_to_skills_md)?;
|
||||
let path = PathUri::from_abs_path(&skill.path_to_skills_md);
|
||||
fs.read_file_text(&path, /*sandbox*/ None).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ async fn read_agents_md(
|
||||
break;
|
||||
}
|
||||
|
||||
let path_uri = PathUri::from_abs_path(&p)?;
|
||||
let path_uri = PathUri::from_abs_path(&p);
|
||||
match fs.get_metadata(&path_uri, /*sandbox*/ None).await {
|
||||
Ok(metadata) if !metadata.is_file => continue,
|
||||
Ok(_) => {}
|
||||
@@ -194,7 +194,7 @@ async fn agents_md_paths(
|
||||
for ancestor in dir.ancestors() {
|
||||
for marker in &project_root_markers {
|
||||
let marker_path = ancestor.join(marker);
|
||||
let marker_path_uri = PathUri::from_abs_path(&marker_path)?;
|
||||
let marker_path_uri = PathUri::from_abs_path(&marker_path);
|
||||
let marker_exists = match fs.get_metadata(&marker_path_uri, /*sandbox*/ None).await
|
||||
{
|
||||
Ok(_) => true,
|
||||
@@ -236,7 +236,7 @@ async fn agents_md_paths(
|
||||
for d in search_dirs {
|
||||
for name in &candidate_filenames {
|
||||
let candidate = d.join(name);
|
||||
let candidate_uri = PathUri::from_abs_path(&candidate)?;
|
||||
let candidate_uri = PathUri::from_abs_path(&candidate);
|
||||
match fs.get_metadata(&candidate_uri, /*sandbox*/ None).await {
|
||||
Ok(md) if md.is_file => {
|
||||
found.push(candidate);
|
||||
|
||||
@@ -268,7 +268,6 @@ fn resolved_local_environments<const N: usize>(
|
||||
cwd,
|
||||
/*shell*/ None,
|
||||
)
|
||||
.expect("local cwd URI")
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ async fn read_resolved_agent_role_file(
|
||||
path: &AbsolutePathBuf,
|
||||
role_name_hint: Option<&str>,
|
||||
) -> std::io::Result<ResolvedAgentRoleFile> {
|
||||
let path_uri = PathUri::from_abs_path(path)?;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
let contents = fs.read_file_text(&path_uri, /*sandbox*/ None).await?;
|
||||
let config_base_dir = path.parent().unwrap_or_else(|| path.clone());
|
||||
parse_agent_role_file_contents(
|
||||
@@ -393,7 +393,7 @@ async fn validate_agent_role_config_file(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let config_file_uri = PathUri::from_abs_path(config_file)?;
|
||||
let config_file_uri = PathUri::from_abs_path(config_file);
|
||||
let metadata = fs
|
||||
.get_metadata(&config_file_uri, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -525,7 +525,7 @@ async fn collect_agent_role_files(
|
||||
let mut files = Vec::new();
|
||||
let mut dirs = vec![dir.clone()];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
let dir_uri = PathUri::from_abs_path(&dir)?;
|
||||
let dir_uri = PathUri::from_abs_path(&dir);
|
||||
let entries = match fs.read_directory(&dir_uri, /*sandbox*/ None).await {
|
||||
Ok(entries) => entries,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => continue,
|
||||
|
||||
@@ -3702,7 +3702,7 @@ impl Config {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let path_uri = PathUri::from_abs_path(path)?;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
let contents = fs
|
||||
.read_file_text(&path_uri, /*sandbox*/ None)
|
||||
.await
|
||||
|
||||
@@ -101,7 +101,7 @@ pub(crate) async fn resolve_environment_selections(
|
||||
environment,
|
||||
selected_environment.cwd.clone(),
|
||||
shell,
|
||||
)?);
|
||||
));
|
||||
}
|
||||
Ok(ResolvedTurnEnvironments { turn_environments })
|
||||
}
|
||||
@@ -270,15 +270,12 @@ url = "ws://127.0.0.1:8765"
|
||||
.expect("remote environment"),
|
||||
);
|
||||
let remote = ResolvedTurnEnvironments {
|
||||
turn_environments: vec![
|
||||
TurnEnvironment::new(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
remote_environment.clone(),
|
||||
cwd.clone(),
|
||||
/*shell*/ None,
|
||||
)
|
||||
.expect("remote cwd URI"),
|
||||
],
|
||||
turn_environments: vec![TurnEnvironment::new(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
remote_environment.clone(),
|
||||
cwd.clone(),
|
||||
/*shell*/ None,
|
||||
)],
|
||||
};
|
||||
let multiple = ResolvedTurnEnvironments {
|
||||
turn_environments: vec![
|
||||
@@ -288,8 +285,7 @@ url = "ws://127.0.0.1:8765"
|
||||
remote_environment,
|
||||
cwd.clone(),
|
||||
/*shell*/ None,
|
||||
)
|
||||
.expect("remote cwd URI"),
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -373,14 +373,8 @@ pub fn build_exec_request(
|
||||
"command args are empty",
|
||||
))
|
||||
})?;
|
||||
let cwd = PathUri::from_abs_path(&cwd).map_err(|_| {
|
||||
CodexErr::InvalidRequest("command cwd cannot be represented as a file URI".to_string())
|
||||
})?;
|
||||
let sandbox_policy_cwd_uri = PathUri::from_abs_path(sandbox_cwd).map_err(|_| {
|
||||
CodexErr::InvalidRequest(
|
||||
"sandbox policy cwd cannot be represented as a file URI".to_string(),
|
||||
)
|
||||
})?;
|
||||
let cwd = PathUri::from_abs_path(&cwd);
|
||||
let sandbox_policy_cwd_uri = PathUri::from_abs_path(sandbox_cwd);
|
||||
|
||||
let manager = SandboxManager::new();
|
||||
let command = SandboxCommand {
|
||||
|
||||
@@ -4035,15 +4035,12 @@ fn turn_environments_for_tests(
|
||||
cwd: &codex_utils_absolute_path::AbsolutePathBuf,
|
||||
) -> crate::environment_selection::ResolvedTurnEnvironments {
|
||||
crate::environment_selection::ResolvedTurnEnvironments {
|
||||
turn_environments: vec![
|
||||
TurnEnvironment::new(
|
||||
codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::clone(environment),
|
||||
cwd.clone(),
|
||||
/*shell*/ None,
|
||||
)
|
||||
.expect("turn environment"),
|
||||
],
|
||||
turn_environments: vec![TurnEnvironment::new(
|
||||
codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::clone(environment),
|
||||
cwd.clone(),
|
||||
/*shell*/ None,
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5665,8 +5662,7 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
|
||||
current_environment.environment,
|
||||
environment_cwd.clone(),
|
||||
current_environment.shell,
|
||||
)
|
||||
.expect("environment cwd URI");
|
||||
);
|
||||
|
||||
let call_id = "call-1".to_string();
|
||||
let handler = RequestPermissionsHandler;
|
||||
@@ -6255,15 +6251,15 @@ async fn primary_environment_uses_first_turn_environment() {
|
||||
let first_environment = turn_context.environments.turn_environments[0].clone();
|
||||
#[allow(deprecated)]
|
||||
let second_cwd = turn_context.cwd.join("second");
|
||||
turn_context.environments.turn_environments.push(
|
||||
TurnEnvironment::new(
|
||||
turn_context
|
||||
.environments
|
||||
.turn_environments
|
||||
.push(TurnEnvironment::new(
|
||||
"second".to_string(),
|
||||
Arc::clone(&first_environment.environment),
|
||||
second_cwd.clone(),
|
||||
/*shell*/ None,
|
||||
)
|
||||
.expect("turn environment"),
|
||||
);
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
turn_context
|
||||
|
||||
@@ -56,19 +56,15 @@ impl TurnEnvironment {
|
||||
environment: Arc<Environment>,
|
||||
cwd: AbsolutePathBuf,
|
||||
shell: Option<shell::Shell>,
|
||||
) -> CodexResult<Self> {
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd).map_err(|_| {
|
||||
CodexErr::InvalidRequest(
|
||||
"turn environment cwd cannot be represented as a file URI".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(Self {
|
||||
) -> Self {
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
Self {
|
||||
environment_id,
|
||||
environment,
|
||||
cwd,
|
||||
cwd_uri,
|
||||
shell,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cwd(&self) -> &AbsolutePathBuf {
|
||||
|
||||
@@ -526,12 +526,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).map_err(|_| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"unable to prepare filesystem sandbox: working directory cannot be represented as a file URI"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let sandbox_cwd = PathUri::from_abs_path(cwd);
|
||||
let sandbox =
|
||||
turn.file_system_sandbox_context(/*additional_permissions*/ None, &sandbox_cwd);
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, Some(&sandbox))
|
||||
|
||||
@@ -150,12 +150,7 @@ impl ViewImageHandler {
|
||||
turn_environment.cwd_uri(),
|
||||
);
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
let path_uri = PathUri::from_abs_path(&abs_path).map_err(|error| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"unable to locate image at `{}`: {error}",
|
||||
abs_path.display()
|
||||
))
|
||||
})?;
|
||||
let path_uri = PathUri::from_abs_path(&abs_path);
|
||||
|
||||
let metadata = fs
|
||||
.get_metadata(&path_uri, Some(&sandbox))
|
||||
@@ -295,8 +290,7 @@ mod tests {
|
||||
current.environment,
|
||||
cwd,
|
||||
current.shell,
|
||||
)
|
||||
.expect("image cwd URI");
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -240,11 +240,7 @@ impl ToolOrchestrator {
|
||||
let use_legacy_landlock = turn_ctx.features.use_legacy_landlock();
|
||||
#[allow(deprecated)]
|
||||
let sandbox_cwd = tool.sandbox_cwd(req).unwrap_or(&turn_ctx.cwd);
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(sandbox_cwd).map_err(|_| {
|
||||
ToolError::Codex(CodexErr::InvalidRequest(
|
||||
"sandbox policy cwd cannot be represented as a file URI".to_string(),
|
||||
))
|
||||
})?;
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(sandbox_cwd);
|
||||
let workspace_roots = turn_ctx.config.effective_workspace_roots();
|
||||
let initial_attempt = SandboxAttempt {
|
||||
sandbox: initial_sandbox,
|
||||
|
||||
@@ -22,7 +22,6 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
|
||||
std::env::temp_dir().abs(),
|
||||
/*shell*/ None,
|
||||
)
|
||||
.expect("turn environment")
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -208,7 +207,7 @@ async fn file_system_sandbox_context_uses_active_attempt() {
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
let manager = SandboxManager::new();
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(&path).expect("path URI");
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(&path);
|
||||
let attempt = SandboxAttempt {
|
||||
sandbox: SandboxType::MacosSeatbelt,
|
||||
permissions: &permissions,
|
||||
@@ -237,7 +236,7 @@ async fn file_system_sandbox_context_uses_active_attempt() {
|
||||
assert_eq!(sandbox.permissions, expected_permissions);
|
||||
assert_eq!(
|
||||
sandbox.cwd,
|
||||
Some(codex_utils_path_uri::PathUri::from_abs_path(&path).expect("path URI"))
|
||||
Some(codex_utils_path_uri::PathUri::from_abs_path(&path))
|
||||
);
|
||||
assert_eq!(
|
||||
sandbox.windows_sandbox_level,
|
||||
@@ -266,7 +265,7 @@ async fn no_sandbox_attempt_has_no_file_system_context() {
|
||||
};
|
||||
let permissions = PermissionProfile::Disabled;
|
||||
let manager = SandboxManager::new();
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(&path).expect("path URI");
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(&path);
|
||||
let attempt = SandboxAttempt {
|
||||
sandbox: SandboxType::None,
|
||||
permissions: &permissions,
|
||||
|
||||
@@ -21,7 +21,6 @@ use codex_network_proxy::PROXY_ENV_KEYS;
|
||||
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
|
||||
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxType;
|
||||
@@ -36,8 +35,7 @@ pub(crate) mod shell;
|
||||
pub(crate) mod unified_exec;
|
||||
|
||||
/// Shared helper to construct sandbox transform inputs from a tokenized command line and native
|
||||
/// working directory. Validates that at least a program is present and that the working directory
|
||||
/// has a file URI representation.
|
||||
/// working directory. Validates that at least a program is present.
|
||||
pub(crate) fn build_sandbox_command(
|
||||
command: &[String],
|
||||
cwd: &AbsolutePathBuf,
|
||||
@@ -47,11 +45,7 @@ pub(crate) fn build_sandbox_command(
|
||||
let (program, args) = command
|
||||
.split_first()
|
||||
.ok_or_else(|| ToolError::Rejected("command args are empty".to_string()))?;
|
||||
let cwd = PathUri::from_abs_path(cwd).map_err(|_| {
|
||||
ToolError::Codex(CodexErr::InvalidRequest(
|
||||
"command cwd cannot be represented as a file URI".to_string(),
|
||||
))
|
||||
})?;
|
||||
let cwd = PathUri::from_abs_path(cwd);
|
||||
Ok(SandboxCommand {
|
||||
program: program.clone().into(),
|
||||
args: args.to_vec(),
|
||||
|
||||
@@ -102,8 +102,8 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow::
|
||||
/*additional_permissions*/ None,
|
||||
)
|
||||
.expect("build sandbox command");
|
||||
assert_eq!(command.cwd, PathUri::from_abs_path(&command_cwd)?);
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(&native_sandbox_policy_cwd)?;
|
||||
assert_eq!(command.cwd, PathUri::from_abs_path(&command_cwd));
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(&native_sandbox_policy_cwd);
|
||||
let options = ExecOptions {
|
||||
expiration: ExecExpiration::DefaultTimeout,
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
|
||||
@@ -969,15 +969,8 @@ impl CoreShellCommandExecutor {
|
||||
self.windows_sandbox_level,
|
||||
self.network.is_some(),
|
||||
);
|
||||
let cwd = PathUri::from_abs_path(workdir).map_err(|_| {
|
||||
CodexErr::InvalidRequest("command cwd cannot be represented as a file URI".to_string())
|
||||
})?;
|
||||
let sandbox_policy_cwd =
|
||||
PathUri::from_abs_path(&self.sandbox_policy_cwd).map_err(|_| {
|
||||
CodexErr::InvalidRequest(
|
||||
"sandbox policy cwd cannot be represented as a file URI".to_string(),
|
||||
)
|
||||
})?;
|
||||
let cwd = PathUri::from_abs_path(workdir);
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(&self.sandbox_policy_cwd);
|
||||
let command = SandboxCommand {
|
||||
program: program.clone().into(),
|
||||
args: args.to_vec(),
|
||||
|
||||
@@ -586,8 +586,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
|
||||
),
|
||||
remote_cwd,
|
||||
/*shell*/ None,
|
||||
)
|
||||
.expect("turn environment"),
|
||||
),
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -1005,7 +1005,7 @@ impl TestCodexHarness {
|
||||
}
|
||||
|
||||
pub async fn remove_abs_path(&self, path: &AbsolutePathBuf) -> Result<()> {
|
||||
let path_uri = PathUri::from_abs_path(path)?;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
self.test
|
||||
.fs()
|
||||
.remove(
|
||||
@@ -1021,7 +1021,7 @@ impl TestCodexHarness {
|
||||
}
|
||||
|
||||
pub async fn abs_path_exists(&self, path: &AbsolutePathBuf) -> Result<bool> {
|
||||
let path_uri = PathUri::from_abs_path(path)?;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
match self
|
||||
.test
|
||||
.fs()
|
||||
|
||||
@@ -583,7 +583,7 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result
|
||||
|
||||
test.fs()
|
||||
.remove(
|
||||
&PathUri::from_abs_path(&remote_cwd)?,
|
||||
&PathUri::from_abs_path(&remote_cwd),
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
force: true,
|
||||
@@ -1092,7 +1092,7 @@ async fn remote_test_env_remove_removes_symlink_not_target() -> Result<()> {
|
||||
|
||||
let symlink_exists = file_system
|
||||
.get_metadata(
|
||||
&PathUri::from_abs_path(&absolute_path(symlink_path))?,
|
||||
&PathUri::from_abs_path(&absolute_path(symlink_path)),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -882,7 +882,7 @@ mod tests {
|
||||
std::env::current_exe().expect("current exe").as_path(),
|
||||
)
|
||||
.expect("absolute current exe");
|
||||
let path = codex_utils_path_uri::PathUri::from_abs_path(&path).expect("path URI");
|
||||
let path = codex_utils_path_uri::PathUri::from_abs_path(&path);
|
||||
let sandbox = crate::FileSystemSandboxContext::from_permission_profile(
|
||||
codex_protocol::models::PermissionProfile::from_runtime_permissions(
|
||||
&codex_protocol::permissions::FileSystemSandboxPolicy::restricted(Vec::new()),
|
||||
|
||||
@@ -145,11 +145,7 @@ fn sandbox_cwd(sandbox: &FileSystemSandboxContext) -> Result<SandboxCwd, JSONRPC
|
||||
|
||||
let native = AbsolutePathBuf::from_absolute_path(current_sandbox_cwd().map_err(io_error)?)
|
||||
.map_err(|err| invalid_request(format!("current directory is not absolute: {err}")))?;
|
||||
let uri = PathUri::from_abs_path(&native).map_err(|err| {
|
||||
invalid_request(format!(
|
||||
"current directory cannot be represented as a file URI: {err}"
|
||||
))
|
||||
})?;
|
||||
let uri = PathUri::from_abs_path(&native);
|
||||
Ok(SandboxCwd { uri, native })
|
||||
}
|
||||
|
||||
@@ -519,7 +515,7 @@ mod tests {
|
||||
.expect("runtime paths");
|
||||
let runner = FileSystemSandboxRunner::new(runtime_paths);
|
||||
let native_cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||
let cwd = PathUri::from_abs_path(&native_cwd).expect("cwd URI");
|
||||
let cwd = PathUri::from_abs_path(&native_cwd);
|
||||
let file_system_policy =
|
||||
restricted_policy(vec![path_entry(native_cwd, FileSystemAccessMode::Write)]);
|
||||
let network_policy = NetworkSandboxPolicy::Restricted;
|
||||
@@ -538,7 +534,7 @@ mod tests {
|
||||
fn sandbox_cwd_uses_context_cwd() {
|
||||
let native_cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path())
|
||||
.expect("absolute cwd");
|
||||
let cwd = PathUri::from_abs_path(&native_cwd).expect("cwd URI");
|
||||
let cwd = PathUri::from_abs_path(&native_cwd);
|
||||
let policy = restricted_policy(vec![special_entry(
|
||||
FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
FileSystemAccessMode::Write,
|
||||
|
||||
@@ -423,7 +423,7 @@ impl DirectFileSystem {
|
||||
let path = path.to_abs_path()?;
|
||||
let canonicalized =
|
||||
AbsolutePathBuf::from_absolute_path(tokio::fs::canonicalize(path.as_path()).await?)?;
|
||||
PathUri::from_abs_path(&canonicalized)
|
||||
Ok(PathUri::from_abs_path(&canonicalized))
|
||||
}
|
||||
|
||||
async fn read_file(
|
||||
|
||||
@@ -404,6 +404,6 @@ mod tests {
|
||||
}
|
||||
|
||||
fn path_uri(name: &str) -> PathUri {
|
||||
PathUri::from_abs_path(&absolute_test_path(name)).expect("path URI")
|
||||
PathUri::from_abs_path(&absolute_test_path(name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,12 +341,7 @@ async fn image_url(
|
||||
path: &AbsolutePathBuf,
|
||||
environment: &ToolEnvironment,
|
||||
) -> Result<ImageUrl, FunctionCallError> {
|
||||
let path_uri = PathUri::from_abs_path(path).map_err(|error| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"unable to read referenced image at `{}`: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
let path_uri = PathUri::from_abs_path(path);
|
||||
let bytes = environment
|
||||
.file_system
|
||||
.read_file(&path_uri, Some(&environment.file_system_sandbox_context))
|
||||
|
||||
@@ -131,12 +131,7 @@ impl SkillProvider for ExecutorSkillProvider {
|
||||
"executor skill resource references unavailable environment `{environment_id}`"
|
||||
)));
|
||||
};
|
||||
let resource_path = PathUri::from_abs_path(resource_path).map_err(|err| {
|
||||
SkillProviderError::new(format!(
|
||||
"failed to read executor skill resource {}: {err}",
|
||||
request.resource.as_str()
|
||||
))
|
||||
})?;
|
||||
let resource_path = PathUri::from_abs_path(resource_path);
|
||||
let contents = environment
|
||||
.get_filesystem()
|
||||
.read_file_text(&resource_path, /*sandbox*/ None)
|
||||
|
||||
@@ -38,10 +38,10 @@ impl SyntheticFileSystem {
|
||||
async fn canonicalize(&self, path: &PathUri) -> io::Result<PathUri> {
|
||||
let path = path.to_abs_path()?;
|
||||
if path == self.alias_root {
|
||||
return PathUri::from_abs_path(&self.canonical_root);
|
||||
return Ok(PathUri::from_abs_path(&self.canonical_root));
|
||||
}
|
||||
self.metadata(&path)?;
|
||||
PathUri::from_abs_path(&path)
|
||||
Ok(PathUri::from_abs_path(&path))
|
||||
}
|
||||
|
||||
async fn read_file(&self, path: &PathUri) -> io::Result<Vec<u8>> {
|
||||
|
||||
@@ -47,7 +47,7 @@ pub async fn get_git_repo_root_with_fs(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
let cwd_uri = PathUri::from_abs_path(cwd).ok()?;
|
||||
let cwd_uri = PathUri::from_abs_path(cwd);
|
||||
let base = match fs.get_metadata(&cwd_uri, /*sandbox*/ None).await {
|
||||
Ok(metadata) if metadata.is_directory => cwd.clone(),
|
||||
_ => cwd.parent()?,
|
||||
@@ -805,7 +805,7 @@ pub async fn resolve_root_git_project_for_trust(
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
let repo_root = get_git_repo_root_with_fs(fs, cwd).await?;
|
||||
let dot_git = repo_root.join(".git");
|
||||
let dot_git_uri = PathUri::from_abs_path(&dot_git).ok()?;
|
||||
let dot_git_uri = PathUri::from_abs_path(&dot_git);
|
||||
if fs
|
||||
.get_metadata(&dot_git_uri, /*sandbox*/ None)
|
||||
.await
|
||||
@@ -859,7 +859,7 @@ async fn find_ancestor_git_entry_with_fs(
|
||||
) -> Option<(AbsolutePathBuf, AbsolutePathBuf)> {
|
||||
for dir in base_dir.ancestors() {
|
||||
let dot_git = dir.join(".git");
|
||||
let dot_git_uri = PathUri::from_abs_path(&dot_git).ok()?;
|
||||
let dot_git_uri = PathUri::from_abs_path(&dot_git);
|
||||
if fs
|
||||
.get_metadata(&dot_git_uri, /*sandbox*/ None)
|
||||
.await
|
||||
|
||||
@@ -75,7 +75,7 @@ fn restricted_file_system_uses_platform_sandbox_without_managed_network() {
|
||||
fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() {
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd).expect("cwd URI");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let permissions = PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
@@ -117,7 +117,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
|
||||
fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd).expect("cwd URI");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let permissions = PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
};
|
||||
@@ -171,7 +171,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
fn transform_additional_permissions_preserves_denied_entries() {
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd).expect("cwd URI");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let workspace_root = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
||||
@@ -297,7 +297,7 @@ fn transform_linux_seccomp_request(
|
||||
) -> super::SandboxExecRequest {
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd).expect("cwd URI");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let permissions = PermissionProfile::Disabled;
|
||||
manager
|
||||
.transform(SandboxTransformRequest {
|
||||
|
||||
@@ -8,6 +8,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! See [`PathUri`] for scheme, normalization, and serialization behavior.
|
||||
|
||||
use base64::Engine;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -17,13 +18,15 @@ use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
pub const FILE_SCHEME: &str = "file";
|
||||
const BAD_PATH_URI_PREFIX: &str = "file:///%00/bad/path/";
|
||||
|
||||
/// An immutable, cross-platform representation of a `file:` URI.
|
||||
///
|
||||
/// Only the `file:` scheme is currently accepted. Construction validates the
|
||||
/// URL, and the URI cannot be mutated after construction. [`Self::basename`],
|
||||
/// [`Self::parent`], and [`Self::join`] operate on URI path segments without
|
||||
/// interpreting them using the operating system running Codex.
|
||||
/// interpreting them using the operating system running Codex. Fallback URIs
|
||||
/// created by [`Self::from_abs_path`] are opaque to these lexical operations.
|
||||
///
|
||||
/// `file:` paths retain their URI spelling so they can be parsed independently
|
||||
/// of the current host. In particular, `/C:/src` remains ambiguous between a
|
||||
@@ -57,27 +60,53 @@ impl PathUri {
|
||||
|
||||
/// Converts an absolute path on the current host to a `file:` URI.
|
||||
///
|
||||
/// On Windows, paths without a URI representation, including `\\.\` device
|
||||
/// paths and generic `\\?\` verbatim namespaces, are reported as invalid
|
||||
/// input.
|
||||
pub fn from_abs_path(path: &AbsolutePathBuf) -> io::Result<Self> {
|
||||
let url = Url::from_file_path(path.as_path()).map_err(|()| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
PathUriParseError::InvalidFileUriPath,
|
||||
)
|
||||
})?;
|
||||
Self::try_from(url).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))
|
||||
/// Paths without a valid URI representation are replaced by
|
||||
/// `file:///%00/bad/path/<base64>`, where `<base64>` is the URL-safe, unpadded
|
||||
/// encoding of the original path (Unix bytes or Windows UTF-16LE). This
|
||||
/// includes paths containing nulls and, on Windows, unsupported prefix
|
||||
/// kinds such as device and generic verbatim namespaces, non-Unicode path
|
||||
/// or UNC components, and UNC server names that are not valid URL hosts.
|
||||
/// The encoded null reserves a URI namespace that cannot collide with a
|
||||
/// real path on Unix or Windows.
|
||||
pub fn from_abs_path(path: &AbsolutePathBuf) -> Self {
|
||||
if let Ok(url) = Url::from_file_path(path.as_path())
|
||||
&& let Ok(uri) = Self::try_from(url)
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let encoded_path = {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.encode(path.as_path().as_os_str().as_bytes())
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let encoded_path = {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
let path_bytes = path
|
||||
.as_path()
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.flat_map(u16::to_le_bytes)
|
||||
.collect::<Vec<_>>();
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(path_bytes)
|
||||
};
|
||||
let Ok(uri) = Self::parse(&format!("{BAD_PATH_URI_PREFIX}{encoded_path}")) else {
|
||||
unreachable!("URL-safe base64 always produces a valid fallback path URI");
|
||||
};
|
||||
uri
|
||||
}
|
||||
|
||||
/// Converts a path on the current host to a `file:` URI.
|
||||
///
|
||||
/// Relative paths and paths without a URI representation are reported as
|
||||
/// invalid input.
|
||||
/// Relative paths are reported as invalid input. Absolute paths without a
|
||||
/// valid URI representation use the fallback documented on
|
||||
/// [`Self::from_abs_path`].
|
||||
pub fn from_path(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let path = AbsolutePathBuf::from_absolute_path_checked(path)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||
Self::from_abs_path(&path)
|
||||
Ok(Self::from_abs_path(&path))
|
||||
}
|
||||
|
||||
/// Returns the percent-encoded URI path.
|
||||
@@ -88,20 +117,26 @@ impl PathUri {
|
||||
self.0.path()
|
||||
}
|
||||
|
||||
/// Returns the decoded final URI path segment, or `None` for the URI root.
|
||||
/// Returns the decoded final URI path segment, or `None` for the URI root
|
||||
/// or an opaque fallback URI created by [`Self::from_abs_path`].
|
||||
///
|
||||
/// If the segment contains non-UTF-8 encoded bytes, its percent-encoded
|
||||
/// spelling is returned instead.
|
||||
pub fn basename(&self) -> Option<String> {
|
||||
if decode_bad_path_uri(&self.0).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.0
|
||||
.path_segments()?
|
||||
.rfind(|segment| !segment.is_empty())
|
||||
.map(decode_uri_path)
|
||||
}
|
||||
|
||||
/// Returns the parent URI, or `None` for the URI root.
|
||||
/// 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> {
|
||||
if self.encoded_path() == "/" {
|
||||
if self.encoded_path() == "/" || decode_bad_path_uri(&self.0).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -122,6 +157,8 @@ impl PathUri {
|
||||
/// without escaping the URI root. Literal `%`, `?`, and `#` characters are
|
||||
/// percent-encoded as filename text. Paths containing a null character are
|
||||
/// rejected because they cannot be safely converted to native paths.
|
||||
/// Opaque fallback URIs created by [`Self::from_abs_path`] reject non-empty
|
||||
/// joins.
|
||||
pub fn join(&self, path: &str) -> Result<Self, PathUriParseError> {
|
||||
if path.starts_with('/') {
|
||||
return Err(PathUriParseError::JoinPathMustBeRelative(path.to_string()));
|
||||
@@ -132,6 +169,9 @@ impl PathUri {
|
||||
if path.is_empty() {
|
||||
return Ok(self.clone());
|
||||
}
|
||||
if decode_bad_path_uri(&self.0).is_some() {
|
||||
return Err(PathUriParseError::InvalidFileUriPath);
|
||||
}
|
||||
|
||||
let mut url = self.0.clone();
|
||||
{
|
||||
@@ -157,13 +197,46 @@ 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. 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.
|
||||
/// [`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.
|
||||
pub fn to_abs_path(&self) -> io::Result<AbsolutePathBuf> {
|
||||
if let Some(path_bytes) = decode_bad_path_uri(&self.0) {
|
||||
#[cfg(unix)]
|
||||
let decoded_path = {
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
Some(std::path::PathBuf::from(std::ffi::OsString::from_vec(
|
||||
path_bytes,
|
||||
)))
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let decoded_path = {
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
path_bytes.len().is_multiple_of(2).then(|| {
|
||||
let path_wide = path_bytes
|
||||
.chunks_exact(2)
|
||||
.map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]]))
|
||||
.collect::<Vec<_>>();
|
||||
std::path::PathBuf::from(std::ffi::OsString::from_wide(&path_wide))
|
||||
})
|
||||
};
|
||||
if let Some(decoded_path) = decoded_path
|
||||
&& let Ok(path) = AbsolutePathBuf::from_absolute_path_checked(decoded_path)
|
||||
&& Self::from_abs_path(&path).eq(self)
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
PathUriParseError::InvalidFileUriPath,
|
||||
));
|
||||
}
|
||||
|
||||
let path = self.0.to_file_path().map_err(|()| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
@@ -232,7 +305,7 @@ impl<'de> Deserialize<'de> for PathUri {
|
||||
.map_or_else(|| path_error.to_string(), |error| error.to_string()),
|
||||
)
|
||||
})?;
|
||||
Self::from_abs_path(&path).map_err(serde::de::Error::custom)
|
||||
Ok(Self::from_abs_path(&path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +363,20 @@ fn decode_uri_path(path: &str) -> String {
|
||||
.unwrap_or_else(|_| path.to_string())
|
||||
}
|
||||
|
||||
/// Returns the original platform path bytes from a canonical bad-path URI.
|
||||
fn decode_bad_path_uri(url: &Url) -> Option<Vec<u8>> {
|
||||
let encoded_path = url.as_str().strip_prefix(BAD_PATH_URI_PREFIX)?;
|
||||
if encoded_path.is_empty() || encoded_path.contains('/') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(encoded_path)
|
||||
.ok()?;
|
||||
(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&path_bytes) == encoded_path)
|
||||
.then_some(path_bytes)
|
||||
}
|
||||
|
||||
/// Rejects URI metadata that has no defined meaning for `file:` URIs.
|
||||
fn validate_common_known_uri(url: &Url) -> Result<(), PathUriParseError> {
|
||||
if !url.username().is_empty() || url.password().is_some() {
|
||||
@@ -312,7 +399,9 @@ fn validate_file_url(url: &Url) -> Result<(), PathUriParseError> {
|
||||
validate_common_known_uri(url)?;
|
||||
// `Url` accepts `%00`, but native path APIs use null as a terminator and
|
||||
// `Url::to_file_path` cannot represent a decoded null byte.
|
||||
if urlencoding::decode_binary(url.path().as_bytes()).contains(&0) {
|
||||
if urlencoding::decode_binary(url.path().as_bytes()).contains(&0)
|
||||
&& decode_bad_path_uri(url).is_none()
|
||||
{
|
||||
return Err(PathUriParseError::InvalidFileUriPath);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use super::*;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(windows)]
|
||||
use std::ffi::OsString;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
#[cfg(unix)]
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
@@ -12,7 +15,7 @@ fn file_uri_round_trips_an_absolute_path() {
|
||||
.expect("current directory")
|
||||
.join("a path/file.rs");
|
||||
|
||||
let uri = PathUri::from_abs_path(&path).expect("path should convert to a file URI");
|
||||
let uri = PathUri::from_abs_path(&path);
|
||||
|
||||
let uri_string = uri.to_string();
|
||||
assert!(uri_string.starts_with("file:"));
|
||||
@@ -57,24 +60,136 @@ fn file_uri_parses_a_windows_path_on_any_host() {
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn file_uri_rejects_windows_prefixes_without_a_uri_representation() {
|
||||
for native_path in [
|
||||
r"\\.\COM1",
|
||||
r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.rs",
|
||||
fn file_uri_falls_back_for_windows_prefixes_without_a_uri_representation() {
|
||||
for (native_path, expected_uri) in [
|
||||
(r"\\.\COM1", "file:///%00/bad/path/XABcAC4AXABDAE8ATQAxAFwA"),
|
||||
(
|
||||
r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.rs",
|
||||
"file:///%00/bad/path/XABcAD8AXABWAG8AbAB1AG0AZQB7ADAAMAAwADAAMAAwADAAMAAtADAAMAAwADAALQAwADAAMAAwAC0AMAAwADAAMAAtADAAMAAwADAAMAAwADAAMAAwADAAMAAwAH0AXABmAGkAbABlAC4AcgBzAA",
|
||||
),
|
||||
] {
|
||||
let path = AbsolutePathBuf::from_absolute_path_checked(native_path)
|
||||
.expect("Windows namespace path should be absolute");
|
||||
|
||||
let uri = PathUri::from_abs_path(&path);
|
||||
|
||||
assert_eq!(uri.to_string(), expected_uri, "converting {native_path}");
|
||||
assert_eq!(
|
||||
PathUri::from_abs_path(&path)
|
||||
.expect_err("Windows namespace path should not convert")
|
||||
.kind(),
|
||||
io::ErrorKind::InvalidInput,
|
||||
"converting {native_path}"
|
||||
PathUri::parse(&uri.to_string())
|
||||
.expect("fallback URI should parse")
|
||||
.to_abs_path()
|
||||
.expect("fallback URI should decode"),
|
||||
path,
|
||||
"round-tripping {native_path}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn file_uri_fallback_round_trips_non_unicode_windows_paths() {
|
||||
let path_wide = r"C:\bad\"
|
||||
.encode_utf16()
|
||||
.chain([0xd800])
|
||||
.collect::<Vec<_>>();
|
||||
let path = PathBuf::from(OsString::from_wide(&path_wide));
|
||||
let path = AbsolutePathBuf::from_absolute_path_checked(path).expect("absolute Windows path");
|
||||
|
||||
let uri = PathUri::from_abs_path(&path);
|
||||
let reparsed = PathUri::parse(&uri.to_string()).expect("fallback URI should parse");
|
||||
|
||||
assert!(uri.to_string().starts_with(BAD_PATH_URI_PREFIX));
|
||||
assert_eq!(
|
||||
reparsed.to_abs_path().expect("fallback URI should decode"),
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn file_uri_falls_back_for_posix_paths_with_null_bytes() {
|
||||
let path = PathBuf::from(std::ffi::OsString::from_vec(
|
||||
b"/tmp/null-\0-\xff-byte".to_vec(),
|
||||
));
|
||||
let path = AbsolutePathBuf::from_absolute_path_checked(path).expect("absolute POSIX path");
|
||||
|
||||
let uri = PathUri::from_abs_path(&path);
|
||||
|
||||
assert_eq!(
|
||||
uri,
|
||||
PathUri::parse("file:///%00/bad/path/L3RtcC9udWxsLQAt_y1ieXRl")
|
||||
.expect("valid fallback URI")
|
||||
);
|
||||
let json = serde_json::to_string(&uri).expect("fallback URI should serialize");
|
||||
let reparsed: PathUri =
|
||||
serde_json::from_str(&json).expect("serialized fallback URI should parse");
|
||||
assert_eq!(json, r#""file:///%00/bad/path/L3RtcC9udWxsLQAt_y1ieXRl""#);
|
||||
assert_eq!(reparsed, uri);
|
||||
assert_eq!(
|
||||
reparsed.to_abs_path().expect("fallback URI should decode"),
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ordinary_bad_path_uri_is_not_decoded_as_a_fallback() {
|
||||
let path = AbsolutePathBuf::from_absolute_path_checked("/bad/path/L3RtcC9udWxsLQAt_y1ieXRl")
|
||||
.expect("absolute POSIX path");
|
||||
let uri = PathUri::from_abs_path(&path);
|
||||
|
||||
assert_eq!(uri.to_string(), "file:///bad/path/L3RtcC9udWxsLQAt_y1ieXRl");
|
||||
assert_eq!(
|
||||
uri.to_abs_path().expect("URI should convert literally"),
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_bad_path_uris_are_rejected() {
|
||||
for uri in [
|
||||
"file:///%00/bad/path/",
|
||||
"file:///%00/bad/path/not*base64",
|
||||
"file:///%00/bad/path/YQ==",
|
||||
"file:///%00/bad/path/YR",
|
||||
"file:///%00/bad/path/YQ/extra",
|
||||
"file:///%00/other/YQ",
|
||||
] {
|
||||
assert_eq!(
|
||||
PathUri::parse(uri),
|
||||
Err(PathUriParseError::InvalidFileUriPath),
|
||||
"parsing {uri}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structurally_valid_bad_path_uri_with_invalid_native_payload_fails_conversion() {
|
||||
let uri = PathUri::parse("file:///%00/bad/path/YQ")
|
||||
.expect("canonical base64 fallback URI should parse");
|
||||
|
||||
assert_eq!(
|
||||
uri.to_abs_path()
|
||||
.expect_err("relative fallback payload should not convert")
|
||||
.kind(),
|
||||
io::ErrorKind::InvalidInput
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_path_uris_are_opaque_to_lexical_operations() {
|
||||
let uri = PathUri::parse("file:///%00/bad/path/YQ")
|
||||
.expect("canonical base64 fallback URI should parse");
|
||||
|
||||
assert_eq!(uri.basename(), None);
|
||||
assert_eq!(uri.parent(), None);
|
||||
assert_eq!(uri.join(""), Ok(uri.clone()));
|
||||
assert_eq!(
|
||||
uri.join("child"),
|
||||
Err(PathUriParseError::InvalidFileUriPath)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_uri_parses_a_posix_path_on_any_host() {
|
||||
let uri = PathUri::parse("file:///home/alice/src/main.rs")
|
||||
@@ -101,7 +216,7 @@ fn file_uri_accepts_non_utf8_posix_paths() {
|
||||
let path = PathBuf::from(std::ffi::OsString::from_vec(b"/tmp/non-utf8-\xff".to_vec()));
|
||||
let path = AbsolutePathBuf::from_absolute_path_checked(path).expect("absolute POSIX path");
|
||||
|
||||
let uri = PathUri::from_abs_path(&path).expect("non-UTF-8 path should convert to a file URI");
|
||||
let uri = PathUri::from_abs_path(&path);
|
||||
assert_eq!(
|
||||
uri.to_abs_path()
|
||||
.expect("URI should convert to native path"),
|
||||
@@ -127,7 +242,7 @@ fn file_uri_round_trips_literal_percent_characters() {
|
||||
fn file_uri_round_trips_windows_unc_paths() {
|
||||
let path = AbsolutePathBuf::from_absolute_path_checked(r"\\server\share\src\main.rs")
|
||||
.expect("absolute UNC path");
|
||||
let uri = PathUri::from_abs_path(&path).expect("UNC path should convert to a file URI");
|
||||
let uri = PathUri::from_abs_path(&path);
|
||||
|
||||
assert_eq!(uri.encoded_path(), "/share/src/main.rs");
|
||||
assert_eq!(uri.to_abs_path().expect("UNC URI should convert"), path);
|
||||
@@ -198,10 +313,7 @@ fn path_uri_deserializes_legacy_absolute_paths() {
|
||||
let json = serde_json::to_string(&path).expect("absolute path should serialize");
|
||||
let uri: PathUri = serde_json::from_str(&json).expect("legacy absolute path should parse");
|
||||
|
||||
assert_eq!(
|
||||
uri,
|
||||
PathUri::from_abs_path(&path).expect("expected file URI")
|
||||
);
|
||||
assert_eq!(uri, PathUri::from_abs_path(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -31,7 +31,7 @@ async fn plugin_manifest_name(
|
||||
let mut manifest_path = None;
|
||||
for relative_path in DISCOVERABLE_PLUGIN_MANIFEST_PATHS {
|
||||
let candidate = plugin_root.join(relative_path);
|
||||
let candidate_uri = PathUri::from_abs_path(&candidate).ok()?;
|
||||
let candidate_uri = PathUri::from_abs_path(&candidate);
|
||||
match fs.get_metadata(&candidate_uri, /*sandbox*/ None).await {
|
||||
Ok(metadata) if metadata.is_file => {
|
||||
manifest_path = Some(candidate);
|
||||
@@ -41,7 +41,7 @@ async fn plugin_manifest_name(
|
||||
}
|
||||
}
|
||||
let manifest_path = manifest_path?;
|
||||
let manifest_path_uri = PathUri::from_abs_path(&manifest_path).ok()?;
|
||||
let manifest_path_uri = PathUri::from_abs_path(&manifest_path);
|
||||
let contents = fs
|
||||
.read_file_text(&manifest_path_uri, /*sandbox*/ None)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user