Skip to content

Commit fdc06b1

Browse files
authored
Merge pull request #1921 from cruessler/introduce-repository-path
Add `gix_path::relative_path::RelativePath`
2 parents f3684a4 + d139fa4 commit fdc06b1

File tree

19 files changed

+400
-125
lines changed

19 files changed

+400
-125
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-fs/src/stack.rs

+20-7
Original file line numberDiff line numberDiff line change
@@ -50,30 +50,43 @@ fn component_to_os_str<'a>(
5050

5151
impl ToNormalPathComponents for &BStr {
5252
fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
53-
self.split(|b| *b == b'/').filter_map(bytes_component_to_os_str)
53+
self.split(|b| *b == b'/')
54+
.filter_map(|c| bytes_component_to_os_str(c, self))
5455
}
5556
}
5657

5758
impl ToNormalPathComponents for &str {
5859
fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
59-
self.split('/').filter_map(|c| bytes_component_to_os_str(c.as_bytes()))
60+
self.split('/')
61+
.filter_map(|c| bytes_component_to_os_str(c.as_bytes(), (*self).into()))
6062
}
6163
}
6264

6365
impl ToNormalPathComponents for &BString {
6466
fn to_normal_path_components(&self) -> impl Iterator<Item = Result<&OsStr, to_normal_path_components::Error>> {
65-
self.split(|b| *b == b'/').filter_map(bytes_component_to_os_str)
67+
self.split(|b| *b == b'/')
68+
.filter_map(|c| bytes_component_to_os_str(c, self.as_bstr()))
6669
}
6770
}
6871

69-
fn bytes_component_to_os_str(component: &[u8]) -> Option<Result<&OsStr, to_normal_path_components::Error>> {
72+
fn bytes_component_to_os_str<'a>(
73+
component: &'a [u8],
74+
path: &BStr,
75+
) -> Option<Result<&'a OsStr, to_normal_path_components::Error>> {
7076
if component.is_empty() {
7177
return None;
7278
}
73-
gix_path::try_from_byte_slice(component.as_bstr())
79+
let component = match gix_path::try_from_byte_slice(component.as_bstr())
7480
.map_err(|_| to_normal_path_components::Error::IllegalUtf8)
75-
.map(Path::as_os_str)
76-
.into()
81+
{
82+
Ok(c) => c,
83+
Err(err) => return Some(Err(err)),
84+
};
85+
let component = component.components().next()?;
86+
Some(component_to_os_str(
87+
component,
88+
gix_path::try_from_byte_slice(path.as_ref()).ok()?,
89+
))
7790
}
7891

7992
/// Access

gix-fs/tests/fs/stack.rs

+13
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,19 @@ fn absolute_paths_are_invalid() -> crate::Result {
246246
r#"Input path "/" contains relative or absolute components"#,
247247
"a leading slash is always considered absolute"
248248
);
249+
s.make_relative_path_current("/", &mut r)?;
250+
assert_eq!(
251+
s.current(),
252+
s.root(),
253+
"as string this is a no-op as it's just split by /"
254+
);
255+
256+
let err = s.make_relative_path_current("../breakout", &mut r).unwrap_err();
257+
assert_eq!(
258+
err.to_string(),
259+
r#"Input path "../breakout" contains relative or absolute components"#,
260+
"otherwise breakout attempts are detected"
261+
);
249262
s.make_relative_path_current(p("a/"), &mut r)?;
250263
assert_eq!(
251264
s.current(),

gix-negotiate/tests/baseline/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ fn run() -> crate::Result {
7171
// }
7272
for tip in lookup_names(&["HEAD"]).into_iter().chain(
7373
refs.iter()?
74-
.prefixed(b"refs/heads")?
74+
.prefixed(b"refs/heads".try_into().unwrap())?
7575
.filter_map(Result::ok)
7676
.map(|r| r.target.into_id()),
7777
) {

gix-path/Cargo.toml

+1
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

+5-1
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,7 @@ 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;
68+
pub use relative_path::types::RelativePath;

gix-path/src/relative_path.rs

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use bstr::BStr;
2+
use bstr::BString;
3+
use bstr::ByteSlice;
4+
use gix_validate::path::component::Options;
5+
use std::path::Path;
6+
7+
use crate::os_str_into_bstr;
8+
use crate::try_from_bstr;
9+
use crate::try_from_byte_slice;
10+
11+
pub(super) mod types {
12+
use bstr::{BStr, ByteSlice};
13+
/// A wrapper for `BStr`. It is used to enforce the following constraints:
14+
///
15+
/// - The path separator always is `/`, independent of the platform.
16+
/// - Only normal components are allowed.
17+
/// - It is always represented as a bunch of bytes.
18+
#[derive()]
19+
pub struct RelativePath {
20+
inner: BStr,
21+
}
22+
23+
impl AsRef<[u8]> for RelativePath {
24+
#[inline]
25+
fn as_ref(&self) -> &[u8] {
26+
self.inner.as_bytes()
27+
}
28+
}
29+
}
30+
use types::RelativePath;
31+
32+
impl RelativePath {
33+
fn new_unchecked(value: &BStr) -> Result<&RelativePath, Error> {
34+
// SAFETY: `RelativePath` is transparent and equivalent to a `&BStr` if provided as reference.
35+
#[allow(unsafe_code)]
36+
unsafe {
37+
std::mem::transmute(value)
38+
}
39+
}
40+
}
41+
42+
/// The error used in [`RelativePath`].
43+
#[derive(Debug, thiserror::Error)]
44+
#[allow(missing_docs)]
45+
pub enum Error {
46+
#[error("A RelativePath is not allowed to be absolute")]
47+
IsAbsolute,
48+
#[error(transparent)]
49+
ContainsInvalidComponent(#[from] gix_validate::path::component::Error),
50+
#[error(transparent)]
51+
IllegalUtf8(#[from] crate::Utf8Error),
52+
}
53+
54+
fn relative_path_from_value_and_path<'a>(path_bstr: &'a BStr, path: &Path) -> Result<&'a RelativePath, Error> {
55+
if path.is_absolute() {
56+
return Err(Error::IsAbsolute);
57+
}
58+
59+
let options = Options::default();
60+
61+
for component in path.components() {
62+
let component = os_str_into_bstr(component.as_os_str())?;
63+
gix_validate::path::component(component, None, options)?;
64+
}
65+
66+
RelativePath::new_unchecked(BStr::new(path_bstr.as_bytes()))
67+
}
68+
69+
impl<'a> TryFrom<&'a str> for &'a RelativePath {
70+
type Error = Error;
71+
72+
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
73+
relative_path_from_value_and_path(value.into(), Path::new(value))
74+
}
75+
}
76+
77+
impl<'a> TryFrom<&'a BStr> for &'a RelativePath {
78+
type Error = Error;
79+
80+
fn try_from(value: &'a BStr) -> Result<Self, Self::Error> {
81+
let path = try_from_bstr(value)?;
82+
relative_path_from_value_and_path(value, &path)
83+
}
84+
}
85+
86+
impl<'a> TryFrom<&'a [u8]> for &'a RelativePath {
87+
type Error = Error;
88+
89+
#[inline]
90+
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
91+
let path = try_from_byte_slice(value)?;
92+
relative_path_from_value_and_path(value.as_bstr(), path)
93+
}
94+
}
95+
96+
impl<'a, const N: usize> TryFrom<&'a [u8; N]> for &'a RelativePath {
97+
type Error = Error;
98+
99+
#[inline]
100+
fn try_from(value: &'a [u8; N]) -> Result<Self, Self::Error> {
101+
let path = try_from_byte_slice(value.as_bstr())?;
102+
relative_path_from_value_and_path(value.as_bstr(), path)
103+
}
104+
}
105+
106+
impl<'a> TryFrom<&'a BString> for &'a RelativePath {
107+
type Error = Error;
108+
109+
fn try_from(value: &'a BString) -> Result<Self, Self::Error> {
110+
let path = try_from_bstr(value.as_bstr())?;
111+
relative_path_from_value_and_path(value.as_bstr(), &path)
112+
}
113+
}

gix-path/tests/path/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
22

33
mod convert;
44
mod realpath;
5+
mod relative_path;
56
mod home_dir {
67
#[test]
78
fn returns_existing_directory() {

gix-path/tests/path/relative_path.rs

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use bstr::{BStr, BString};
2+
use gix_path::relative_path::Error;
3+
use gix_path::RelativePath;
4+
5+
#[cfg(not(windows))]
6+
#[test]
7+
fn absolute_paths_return_err() {
8+
let path_str: &str = "/refs/heads";
9+
let path_bstr: &BStr = path_str.into();
10+
let path_u8a: &[u8; 11] = b"/refs/heads";
11+
let path_u8: &[u8] = &b"/refs/heads"[..];
12+
let path_bstring: BString = "/refs/heads".into();
13+
14+
assert!(matches!(
15+
TryInto::<&RelativePath>::try_into(path_str),
16+
Err(Error::IsAbsolute)
17+
));
18+
assert!(matches!(
19+
TryInto::<&RelativePath>::try_into(path_bstr),
20+
Err(Error::IsAbsolute)
21+
));
22+
assert!(matches!(
23+
TryInto::<&RelativePath>::try_into(path_u8),
24+
Err(Error::IsAbsolute)
25+
));
26+
assert!(matches!(
27+
TryInto::<&RelativePath>::try_into(path_u8a),
28+
Err(Error::IsAbsolute)
29+
));
30+
assert!(matches!(
31+
TryInto::<&RelativePath>::try_into(&path_bstring),
32+
Err(Error::IsAbsolute)
33+
));
34+
}
35+
36+
#[cfg(windows)]
37+
#[test]
38+
fn absolute_paths_with_backslashes_return_err() {
39+
let path_str: &str = r"c:\refs\heads";
40+
let path_bstr: &BStr = path_str.into();
41+
let path_u8: &[u8] = &b"c:\\refs\\heads"[..];
42+
let path_bstring: BString = r"c:\refs\heads".into();
43+
44+
assert!(matches!(
45+
TryInto::<&RelativePath>::try_into(path_str),
46+
Err(Error::IsAbsolute)
47+
));
48+
assert!(matches!(
49+
TryInto::<&RelativePath>::try_into(path_bstr),
50+
Err(Error::IsAbsolute)
51+
));
52+
assert!(matches!(
53+
TryInto::<&RelativePath>::try_into(path_u8),
54+
Err(Error::IsAbsolute)
55+
));
56+
assert!(matches!(
57+
TryInto::<&RelativePath>::try_into(&path_bstring),
58+
Err(Error::IsAbsolute)
59+
));
60+
}
61+
62+
#[test]
63+
fn dots_in_paths_return_err() {
64+
let path_str: &str = "./heads";
65+
let path_bstr: &BStr = path_str.into();
66+
let path_u8: &[u8] = &b"./heads"[..];
67+
let path_bstring: BString = "./heads".into();
68+
69+
assert!(matches!(
70+
TryInto::<&RelativePath>::try_into(path_str),
71+
Err(Error::ContainsInvalidComponent(_))
72+
));
73+
assert!(matches!(
74+
TryInto::<&RelativePath>::try_into(path_bstr),
75+
Err(Error::ContainsInvalidComponent(_))
76+
));
77+
assert!(matches!(
78+
TryInto::<&RelativePath>::try_into(path_u8),
79+
Err(Error::ContainsInvalidComponent(_))
80+
));
81+
assert!(matches!(
82+
TryInto::<&RelativePath>::try_into(&path_bstring),
83+
Err(Error::ContainsInvalidComponent(_))
84+
));
85+
}
86+
87+
#[test]
88+
fn dots_in_paths_with_backslashes_return_err() {
89+
let path_str: &str = r".\heads";
90+
let path_bstr: &BStr = path_str.into();
91+
let path_u8: &[u8] = &b".\\heads"[..];
92+
let path_bstring: BString = r".\heads".into();
93+
94+
assert!(matches!(
95+
TryInto::<&RelativePath>::try_into(path_str),
96+
Err(Error::ContainsInvalidComponent(_))
97+
));
98+
assert!(matches!(
99+
TryInto::<&RelativePath>::try_into(path_bstr),
100+
Err(Error::ContainsInvalidComponent(_))
101+
));
102+
assert!(matches!(
103+
TryInto::<&RelativePath>::try_into(path_u8),
104+
Err(Error::ContainsInvalidComponent(_))
105+
));
106+
assert!(matches!(
107+
TryInto::<&RelativePath>::try_into(&path_bstring),
108+
Err(Error::ContainsInvalidComponent(_))
109+
));
110+
}
111+
112+
#[test]
113+
fn double_dots_in_paths_return_err() {
114+
let path_str: &str = "../heads";
115+
let path_bstr: &BStr = path_str.into();
116+
let path_u8: &[u8] = &b"../heads"[..];
117+
let path_bstring: BString = "../heads".into();
118+
119+
assert!(matches!(
120+
TryInto::<&RelativePath>::try_into(path_str),
121+
Err(Error::ContainsInvalidComponent(_))
122+
));
123+
assert!(matches!(
124+
TryInto::<&RelativePath>::try_into(path_bstr),
125+
Err(Error::ContainsInvalidComponent(_))
126+
));
127+
assert!(matches!(
128+
TryInto::<&RelativePath>::try_into(path_u8),
129+
Err(Error::ContainsInvalidComponent(_))
130+
));
131+
assert!(matches!(
132+
TryInto::<&RelativePath>::try_into(&path_bstring),
133+
Err(Error::ContainsInvalidComponent(_))
134+
));
135+
}
136+
137+
#[test]
138+
fn double_dots_in_paths_with_backslashes_return_err() {
139+
let path_str: &str = r"..\heads";
140+
let path_bstr: &BStr = path_str.into();
141+
let path_u8: &[u8] = &b"..\\heads"[..];
142+
let path_bstring: BString = r"..\heads".into();
143+
144+
assert!(matches!(
145+
TryInto::<&RelativePath>::try_into(path_str),
146+
Err(Error::ContainsInvalidComponent(_))
147+
));
148+
assert!(matches!(
149+
TryInto::<&RelativePath>::try_into(path_bstr),
150+
Err(Error::ContainsInvalidComponent(_))
151+
));
152+
assert!(matches!(
153+
TryInto::<&RelativePath>::try_into(path_u8),
154+
Err(Error::ContainsInvalidComponent(_))
155+
));
156+
assert!(matches!(
157+
TryInto::<&RelativePath>::try_into(&path_bstring),
158+
Err(Error::ContainsInvalidComponent(_))
159+
));
160+
}

0 commit comments

Comments
 (0)