Skip to content

Commit 3cff2af

Browse files
branchseerCopilot
andauthored
feat: add crate vite_path (#58)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent bfeed6b commit 3cff2af

22 files changed

Lines changed: 650 additions & 14 deletions

Cargo.lock

Lines changed: 61 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ petgraph = "0.8.2"
6161
portable-pty = "0.9.0"
6262
ratatui = "0.29.0"
6363
rayon = "1.10.0"
64+
ref-cast = "1.0.24"
6465
relative-path = "2.0.1"
6566
rusqlite = "0.37.0"
6667
rustc-hash = "2.1.1"
@@ -80,6 +81,8 @@ twox-hash = "2.1.1"
8081
vite_error = { path = "crates/vite_error" }
8182
vite_package_manager = { path = "crates/vite_package_manager" }
8283
vite_task = { path = "crates/vite_task" }
84+
vite_str = { path = "crates/vite_str" }
85+
vite_path = { path = "crates/vite_path" }
8386
wax = "0.6.0"
8487
wildmatch = "2.4.0"
8588

crates/vite_path/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "vite_paths"
3+
version = "0.1.0"
4+
authors.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
rust-version.workspace = true
8+
9+
[dependencies]
10+
bincode = { workspace = true }
11+
ref-cast = { workspace = true }
12+
vite_str = { workspace = true }
13+
thiserror = { workspace = true }
14+
15+
[lints]
16+
workspace = true
17+
18+
[dev-dependencies]
19+
assert2 = "0.3.16"
20+

crates/vite_path/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# vite_path
2+
3+
Provides path typed with its relativity: `AbsolutePath(Buf)` and `RelativePath(Buf)`, and safe methods to convert between them (for example, `AbsolutePath::join(RelativePath)` produces `AbsolutePathBuf`).

crates/vite_path/src/absolute.rs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
use ref_cast::{RefCastCustom, ref_cast_custom};
2+
use std::{
3+
fmt::Display,
4+
ops::Deref,
5+
path::{Path, PathBuf},
6+
};
7+
8+
use crate::relative::{FromPathError, InvalidPathDataError, RelativePath, RelativePathBuf};
9+
10+
/// A path that is guaranteed to be absolute
11+
#[derive(RefCastCustom, Debug, PartialEq, Eq)]
12+
#[repr(transparent)]
13+
pub struct AbsolutePath(Path);
14+
impl AsRef<AbsolutePath> for AbsolutePath {
15+
fn as_ref(&self) -> &AbsolutePath {
16+
&self
17+
}
18+
}
19+
20+
impl AbsolutePath {
21+
/// Creates a [`AbsolutePath`] if the give path is absolute.
22+
pub fn new(path: &Path) -> Option<&Self> {
23+
if path.is_absolute() { Some(unsafe { Self::assume_absolute(path) }) } else { None }
24+
}
25+
26+
#[ref_cast_custom]
27+
pub(crate) unsafe fn assume_absolute(abs_path: &Path) -> &Self;
28+
29+
/// Gets the underlying [`Path`]
30+
pub fn as_path(&self) -> &Path {
31+
&self.0
32+
}
33+
34+
/// Converts `self` to an owned [`AbsolutePathBuf`].
35+
pub fn to_absolute_path_buf(&self) -> AbsolutePathBuf {
36+
unsafe { AbsolutePathBuf::assume_absolute(self.0.to_path_buf()) }
37+
}
38+
39+
/// Returns a path that, when joined onto base, yields self.
40+
///
41+
/// If `base` is not a prefix of `self`, returns [`None`].
42+
///
43+
/// If the stripped path is not a valid [`RelativePath`]. Returns an error with the reason and the stripped path.
44+
pub fn strip_prefix<P: AsRef<AbsolutePath>>(
45+
&self,
46+
base: P,
47+
) -> Result<Option<RelativePathBuf>, StripPrefixError<'_>> {
48+
let base = base.as_ref();
49+
let Ok(stripped_path) = self.0.strip_prefix(&base.0) else {
50+
return Ok(None);
51+
};
52+
match RelativePathBuf::try_from(stripped_path) {
53+
Ok(relative_path) => Ok(Some(relative_path)),
54+
Err(FromPathError::NonRelative) => {
55+
unreachable!("stripped path should always be relative")
56+
}
57+
Err(FromPathError::InvalidPathData(invalid_path_data_error)) => {
58+
Err(StripPrefixError { stripped_path, invalid_path_data_error })
59+
}
60+
}
61+
}
62+
63+
/// Creates an owned [`AbsolutePathBuf`] with `rel_path` adjoined to `self`.
64+
pub fn join<P: AsRef<RelativePath>>(&self, rel_path: P) -> AbsolutePathBuf {
65+
let mut absolute_path_buf = self.to_absolute_path_buf();
66+
absolute_path_buf.push(rel_path);
67+
absolute_path_buf
68+
}
69+
}
70+
71+
/// An Error returned from [`AbsolutePath::strip_prefix`] if the stripped path is not a valid `RelativePath`
72+
#[derive(thiserror::Error, Debug)]
73+
pub struct StripPrefixError<'a> {
74+
pub stripped_path: &'a Path,
75+
#[source]
76+
pub invalid_path_data_error: InvalidPathDataError,
77+
}
78+
79+
impl Display for StripPrefixError<'_> {
80+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81+
f.write_fmt(format_args!(
82+
"{}: {}",
83+
self.stripped_path.display(),
84+
&self.invalid_path_data_error
85+
))
86+
}
87+
}
88+
89+
impl AsRef<Path> for AbsolutePath {
90+
fn as_ref(&self) -> &Path {
91+
self.as_path()
92+
}
93+
}
94+
95+
/// An owned path buf that is guaranteed to be absolute
96+
#[derive(Debug, Clone, PartialEq, Eq)]
97+
pub struct AbsolutePathBuf(PathBuf);
98+
99+
impl AbsolutePathBuf {
100+
pub fn new(path: PathBuf) -> Option<Self> {
101+
if path.is_absolute() { Some(unsafe { Self::assume_absolute(path) }) } else { None }
102+
}
103+
pub unsafe fn assume_absolute(abs_path: PathBuf) -> Self {
104+
Self(abs_path)
105+
}
106+
pub fn as_absolute_path(&self) -> &AbsolutePath {
107+
unsafe { AbsolutePath::assume_absolute(self.0.as_path()) }
108+
}
109+
110+
/// Extends `self` with `path`.
111+
///
112+
/// Unlike [`PathBuf::push`], `path` is always relative, so `self` can only be appended, not replaced.
113+
pub fn push<P: AsRef<RelativePath>>(&mut self, rel_path: P) {
114+
self.0.push(rel_path.as_ref().as_path());
115+
}
116+
}
117+
118+
impl PartialEq<AbsolutePath> for AbsolutePathBuf {
119+
fn eq(&self, other: &AbsolutePath) -> bool {
120+
self.as_absolute_path().eq(other)
121+
}
122+
}
123+
impl PartialEq<&AbsolutePath> for AbsolutePathBuf {
124+
fn eq(&self, other: &&AbsolutePath) -> bool {
125+
self.as_absolute_path().eq(other)
126+
}
127+
}
128+
129+
impl AsRef<Path> for AbsolutePathBuf {
130+
fn as_ref(&self) -> &Path {
131+
self.as_absolute_path().as_path()
132+
}
133+
}
134+
impl AsRef<AbsolutePath> for AbsolutePathBuf {
135+
fn as_ref(&self) -> &AbsolutePath {
136+
self.as_absolute_path()
137+
}
138+
}
139+
140+
impl Deref for AbsolutePathBuf {
141+
type Target = AbsolutePath;
142+
143+
fn deref(&self) -> &Self::Target {
144+
self.as_absolute_path()
145+
}
146+
}
147+
148+
#[cfg(test)]
149+
mod tests {
150+
use std::path::Path;
151+
152+
use super::*;
153+
use assert2::let_assert;
154+
155+
#[test]
156+
fn non_absolute() {
157+
assert!(AbsolutePath::new(Path::new("foo/bar")).is_none())
158+
}
159+
160+
#[test]
161+
fn strip_prefix() {
162+
let abs_path = AbsolutePath::new(Path::new(if cfg!(windows) {
163+
"C:\\Users\\foo\\bar"
164+
} else {
165+
"/home/foo/bar"
166+
}))
167+
.unwrap();
168+
169+
let prefix =
170+
AbsolutePath::new(Path::new(if cfg!(windows) { "C:\\Users" } else { "/home" }))
171+
.unwrap();
172+
173+
let rel_path = abs_path.strip_prefix(prefix).unwrap().unwrap();
174+
assert_eq!(rel_path.as_str(), "foo/bar");
175+
176+
assert_eq!(prefix.join(&rel_path), abs_path);
177+
let mut pushed_path = prefix.to_absolute_path_buf();
178+
pushed_path.push(rel_path);
179+
180+
assert_eq!(pushed_path, abs_path);
181+
}
182+
183+
#[test]
184+
fn strip_prefix_trailing_slash() {
185+
let abs_path = AbsolutePath::new(Path::new(if cfg!(windows) {
186+
"C:\\Users\\foo\\bar"
187+
} else {
188+
"/home/foo/bar"
189+
}))
190+
.unwrap();
191+
192+
let prefix =
193+
AbsolutePath::new(Path::new(if cfg!(windows) { "C:\\Users\\" } else { "/home//" }))
194+
.unwrap();
195+
196+
let rel_path = abs_path.strip_prefix(prefix).unwrap().unwrap();
197+
assert_eq!(rel_path.as_str(), "foo/bar");
198+
}
199+
200+
#[test]
201+
fn strip_prefix_not_found() {
202+
let abs_path = AbsolutePath::new(Path::new(if cfg!(windows) {
203+
"C:\\Users\\foo\\bar"
204+
} else {
205+
"/home/foo/bar"
206+
}))
207+
.unwrap();
208+
209+
let prefix = AbsolutePath::new(Path::new(if cfg!(windows) {
210+
"C:\\Users\\barz"
211+
} else {
212+
"/home/baz"
213+
}))
214+
.unwrap();
215+
216+
let rel_path = abs_path.strip_prefix(prefix).unwrap();
217+
assert!(rel_path.is_none());
218+
}
219+
220+
#[cfg(unix)]
221+
#[test]
222+
fn strip_prefix_invalid_relative() {
223+
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
224+
225+
let mut abs_path = b"/home/".to_vec();
226+
abs_path.push(0xC0);
227+
let abs_path = AbsolutePath::new(Path::new(OsStr::from_bytes(&abs_path))).unwrap();
228+
229+
let prefix = AbsolutePath::new(Path::new("/home")).unwrap();
230+
let_assert!(Err(err) = abs_path.strip_prefix(prefix));
231+
232+
assert_eq!(err.stripped_path.as_os_str().as_bytes(), &[0xC0]);
233+
let_assert!(InvalidPathDataError::NonUtf8 = err.invalid_path_data_error);
234+
}
235+
}

crates/vite_path/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
pub mod absolute;
2+
pub mod relative;
3+
4+
use std::io;
5+
6+
use absolute::{AbsolutePath, AbsolutePathBuf};
7+
8+
pub fn current_dir() -> io::Result<AbsolutePathBuf> {
9+
let cwd = std::env::current_dir()?;
10+
// `std::env::current_dir` should always return a absolute path but its documentation doesn't guarantee that.
11+
// Do a runtime check just in case.
12+
Ok(AbsolutePathBuf::new(cwd).unwrap())
13+
}

0 commit comments

Comments
 (0)