Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:32:24.342Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

abs_path.rs

287 lines · 7.9 KB · rust
1use std::{
2    borrow::{Borrow, Cow},
3    fmt, io,
4    ops::Deref,
5    path::{Path, PathBuf},
6    rc::Rc,
7    sync::Arc,
8};
9
10use anyhow::Context;
11
12use crate::{PathStyle, rel_path::RelPath};
13
14// An absolute path on the user's local filesystem.
15// Requires paths to be valid utf-8
16#[derive(PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
17#[repr(transparent)]
18pub struct AbsPath(Path);
19
20impl AbsPath {
21    pub fn new(path: &Path) -> anyhow::Result<&Self> {
22        if !path.is_absolute() {
23            return Err(anyhow::anyhow!("Path is not absolute: {:?}", path));
24        }
25        if path.to_str().is_none() {
26            return Err(anyhow::anyhow!("Path is not valid utf-8: {:?}", path));
27        }
28        Ok(Self::new_unchecked(path))
29    }
30
31    fn new_unchecked(path: &Path) -> &Self {
32        // SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
33        unsafe { &*(path as *const Path as *const Self) }
34    }
35
36    pub fn to_abs_path_buf(&self) -> AbsPathBuf {
37        AbsPathBuf(self.0.to_owned())
38    }
39
40    pub fn join(&self, name: impl AsRef<str>) -> AbsPathBuf {
41        AbsPathBuf(self.0.join(name.as_ref()))
42    }
43
44    pub fn join_rel_path(&self, relative_path: &RelPath) -> AbsPathBuf {
45        AbsPathBuf(self.0.join(relative_path.as_std_path()))
46    }
47
48    pub fn parent(&self) -> Option<&AbsPath> {
49        let parent = self.0.parent()?;
50        Some(AbsPath::new_unchecked(parent))
51    }
52
53    pub fn starts_with(&self, other: &AbsPath) -> bool {
54        self.0.starts_with(&other.0)
55    }
56
57    pub fn ends_with(&self, other: &RelPath) -> bool {
58        self.0.ends_with(other.as_std_path())
59    }
60
61    pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
62        if self == ancestor {
63            return false;
64        }
65        self.starts_with(ancestor)
66    }
67
68    pub fn file_name(&self) -> Option<&str> {
69        self.0.file_name()?.to_str()
70    }
71
72    pub fn display(&self) -> impl fmt::Display + '_ {
73        self.0.display()
74    }
75
76    pub fn as_std_path(&self) -> &Path {
77        &self.0
78    }
79
80    pub fn as_str(&self) -> &str {
81        self.0
82            .to_str()
83            .expect("valid UTF-8 enforced in constructor")
84    }
85
86    pub fn ancestors(&self) -> impl Iterator<Item = &AbsPath> {
87        self.0.ancestors().map(|p| AbsPath::new_unchecked(p))
88    }
89
90    pub fn strip_prefix<'a>(&'a self, prefix: &AbsPath) -> Option<Cow<'a, RelPath>> {
91        let prefix = self.0.strip_prefix(&prefix.0).ok()?;
92        RelPath::new(prefix, PathStyle::local()).ok()
93    }
94}
95
96impl ToOwned for AbsPath {
97    type Owned = AbsPathBuf;
98
99    fn to_owned(&self) -> Self::Owned {
100        self.to_abs_path_buf()
101    }
102}
103
104impl AsRef<Path> for AbsPath {
105    fn as_ref(&self) -> &Path {
106        &self.0
107    }
108}
109
110impl AsRef<AbsPath> for AbsPath {
111    fn as_ref(&self) -> &AbsPath {
112        self
113    }
114}
115
116impl From<&AbsPath> for Arc<AbsPath> {
117    fn from(path: &AbsPath) -> Self {
118        let arc: Arc<Path> = Arc::from(&path.0);
119        // SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
120        unsafe { Arc::from_raw(Arc::into_raw(arc) as *const AbsPath) }
121    }
122}
123
124impl From<&AbsPath> for Rc<AbsPath> {
125    fn from(path: &AbsPath) -> Self {
126        let arc: Rc<Path> = Rc::from(&path.0);
127        // SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
128        unsafe { Rc::from_raw(Rc::into_raw(arc) as *const AbsPath) }
129    }
130}
131
132// An absolute path on the user's local filesystem.
133#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
134pub struct AbsPathBuf(PathBuf);
135
136impl AbsPathBuf {
137    pub fn new(path: impl Into<PathBuf>) -> anyhow::Result<Self> {
138        let path = path.into();
139        if let Err(e) = AbsPath::new(&path) {
140            return Err(e);
141        }
142        Ok(Self(path))
143    }
144
145    pub fn home_dir() -> anyhow::Result<Self> {
146        let home = std::env::home_dir().context("no home dir available")?;
147        Self::new(home)
148    }
149
150    /// Resolves `path` to its canonical on-disk spelling: symlinks and `..`
151    /// are resolved, relative input is anchored on the current directory, and
152    /// on case-insensitive filesystems each component takes the casing stored
153    /// on disk. Unlike [`std::fs::canonicalize`], the result never uses
154    /// Windows extended-length (`\\?\`) syntax, which chokes tools the path
155    /// is later handed to (e.g. `git`).
156    ///
157    /// Paths act as identity in several places (lock keys, watch-target
158    /// comparisons, persisted repository records), so canonicalize a path
159    /// where it enters the system whenever it may have been spelled by a user
160    /// or an external tool.
161    pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<Self> {
162        let canonical = dunce::canonicalize(path.as_ref())?;
163        Self::new(canonical).map_err(io::Error::other)
164    }
165
166    pub fn push(&mut self, name: &str) {
167        self.0.push(name);
168    }
169
170    #[cfg(any(test, feature = "test-support"))]
171    pub fn new_test(path: &'static str) -> Self {
172        if cfg!(windows) {
173            Self::new(format!("C:{path}")).unwrap()
174        } else {
175            Self::new(path).unwrap()
176        }
177    }
178}
179
180#[cfg(any(test, feature = "test-support"))]
181pub fn abs_path(path: &str) -> AbsPathBuf {
182    if cfg!(windows) {
183        AbsPathBuf::new(format!("C:{path}")).unwrap()
184    } else {
185        AbsPathBuf::new(path).unwrap()
186    }
187}
188
189impl Deref for AbsPathBuf {
190    type Target = AbsPath;
191
192    fn deref(&self) -> &Self::Target {
193        AbsPath::new_unchecked(&self.0)
194    }
195}
196
197impl Borrow<AbsPath> for AbsPathBuf {
198    fn borrow(&self) -> &AbsPath {
199        self
200    }
201}
202
203impl AsRef<Path> for AbsPathBuf {
204    fn as_ref(&self) -> &Path {
205        self.0.as_ref()
206    }
207}
208
209impl AsRef<AbsPath> for AbsPathBuf {
210    fn as_ref(&self) -> &AbsPath {
211        self
212    }
213}
214
215impl From<AbsPathBuf> for PathBuf {
216    fn from(path: AbsPathBuf) -> PathBuf {
217        path.0
218    }
219}
220
221impl fmt::Display for AbsPathBuf {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        self.as_str().fmt(f)
224    }
225}
226
227impl PartialEq<AbsPath> for AbsPathBuf {
228    fn eq(&self, other: &AbsPath) -> bool {
229        **self == *other
230    }
231}
232
233impl PartialEq<AbsPathBuf> for AbsPath {
234    fn eq(&self, other: &AbsPathBuf) -> bool {
235        *self == **other
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_canonicalize_restores_on_disk_spelling() {
245        let temp = tempfile::tempdir().unwrap();
246        let root = dunce::canonicalize(temp.path()).unwrap();
247        let dir = root.join("CamelCase");
248        std::fs::create_dir(&dir).unwrap();
249
250        let canonical = AbsPathBuf::canonicalize(&dir).unwrap();
251        assert_eq!(canonical.as_std_path(), dir);
252        assert!(
253            !canonical.as_str().starts_with(r"\\?\"),
254            "canonical paths must not use Windows extended-length syntax: {canonical}"
255        );
256
257        // A differently-cased spelling addresses the same directory only on a
258        // case-insensitive filesystem; when it does, canonicalization must
259        // restore the stored casing.
260        let lowercased = root.join("camelcase");
261        if std::fs::metadata(&lowercased).is_ok() {
262            assert_eq!(
263                AbsPathBuf::canonicalize(&lowercased).unwrap().as_std_path(),
264                dir,
265                "canonicalization should restore the on-disk casing"
266            );
267        }
268    }
269
270    #[test]
271    fn test_new_test_normalizes_rooted_paths() {
272        if cfg!(windows) {
273            assert_eq!(AbsPathBuf::new_test("/").as_str(), "C:/");
274            assert_eq!(
275                AbsPathBuf::new_test("/test/project").as_str(),
276                "C:/test/project"
277            );
278        } else {
279            assert_eq!(AbsPathBuf::new_test("/").as_str(), "/");
280            assert_eq!(
281                AbsPathBuf::new_test("/test/project").as_str(),
282                "/test/project"
283            );
284        }
285    }
286}
287
Served at tenant.openagents/omega Member data and write actions are omitted.