mirror of
https://github.com/openai/codex.git
synced 2026-04-27 08:05:51 +00:00
fix: introduce AbsolutePathBuf and resolve relative paths in config.toml (#7796)
This PR attempts to solve two problems by introducing a `AbsolutePathBuf` type with a special deserializer: - `AbsolutePathBuf` attempts to be a generally useful abstraction, as it ensures, by constructing, that it represents a value that is an absolute, normalized path, which is a stronger guarantee than an arbitrary `PathBuf`. - Values in `config.toml` that can be either an absolute or relative path should be resolved against the folder containing the `config.toml` in the relative path case. This PR makes this easy to support: the main cost is ensuring `AbsolutePathBufGuard` is used inside `deserialize_config_toml_with_base()`. While `AbsolutePathBufGuard` may seem slightly distasteful because it relies on thread-local storage, this seems much cleaner to me than using than my various experiments with https://docs.rs/serde/latest/serde/de/trait.DeserializeSeed.html. Further, since the `deserialize()` method from the `Deserialize` trait is not async, we do not really have to worry about the deserialization work being spread across multiple threads in a way that would interfere with `AbsolutePathBufGuard`. To start, this PR introduces the use of `AbsolutePathBuf` in `OtelTlsConfig`. Note how this simplifies `otel_provider.rs` because it no longer requires `settings.codex_home` to be threaded through. Furthermore, this sets us up better for a world where multiple `config.toml` files from different folders could be loaded and then merged together, as the absolutifying of the paths must be done against the correct parent folder.
This commit is contained in:
152
codex-rs/utils/absolute-path/src/lib.rs
Normal file
152
codex-rs/utils/absolute-path/src/lib.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use path_absolutize::Absolutize;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::de::Error as SerdeError;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Display;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A path that is guaranteed to be absolute and normalized (though it is not
|
||||
/// guaranteed to be canonicalized or exist on the filesystem).
|
||||
///
|
||||
/// IMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set
|
||||
/// using `AbsolutePathBufGuard::new(base_path)`. If no base path is set, the
|
||||
/// deserialization will fail unless the path being deserialized is already
|
||||
/// absolute.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct AbsolutePathBuf(PathBuf);
|
||||
|
||||
impl AbsolutePathBuf {
|
||||
pub fn resolve_path_against_base<P, B>(path: P, base_path: B) -> std::io::Result<Self>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let absolute_path = path.as_ref().absolutize_from(base_path.as_ref())?;
|
||||
Ok(Self(absolute_path.into_owned()))
|
||||
}
|
||||
|
||||
pub fn from_absolute_path<P>(path: P) -> std::io::Result<Self>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let absolute_path = path.as_ref().absolutize()?;
|
||||
Ok(Self(absolute_path.into_owned()))
|
||||
}
|
||||
|
||||
pub fn as_path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn into_path_buf(self) -> PathBuf {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn to_path_buf(&self) -> PathBuf {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
pub fn display(&self) -> Display<'_> {
|
||||
self.0.display()
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static ABSOLUTE_PATH_BASE: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub struct AbsolutePathBufGuard;
|
||||
|
||||
impl AbsolutePathBufGuard {
|
||||
pub fn new(base_path: &Path) -> Self {
|
||||
ABSOLUTE_PATH_BASE.with(|cell| {
|
||||
*cell.borrow_mut() = Some(base_path.to_path_buf());
|
||||
});
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AbsolutePathBufGuard {
|
||||
fn drop(&mut self) {
|
||||
ABSOLUTE_PATH_BASE.with(|cell| {
|
||||
*cell.borrow_mut() = None;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AbsolutePathBuf {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let path = PathBuf::deserialize(deserializer)?;
|
||||
ABSOLUTE_PATH_BASE.with(|cell| match cell.borrow().as_deref() {
|
||||
Some(base) => {
|
||||
Ok(Self::resolve_path_against_base(path, base).map_err(SerdeError::custom)?)
|
||||
}
|
||||
None if path.is_absolute() => {
|
||||
Self::from_absolute_path(path).map_err(SerdeError::custom)
|
||||
}
|
||||
None => Err(SerdeError::custom(
|
||||
"AbsolutePathBuf deserialized without a base path",
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for AbsolutePathBuf {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AbsolutePathBuf> for PathBuf {
|
||||
fn from(path: AbsolutePathBuf) -> Self {
|
||||
path.into_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn create_with_absolute_path_ignores_base_path() {
|
||||
let base_dir = tempdir().expect("base dir");
|
||||
let absolute_dir = tempdir().expect("absolute dir");
|
||||
let base_path = base_dir.path();
|
||||
let absolute_path = absolute_dir.path().join("file.txt");
|
||||
let abs_path_buf =
|
||||
AbsolutePathBuf::resolve_path_against_base(absolute_path.clone(), base_path)
|
||||
.expect("failed to create");
|
||||
assert_eq!(abs_path_buf.as_path(), absolute_path.as_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_path_is_resolved_against_base_path() {
|
||||
let temp_dir = tempdir().expect("base dir");
|
||||
let base_dir = temp_dir.path();
|
||||
let abs_path_buf = AbsolutePathBuf::resolve_path_against_base("file.txt", base_dir)
|
||||
.expect("failed to create");
|
||||
assert_eq!(abs_path_buf.as_path(), base_dir.join("file.txt").as_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_used_in_deserialization() {
|
||||
let temp_dir = tempdir().expect("base dir");
|
||||
let base_dir = temp_dir.path();
|
||||
let relative_path = "subdir/file.txt";
|
||||
let abs_path_buf = {
|
||||
let _guard = AbsolutePathBufGuard::new(base_dir);
|
||||
serde_json::from_str::<AbsolutePathBuf>(&format!(r#""{relative_path}""#))
|
||||
.expect("failed to deserialize")
|
||||
};
|
||||
assert_eq!(
|
||||
abs_path_buf.as_path(),
|
||||
base_dir.join(relative_path).as_path()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user