Skip to repository content287 lines · 7.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:32:24.342Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
abs_path.rs
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