Skip to content

Commit 896cb4d

Browse files
authored
Merge pull request #2261 from GitoxideLabs/copilot/replace-url-dependency
Replace url crate with minimal custom parser in gix-url
2 parents dc2794a + a9a4d4d commit 896cb4d

File tree

9 files changed

+313
-41
lines changed

9 files changed

+313
-41
lines changed

Cargo.lock

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

gix-credentials/src/protocol/context/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ mod mutate {
101101
self.username = url.user().map(ToOwned::to_owned);
102102
self.password = url.password().map(ToOwned::to_owned);
103103
self.host = url.host().map(ToOwned::to_owned).map(|mut host| {
104-
if let Some(port) = url.port {
104+
let port = url.port.filter(|port| {
105+
url.scheme
106+
.default_port()
107+
.is_none_or(|default_port| *port != default_port)
108+
});
109+
if let Some(port) = port {
105110
use std::fmt::Write;
106111
write!(host, ":{port}").expect("infallible");
107112
}

gix-url/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ gix-path = { version = "^0.10.21", path = "../gix-path" }
2424

2525
serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] }
2626
thiserror = "2.0.17"
27-
url = "2.5.2"
2827
bstr = { version = "1.12.0", default-features = false, features = ["std"] }
2928
percent-encoding = "2.3.1"
3029

gix-url/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ mod impls;
2222
///
2323
pub mod parse;
2424

25+
/// Minimal URL parser to replace the `url` crate dependency
26+
mod simple_url;
27+
2528
/// Parse the given `bytes` as a [git url](Url).
2629
///
2730
/// # Note

gix-url/src/parse.rs

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub enum Error {
1919
Url {
2020
url: String,
2121
kind: UrlKind,
22-
source: url::ParseError,
22+
source: crate::simple_url::UrlParseError,
2323
},
2424

2525
#[error("The host portion of the following URL is too long ({} bytes, {len} bytes total): {truncated_url:?}", truncated_url.len())]
@@ -99,30 +99,33 @@ pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result<crate::Url, Error
9999
});
100100
}
101101
let (input, url) = input_to_utf8_and_url(input, UrlKind::Url)?;
102-
let scheme = url.scheme().into();
102+
let scheme = Scheme::from(url.scheme.as_str());
103103

104-
if matches!(scheme, Scheme::Git | Scheme::Ssh) && url.path().is_empty() {
104+
if matches!(scheme, Scheme::Git | Scheme::Ssh) && url.path.is_empty() {
105105
return Err(Error::MissingRepositoryPath {
106106
url: input.into(),
107107
kind: UrlKind::Url,
108108
});
109109
}
110110

111-
if url.cannot_be_a_base() {
112-
return Err(Error::RelativeUrl { url: input.to_owned() });
113-
}
111+
// Normalize empty path to "/" for http/https URLs only
112+
let path = if url.path.is_empty() && matches!(scheme, Scheme::Http | Scheme::Https) {
113+
"/".into()
114+
} else {
115+
url.path.into()
116+
};
114117

115118
Ok(crate::Url {
116119
serialize_alternative_form: false,
117120
scheme,
118121
user: url_user(&url, UrlKind::Url)?,
119122
password: url
120-
.password()
123+
.password
121124
.map(|s| percent_decoded_utf8(s, UrlKind::Url))
122125
.transpose()?,
123-
host: url.host_str().map(Into::into),
124-
port: url.port(),
125-
path: url.path().into(),
126+
host: url.host,
127+
port: url.port,
128+
path,
126129
})
127130
}
128131

@@ -156,31 +159,32 @@ pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> {
156159
// should never differ in any other way (ssh URLs should not contain a query or fragment part).
157160
// To avoid the various off-by-one errors caused by the `/` characters, we keep using the path
158161
// determined above and can therefore skip parsing it here as well.
159-
let url = url::Url::parse(&format!("ssh://{host}")).map_err(|source| Error::Url {
162+
let url_string = format!("ssh://{host}");
163+
let url = crate::simple_url::ParsedUrl::parse(&url_string).map_err(|source| Error::Url {
160164
url: input.to_owned(),
161165
kind: UrlKind::Scp,
162166
source,
163167
})?;
164168

165169
Ok(crate::Url {
166170
serialize_alternative_form: true,
167-
scheme: url.scheme().into(),
171+
scheme: Scheme::from(url.scheme.as_str()),
168172
user: url_user(&url, UrlKind::Scp)?,
169173
password: url
170-
.password()
174+
.password
171175
.map(|s| percent_decoded_utf8(s, UrlKind::Scp))
172176
.transpose()?,
173-
host: url.host_str().map(Into::into),
174-
port: url.port(),
177+
host: url.host,
178+
port: url.port,
175179
path: path.into(),
176180
})
177181
}
178182

179-
fn url_user(url: &url::Url, kind: UrlKind) -> Result<Option<String>, Error> {
180-
if url.username().is_empty() && url.password().is_none() {
183+
fn url_user(url: &crate::simple_url::ParsedUrl<'_>, kind: UrlKind) -> Result<Option<String>, Error> {
184+
if url.username.is_empty() && url.password.is_none() {
181185
Ok(None)
182186
} else {
183-
Ok(Some(percent_decoded_utf8(url.username(), kind)?))
187+
Ok(Some(percent_decoded_utf8(url.username, kind)?))
184188
}
185189
}
186190

@@ -269,13 +273,22 @@ fn input_to_utf8(input: &BStr, kind: UrlKind) -> Result<&str, Error> {
269273
})
270274
}
271275

272-
fn input_to_utf8_and_url(input: &BStr, kind: UrlKind) -> Result<(&str, url::Url), Error> {
276+
fn input_to_utf8_and_url(input: &BStr, kind: UrlKind) -> Result<(&str, crate::simple_url::ParsedUrl<'_>), Error> {
273277
let input = input_to_utf8(input, kind)?;
274-
url::Url::parse(input)
278+
crate::simple_url::ParsedUrl::parse(input)
275279
.map(|url| (input, url))
276-
.map_err(|source| Error::Url {
277-
url: input.to_owned(),
278-
kind,
279-
source,
280+
.map_err(|source| {
281+
// If the parser rejected it as RelativeUrlWithoutBase, map to Error::RelativeUrl
282+
// to match the expected error type for malformed URLs like "invalid:://"
283+
match source {
284+
crate::simple_url::UrlParseError::RelativeUrlWithoutBase => {
285+
Error::RelativeUrl { url: input.to_owned() }
286+
}
287+
_ => Error::Url {
288+
url: input.to_owned(),
289+
kind,
290+
source,
291+
},
292+
}
280293
})
281294
}

gix-url/src/scheme.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ impl Scheme {
4848
Ext(name) => name.as_str(),
4949
}
5050
}
51+
52+
/// Return the default port for this scheme, or `None` if it is not known.
53+
pub fn default_port(&self) -> Option<u16> {
54+
match self {
55+
Scheme::Http => Some(80),
56+
Scheme::Https => Some(443),
57+
Scheme::Ssh => Some(22),
58+
Scheme::Git => Some(9418),
59+
_ => None,
60+
}
61+
}
5162
}
5263

5364
impl std::fmt::Display for Scheme {

0 commit comments

Comments
 (0)