1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use super::Definition;
use crate::remote::{ParsedUrl, Protocol};
impl Definition {
/// Resolves a relative submodule URL against an explicitly selected superproject remote.
///
/// `./child` appends to the remote repository path; `../child` selects its sibling. This is
/// Git's repository-relative rule, rather than web URL resolution. Only leading `./` and `../`
/// components are processed: later path bytes, percent escapes and credentials are preserved.
/// Absolute/non-relative URLs are returned unchanged without consulting `superproject_url`.
/// No network access, URL rewriting or protocol authorization occurs. Avoid logging returned
/// URLs because they can contain credentials.
///
/// # Errors
///
/// Returns [`UrlError`] for missing/unsafe URL bytes, malformed relative bases, remote-helper
/// bases, query/fragment bases, or traversal beyond the remote path's root. Git can produce
/// unusual results by traversing an authority or scp host; this API explicitly refuses those
/// cases instead of changing the endpoint. Relative local bases retain their relativity.
///
/// ```
/// use girt::submodule::Modules;
/// let modules = Modules::parse(b"[submodule \"math\"]\nurl = ../math.git\n")?;
/// let url = modules
/// .get(b"math")
/// .unwrap()
/// .resolve_url(b"https://example.com/team/parent.git")?;
/// assert_eq!(url, b"https://example.com/team/math.git");
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn resolve_url(&self, superproject_url: &[u8]) -> Result<Vec<u8>, UrlError> {
let url = self
.url()
.filter(|url| safe_url(url))
.ok_or(UrlError::Invalid)?;
if !relative(url) {
return Ok(url.to_vec());
}
if !safe_url(superproject_url) {
return Err(UrlError::Invalid);
}
let parsed = ParsedUrl::parse(superproject_url).map_err(|_| UrlError::Invalid)?;
if parsed.protocol() == Protocol::Helper
|| superproject_url
.iter()
.any(|byte| matches!(byte, b'?' | b'#'))
{
return Err(UrlError::UnsupportedBase);
}
let prefix_len = superproject_url.len() - parsed.path().len();
let prefix = &superproject_url[..prefix_len];
let mut base = parsed.path();
while let Some(rest) = base.strip_prefix(b"./") {
base = rest;
}
let rooted = base.starts_with(b"/");
let mut path = base.to_vec();
while path.ends_with(b"/") {
path.pop();
}
let mut remaining = url;
while relative(remaining) {
if let Some(rest) = remaining.strip_prefix(b"./") {
remaining = rest;
} else if let Some(rest) = remaining.strip_prefix(b"../") {
let component = path.rsplit(|byte| *byte == b'/').next().unwrap_or_default();
if path.is_empty() || component == b".." {
return Err(UrlError::AboveRoot);
}
match path.iter().rposition(|byte| *byte == b'/') {
Some(end) => path.truncate(end),
None => path.clear(),
}
remaining = rest;
}
}
let mut resolved = prefix.to_vec();
if rooted && !path.starts_with(b"/") {
resolved.push(b'/');
}
resolved.extend_from_slice(&path);
if !path.is_empty() || (!prefix.is_empty() && !rooted && !prefix.ends_with(b":")) {
resolved.push(b'/');
}
resolved.extend_from_slice(remaining);
Ok(resolved)
}
}
fn safe_url(url: &[u8]) -> bool {
!url.is_empty() && !url.starts_with(b"-") && !url.iter().any(u8::is_ascii_control)
}
fn relative(url: &[u8]) -> bool {
url.starts_with(b"./") || url.starts_with(b"../")
}
/// A relative submodule URL could not be resolved without guessing or changing its endpoint.
///
/// Error messages omit URL values because they can contain credentials.
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum UrlError {
/// A required URL is missing, empty, malformed, option-like or contains control bytes.
#[error("invalid submodule URL or superproject remote")]
Invalid,
/// Relative resolution is undefined here for helper URLs or query/fragment bases.
#[error("unsupported superproject remote for relative submodule URL")]
UnsupportedBase,
/// Parent traversal would consume a root, authority, host or unresolved parent component.
#[error("relative submodule URL traverses beyond the remote path")]
AboveRoot,
}