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:
Michael Bolin
2025-12-09 17:37:52 -08:00
committed by GitHub
parent 0c8828c5e2
commit fa4cac1e6b
10 changed files with 246 additions and 62 deletions

View 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()
);
}
}