Use AbsolutePathBuf in skill loading and codex_home (#17407)

Helps with FS migration later
This commit is contained in:
pakrym-oai
2026-04-13 10:26:51 -07:00
committed by GitHub
parent d25a9822a7
commit ac82443d07
86 changed files with 850 additions and 625 deletions

View File

@@ -19,7 +19,7 @@ mod absolutize;
/// using [AbsolutePathBufGuard::new]. If no base path is set, the
/// deserialization will fail unless the path being deserialized is already
/// absolute.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, JsonSchema, TS)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, JsonSchema, TS)]
pub struct AbsolutePathBuf(PathBuf);
impl AbsolutePathBuf {
@@ -54,6 +54,18 @@ impl AbsolutePathBuf {
Ok(Self(absolutize::absolutize(&expanded)?))
}
pub fn from_absolute_path_checked<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
let expanded = Self::maybe_expand_home_directory(path.as_ref());
if !expanded.is_absolute() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("path is not absolute: {}", path.as_ref().display()),
));
}
Ok(Self(absolutize::absolutize_from(&expanded, Path::new("/"))))
}
pub fn current_dir() -> std::io::Result<Self> {
let current_dir = std::env::current_dir()?;
Ok(Self(absolutize::absolutize_from(
@@ -75,6 +87,10 @@ impl AbsolutePathBuf {
Self::resolve_path_against_base(path, &self.0)
}
pub fn canonicalize(&self) -> std::io::Result<Self> {
dunce::canonicalize(&self.0).map(Self)
}
pub fn parent(&self) -> Option<Self> {
self.0.parent().map(|p| {
debug_assert!(
@@ -85,6 +101,16 @@ impl AbsolutePathBuf {
})
}
pub fn ancestors(&self) -> impl Iterator<Item = Self> + '_ {
self.0.ancestors().map(|p| {
debug_assert!(
p.is_absolute(),
"ancestor of AbsolutePathBuf must be absolute"
);
Self(p.to_path_buf())
})
}
pub fn as_path(&self) -> &Path {
&self.0
}
@@ -174,6 +200,24 @@ pub mod test_support {
use std::path::Path;
use std::path::PathBuf;
/// Creates a platform-absolute [`PathBuf`] from a Unix-style absolute test path.
///
/// On Windows, `/tmp/example` maps to `C:\tmp\example`.
pub fn test_path_buf(unix_path: &str) -> PathBuf {
if cfg!(windows) {
let mut path = PathBuf::from(r"C:\");
path.extend(
unix_path
.trim_start_matches('/')
.split('/')
.filter(|segment| !segment.is_empty()),
);
path
} else {
PathBuf::from(unix_path)
}
}
/// Extension methods for converting paths into [`AbsolutePathBuf`] values in tests.
pub trait PathExt {
/// Converts an already absolute path into an [`AbsolutePathBuf`].
@@ -183,7 +227,8 @@ pub mod test_support {
impl PathExt for Path {
#[expect(clippy::expect_used)]
fn abs(&self) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(self).expect("path should already be absolute")
AbsolutePathBuf::from_absolute_path_checked(self)
.expect("path should already be absolute")
}
}
@@ -280,7 +325,9 @@ impl<'de> Deserialize<'de> for AbsolutePathBuf {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::test_path_buf;
use pretty_assertions::assert_eq;
use std::fs;
use tempfile::tempdir;
#[test]
@@ -294,6 +341,14 @@ mod tests {
assert_eq!(abs_path_buf.as_path(), absolute_path.as_path());
}
#[test]
fn from_absolute_path_checked_rejects_relative_path() {
let err = AbsolutePathBuf::from_absolute_path_checked("relative/path")
.expect_err("relative path should fail");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn relative_path_is_resolved_against_base_path() {
let temp_dir = tempdir().expect("base dir");
@@ -311,6 +366,56 @@ mod tests {
assert_eq!(abs_path_buf.as_path(), base_dir.join("file.txt").as_path());
}
#[test]
fn canonicalize_returns_absolute_path_buf() {
let temp_dir = tempdir().expect("base dir");
fs::create_dir(temp_dir.path().join("one")).expect("create one dir");
fs::create_dir(temp_dir.path().join("two")).expect("create two dir");
fs::write(temp_dir.path().join("two").join("file.txt"), "").expect("write file");
let abs_path_buf =
AbsolutePathBuf::from_absolute_path(temp_dir.path().join("one/../two/./file.txt"))
.expect("absolute path");
assert_eq!(
abs_path_buf
.canonicalize()
.expect("path should canonicalize")
.as_path(),
dunce::canonicalize(temp_dir.path().join("two").join("file.txt"))
.expect("expected path should canonicalize")
.as_path()
);
}
#[test]
fn canonicalize_returns_error_for_missing_path() {
let temp_dir = tempdir().expect("base dir");
let abs_path_buf = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("missing.txt"))
.expect("absolute path");
assert!(abs_path_buf.canonicalize().is_err());
}
#[test]
fn ancestors_returns_absolute_path_bufs() {
let abs_path_buf =
AbsolutePathBuf::from_absolute_path_checked(test_path_buf("/tmp/one/two"))
.expect("absolute path");
let ancestors = abs_path_buf
.ancestors()
.map(|path| path.to_path_buf())
.collect::<Vec<_>>();
let expected = vec![
test_path_buf("/tmp/one/two"),
test_path_buf("/tmp/one"),
test_path_buf("/tmp"),
test_path_buf("/"),
];
assert_eq!(ancestors, expected);
}
#[test]
fn relative_to_current_dir_resolves_relative_path() -> std::io::Result<()> {
let current_dir = std::env::current_dir()?;