Skip to repository content264 lines · 8.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:29:34.764Z 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
path.rs
1//! Relative path types for deltadb.
2//!
3//! Provides [`RelPath`] and [`RelPathBuf`] — path types that are guaranteed to be
4//! relative, normalized, and valid unicode. Internally stored in POSIX (`/`-delimited)
5//! format regardless of host platform.
6//!
7//! Adapted from Zed's `util::rel_path` module.
8
9use std::{
10 borrow::Cow,
11 path::{Path, PathBuf},
12};
13
14use crate::rel_path::RelPath;
15
16pub mod abs_path;
17pub mod rel_path;
18
19pub trait PathExt {
20 fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf>;
21}
22
23impl<T: AsRef<Path> + ?Sized> PathExt for T {
24 fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf> {
25 Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum PathStyle {
31 Unix,
32 Windows,
33}
34
35impl PathStyle {
36 #[cfg(target_os = "windows")]
37 pub const fn local() -> Self {
38 PathStyle::Windows
39 }
40
41 #[cfg(not(target_os = "windows"))]
42 pub const fn local() -> Self {
43 PathStyle::Unix
44 }
45
46 #[inline]
47 pub fn primary_separator(&self) -> &'static str {
48 match self {
49 PathStyle::Unix => "/",
50 PathStyle::Windows => "\\",
51 }
52 }
53
54 pub fn separators(&self) -> &'static [&'static str] {
55 match self {
56 PathStyle::Unix => &["/"],
57 PathStyle::Windows => &["\\", "/"],
58 }
59 }
60
61 pub fn separators_ch(&self) -> &'static [char] {
62 match self {
63 PathStyle::Unix => &['/'],
64 PathStyle::Windows => &['\\', '/'],
65 }
66 }
67
68 pub fn is_absolute(&self, path_like: &str) -> bool {
69 path_like.starts_with('/')
70 || *self == PathStyle::Windows
71 && (path_like.starts_with('\\')
72 || path_like
73 .chars()
74 .next()
75 .is_some_and(|c| c.is_ascii_alphabetic())
76 && path_like[1..]
77 .strip_prefix(':')
78 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
79 }
80
81 pub fn is_windows(&self) -> bool {
82 *self == PathStyle::Windows
83 }
84
85 pub fn is_posix(&self) -> bool {
86 *self == PathStyle::Unix
87 }
88
89 pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
90 let right = right.as_ref().to_str()?;
91 if is_absolute(right, self) {
92 return None;
93 }
94 let left = left.as_ref().to_str()?;
95 if left.is_empty() {
96 Some(right.into())
97 } else {
98 Some(format!(
99 "{left}{}{right}",
100 if left.ends_with(self.primary_separator()) {
101 ""
102 } else {
103 self.primary_separator()
104 }
105 ))
106 }
107 }
108
109 pub fn join_path(
110 self,
111 left: impl AsRef<Path>,
112 right: impl AsRef<Path>,
113 ) -> anyhow::Result<PathBuf> {
114 let left = left
115 .as_ref()
116 .to_str()
117 .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
118 let right = right.as_ref();
119 let right_string = right
120 .to_str()
121 .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
122 let joined = self
123 .join(left, right_string)
124 .ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
125 Ok(PathBuf::from(self.normalize(&joined)))
126 }
127
128 pub fn normalize(self, path_like: &str) -> String {
129 match self {
130 PathStyle::Windows => crate::normalize_path(Path::new(path_like))
131 .to_string_lossy()
132 .into_owned(),
133 PathStyle::Unix => {
134 let is_absolute = path_like.starts_with('/');
135 let remainder = if is_absolute {
136 path_like.trim_start_matches('/')
137 } else {
138 path_like
139 };
140
141 let mut components = Vec::new();
142 for component in remainder.split(self.separators_ch()) {
143 match component {
144 "" | "." => {}
145 ".." => {
146 if components
147 .last()
148 .is_some_and(|component| *component != "..")
149 {
150 components.pop();
151 } else if !is_absolute {
152 components.push(component);
153 }
154 }
155 component => components.push(component),
156 }
157 }
158
159 let normalized = components.join(self.primary_separator());
160 if is_absolute && normalized.is_empty() {
161 "/".to_string()
162 } else if is_absolute {
163 format!("/{normalized}")
164 } else {
165 normalized
166 }
167 }
168 }
169 }
170
171 pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
172 let Some(pos) = path_like.rfind(self.primary_separator()) else {
173 return (None, path_like);
174 };
175 let filename_start = pos + self.primary_separator().len();
176 (
177 Some(&path_like[..filename_start]),
178 &path_like[filename_start..],
179 )
180 }
181
182 pub fn strip_prefix<'a>(
183 &self,
184 child: &'a Path,
185 parent: &'a Path,
186 ) -> Option<std::borrow::Cow<'a, RelPath>> {
187 let parent = parent.to_str()?;
188 if parent.is_empty() {
189 return RelPath::new(child, *self).ok();
190 }
191 let parent = self
192 .separators()
193 .iter()
194 .find_map(|sep| parent.strip_suffix(sep))
195 .unwrap_or(parent);
196 let child = child.to_str()?;
197
198 // Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:")
199 let stripped = if self.is_windows()
200 && child.as_bytes().get(1) == Some(&b':')
201 && parent.as_bytes().get(1) == Some(&b':')
202 && child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0])
203 {
204 child[2..].strip_prefix(&parent[2..])?
205 } else {
206 child.strip_prefix(parent)?
207 };
208 if let Some(relative) = self
209 .separators()
210 .iter()
211 .find_map(|sep| stripped.strip_prefix(sep))
212 {
213 RelPath::new(relative.as_ref(), *self).ok()
214 } else if stripped.is_empty() {
215 Some(Cow::Borrowed(RelPath::empty()))
216 } else {
217 None
218 }
219 }
220}
221
222fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
223 path_like.starts_with('/')
224 || path_style == PathStyle::Windows
225 && (path_like.starts_with('\\')
226 || path_like
227 .chars()
228 .next()
229 .is_some_and(|c| c.is_ascii_alphabetic())
230 && path_like[1..]
231 .strip_prefix(':')
232 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
233}
234
235/// Normalizes a path by resolving `.` and `..` components without
236/// requiring the path to exist on disk (unlike `canonicalize`).
237pub fn normalize_path(path: &Path) -> PathBuf {
238 use std::path::Component;
239 let mut components = path.components().peekable();
240 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
241 components.next();
242 PathBuf::from(c.as_os_str())
243 } else {
244 PathBuf::new()
245 };
246
247 for component in components {
248 match component {
249 Component::Prefix(..) => unreachable!(),
250 Component::RootDir => {
251 ret.push(component.as_os_str());
252 }
253 Component::CurDir => {}
254 Component::ParentDir => {
255 ret.pop();
256 }
257 Component::Normal(c) => {
258 ret.push(c);
259 }
260 }
261 }
262 ret
263}
264