Skip to content

Commit 9f8a468

Browse files
cruesslerByron
authored andcommitted
feat: Add &gix_path::RelativePath.
It's a utility to assure functions get the right input, i.e. a type-safe version of what previously was `&BStr`
1 parent cd1a777 commit 9f8a468

File tree

4 files changed

+320
-1
lines changed

4 files changed

+320
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-path/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ doctest = false
1616

1717
[dependencies]
1818
gix-trace = { version = "^0.1.12", path = "../gix-trace" }
19+
gix-validate = { version = "^0.9.4", path = "../gix-validate" }
1920
bstr = { version = "1.12.0", default-features = false, features = ["std"] }
2021
thiserror = "2.0.0"
2122
once_cell = "1.21.3"

gix-path/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
//! ever get into a code-path which does panic though.
4848
//! </details>
4949
#![deny(missing_docs, rust_2018_idioms)]
50-
#![cfg_attr(not(test), forbid(unsafe_code))]
50+
#![cfg_attr(not(test), deny(unsafe_code))]
5151

5252
/// A dummy type to represent path specs and help finding all spots that take path specs once it is implemented.
5353
mod convert;
@@ -62,3 +62,6 @@ pub use realpath::function::{realpath, realpath_opts};
6262

6363
/// Information about the environment in terms of locations of resources.
6464
pub mod env;
65+
66+
///
67+
pub mod relative_path;

gix-path/src/relative_path.rs

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
use bstr::BStr;
2+
use bstr::BString;
3+
use bstr::ByteSlice;
4+
use gix_validate::path::component::Options;
5+
use std::borrow::Cow;
6+
7+
use crate::os_str_into_bstr;
8+
use crate::try_from_bstr;
9+
use crate::try_from_byte_slice;
10+
11+
/// A wrapper for `BStr`. It is used to enforce the following constraints:
12+
///
13+
/// - The path separator always is `/`, independent of the platform.
14+
/// - Only normal components are allowed.
15+
/// - It is always represented as a bunch of bytes.
16+
#[derive()]
17+
pub struct RelativePath {
18+
inner: BStr,
19+
}
20+
21+
impl RelativePath {
22+
fn new_unchecked(value: &BStr) -> Result<&RelativePath, Error> {
23+
// SAFETY: `RelativePath` is transparent and equivalent to a `&BStr` if provided as reference.
24+
#[allow(unsafe_code)]
25+
unsafe {
26+
std::mem::transmute(value)
27+
}
28+
}
29+
30+
/// TODO
31+
/// Needs docs.
32+
pub fn ends_with(&self, needle: &[u8]) -> bool {
33+
self.inner.ends_with(needle)
34+
}
35+
}
36+
37+
/// The error used in [`RelativePath`].
38+
#[derive(Debug, thiserror::Error)]
39+
#[allow(missing_docs)]
40+
pub enum Error {
41+
#[error("A RelativePath is not allowed to be absolute")]
42+
IsAbsolute,
43+
#[error(transparent)]
44+
ContainsInvalidComponent(#[from] gix_validate::path::component::Error),
45+
#[error(transparent)]
46+
IllegalUtf8(#[from] crate::Utf8Error),
47+
}
48+
49+
impl<'a> TryFrom<&'a str> for &'a RelativePath {
50+
type Error = Error;
51+
52+
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
53+
use std::path::Path;
54+
55+
let path: &std::path::Path = Path::new(value);
56+
57+
if path.is_absolute() {
58+
return Err(Error::IsAbsolute);
59+
}
60+
61+
let options: Options = Default::default();
62+
63+
for component in path.components() {
64+
let component = os_str_into_bstr(component.as_os_str())?;
65+
66+
gix_validate::path::component(component, None, options)?;
67+
}
68+
69+
RelativePath::new_unchecked(BStr::new(value.as_bytes()))
70+
}
71+
}
72+
73+
impl<'a> TryFrom<&'a BStr> for &'a RelativePath {
74+
type Error = Error;
75+
76+
fn try_from(value: &'a BStr) -> Result<Self, Self::Error> {
77+
let path: &std::path::Path = &try_from_bstr(value)?;
78+
79+
if path.is_absolute() {
80+
return Err(Error::IsAbsolute);
81+
}
82+
83+
let options: Options = Default::default();
84+
85+
for component in path.components() {
86+
let component = os_str_into_bstr(component.as_os_str())?;
87+
88+
gix_validate::path::component(component, None, options)?;
89+
}
90+
91+
RelativePath::new_unchecked(value)
92+
}
93+
}
94+
95+
impl<'a, const N: usize> TryFrom<&'a [u8; N]> for &'a RelativePath {
96+
type Error = Error;
97+
98+
#[inline]
99+
fn try_from(value: &'a [u8; N]) -> Result<Self, Self::Error> {
100+
let path: &std::path::Path = try_from_byte_slice(value)?;
101+
102+
if path.is_absolute() {
103+
return Err(Error::IsAbsolute);
104+
}
105+
106+
let options: Options = Default::default();
107+
108+
for component in path.components() {
109+
let component = os_str_into_bstr(component.as_os_str())?;
110+
111+
gix_validate::path::component(component, None, options)?;
112+
}
113+
114+
RelativePath::new_unchecked(value.into())
115+
}
116+
}
117+
118+
impl<'a> TryFrom<&'a BString> for &'a RelativePath {
119+
type Error = Error;
120+
121+
fn try_from(value: &'a BString) -> Result<Self, Self::Error> {
122+
let path: &std::path::Path = &try_from_bstr(value.as_bstr())?;
123+
124+
if path.is_absolute() {
125+
return Err(Error::IsAbsolute);
126+
}
127+
128+
let options: Options = Default::default();
129+
130+
for component in path.components() {
131+
let component = os_str_into_bstr(component.as_os_str())?;
132+
133+
gix_validate::path::component(component, None, options)?;
134+
}
135+
136+
RelativePath::new_unchecked(value.as_bstr())
137+
}
138+
}
139+
140+
/// This is required by a trait bound on [`from_str`](crate::from_bstr).
141+
impl<'a> From<&'a RelativePath> for Cow<'a, BStr> {
142+
#[inline]
143+
fn from(value: &'a RelativePath) -> Cow<'a, BStr> {
144+
Cow::Borrowed(&value.inner)
145+
}
146+
}
147+
148+
impl AsRef<[u8]> for RelativePath {
149+
#[inline]
150+
fn as_ref(&self) -> &[u8] {
151+
self.inner.as_bytes()
152+
}
153+
}
154+
155+
#[cfg(test)]
156+
mod tests {
157+
use super::*;
158+
159+
#[cfg(not(windows))]
160+
#[test]
161+
fn absolute_paths_return_err() {
162+
let path_str: &str = "/refs/heads";
163+
let path_bstr: &BStr = path_str.into();
164+
let path_u8: &[u8; 11] = b"/refs/heads";
165+
let path_bstring: BString = "/refs/heads".into();
166+
167+
assert!(matches!(
168+
TryInto::<&RelativePath>::try_into(path_str),
169+
Err(Error::IsAbsolute)
170+
));
171+
assert!(matches!(
172+
TryInto::<&RelativePath>::try_into(path_bstr),
173+
Err(Error::IsAbsolute)
174+
));
175+
assert!(matches!(
176+
TryInto::<&RelativePath>::try_into(path_u8),
177+
Err(Error::IsAbsolute)
178+
));
179+
assert!(matches!(
180+
TryInto::<&RelativePath>::try_into(&path_bstring),
181+
Err(Error::IsAbsolute)
182+
));
183+
}
184+
185+
#[cfg(windows)]
186+
#[test]
187+
fn absolute_paths_return_err() {
188+
let path_str: &str = r"c:\refs\heads";
189+
let path_bstr: &BStr = path_str.into();
190+
let path_u8: &[u8; 13] = b"c:\\refs\\heads";
191+
let path_bstring: BString = r"c:\refs\heads".into();
192+
193+
assert!(matches!(
194+
TryInto::<&RelativePath>::try_into(path_str),
195+
Err(Error::IsAbsolute)
196+
));
197+
assert!(matches!(
198+
TryInto::<&RelativePath>::try_into(path_bstr),
199+
Err(Error::IsAbsolute)
200+
));
201+
assert!(matches!(
202+
TryInto::<&RelativePath>::try_into(path_u8),
203+
Err(Error::IsAbsolute)
204+
));
205+
assert!(matches!(
206+
TryInto::<&RelativePath>::try_into(&path_bstring),
207+
Err(Error::IsAbsolute)
208+
));
209+
}
210+
211+
#[cfg(not(windows))]
212+
#[test]
213+
fn dots_in_paths_return_err() {
214+
let path_str: &str = "./heads";
215+
let path_bstr: &BStr = path_str.into();
216+
let path_u8: &[u8; 7] = b"./heads";
217+
let path_bstring: BString = "./heads".into();
218+
219+
assert!(matches!(
220+
TryInto::<&RelativePath>::try_into(path_str),
221+
Err(Error::ContainsInvalidComponent(_))
222+
));
223+
assert!(matches!(
224+
TryInto::<&RelativePath>::try_into(path_bstr),
225+
Err(Error::ContainsInvalidComponent(_))
226+
));
227+
assert!(matches!(
228+
TryInto::<&RelativePath>::try_into(path_u8),
229+
Err(Error::ContainsInvalidComponent(_))
230+
));
231+
assert!(matches!(
232+
TryInto::<&RelativePath>::try_into(&path_bstring),
233+
Err(Error::ContainsInvalidComponent(_))
234+
));
235+
}
236+
237+
#[cfg(windows)]
238+
#[test]
239+
fn dots_in_paths_return_err() {
240+
let path_str: &str = r".\heads";
241+
let path_bstr: &BStr = path_str.into();
242+
let path_u8: &[u8; 7] = b".\\heads";
243+
let path_bstring: BString = r".\heads".into();
244+
245+
assert!(matches!(
246+
TryInto::<&RelativePath>::try_into(path_str),
247+
Err(Error::ContainsInvalidComponent(_))
248+
));
249+
assert!(matches!(
250+
TryInto::<&RelativePath>::try_into(path_bstr),
251+
Err(Error::ContainsInvalidComponent(_))
252+
));
253+
assert!(matches!(
254+
TryInto::<&RelativePath>::try_into(path_u8),
255+
Err(Error::ContainsInvalidComponent(_))
256+
));
257+
assert!(matches!(
258+
TryInto::<&RelativePath>::try_into(&path_bstring),
259+
Err(Error::ContainsInvalidComponent(_))
260+
));
261+
}
262+
263+
#[cfg(not(windows))]
264+
#[test]
265+
fn double_dots_in_paths_return_err() {
266+
let path_str: &str = "../heads";
267+
let path_bstr: &BStr = path_str.into();
268+
let path_u8: &[u8; 8] = b"../heads";
269+
let path_bstring: BString = "../heads".into();
270+
271+
assert!(matches!(
272+
TryInto::<&RelativePath>::try_into(path_str),
273+
Err(Error::ContainsInvalidComponent(_))
274+
));
275+
assert!(matches!(
276+
TryInto::<&RelativePath>::try_into(path_bstr),
277+
Err(Error::ContainsInvalidComponent(_))
278+
));
279+
assert!(matches!(
280+
TryInto::<&RelativePath>::try_into(path_u8),
281+
Err(Error::ContainsInvalidComponent(_))
282+
));
283+
assert!(matches!(
284+
TryInto::<&RelativePath>::try_into(&path_bstring),
285+
Err(Error::ContainsInvalidComponent(_))
286+
));
287+
}
288+
289+
#[cfg(windows)]
290+
#[test]
291+
fn double_dots_in_paths_return_err() {
292+
let path_str: &str = r"..\heads";
293+
let path_bstr: &BStr = path_str.into();
294+
let path_u8: &[u8; 8] = b"..\\heads";
295+
let path_bstring: BString = r"..\heads".into();
296+
297+
assert!(matches!(
298+
TryInto::<&RelativePath>::try_into(path_str),
299+
Err(Error::ContainsInvalidComponent(_))
300+
));
301+
assert!(matches!(
302+
TryInto::<&RelativePath>::try_into(path_bstr),
303+
Err(Error::ContainsInvalidComponent(_))
304+
));
305+
assert!(matches!(
306+
TryInto::<&RelativePath>::try_into(path_u8),
307+
Err(Error::ContainsInvalidComponent(_))
308+
));
309+
assert!(matches!(
310+
TryInto::<&RelativePath>::try_into(&path_bstring),
311+
Err(Error::ContainsInvalidComponent(_))
312+
));
313+
}
314+
}

0 commit comments

Comments
 (0)