grit_lib/
transport_path.rs1use thiserror::Error;
4
5#[derive(Clone, Debug, Error, PartialEq, Eq)]
7pub enum TransportPathError {
8 #[error("fatal: strange pathname '{0}' blocked")]
10 OptionLikePath(String),
11}
12
13#[must_use]
16pub fn looks_like_command_line_option(s: &str) -> bool {
17 !s.is_empty() && s.starts_with('-')
18}
19
20pub fn check_local_url_path_not_option_like(url: &str) -> Result<(), TransportPathError> {
25 let path = url
26 .strip_prefix("file://")
27 .unwrap_or(url)
28 .split('?')
29 .next()
30 .unwrap_or("");
31 if looks_like_command_line_option(path) {
32 return Err(TransportPathError::OptionLikePath(path.to_owned()));
33 }
34 Ok(())
35}
36
37#[derive(Clone, Debug, Error, PartialEq, Eq)]
39#[error("No directory name could be guessed.\nPlease specify a directory on the command line")]
40pub struct NoDirectoryName;
41
42#[inline]
44fn is_dir_sep(b: u8) -> bool {
45 b == b'/'
46}
47
48pub fn git_url_basename(
65 repo: &str,
66 is_bundle: bool,
67 is_bare: bool,
68) -> Result<String, NoDirectoryName> {
69 let bytes = repo.as_bytes();
70 let mut end = bytes.len();
71
72 let mut start = match repo.find("://") {
74 Some(idx) => idx + 3,
75 None => 0,
76 };
77
78 let mut ptr = start;
80 while ptr < end && !is_dir_sep(bytes[ptr]) {
81 if bytes[ptr] == b'@' {
82 start = ptr + 1;
83 }
84 ptr += 1;
85 }
86
87 while start < end && (is_dir_sep(bytes[end - 1]) || bytes[end - 1].is_ascii_whitespace()) {
89 end -= 1;
90 }
91 if end > start + 5 && is_dir_sep(bytes[end - 5]) && &bytes[end - 4..end] == b".git" {
92 end -= 5;
93 while start < end && is_dir_sep(bytes[end - 1]) {
94 end -= 1;
95 }
96 }
97
98 if end < start {
99 return Err(NoDirectoryName);
100 }
101
102 let slice = &bytes[start..end];
106 if !slice.contains(&b'/') && slice.contains(&b':') {
107 let mut p = end;
108 while start < p && bytes[p - 1].is_ascii_digit() && bytes[p - 1] != b':' {
109 p -= 1;
110 }
111 if start < p && bytes[p - 1] == b':' {
112 end = p - 1;
113 }
114 }
115
116 let mut p = end;
119 while start < p && !is_dir_sep(bytes[p - 1]) && bytes[p - 1] != b':' {
120 p -= 1;
121 }
122 start = p;
123
124 let suffix: &[u8] = if is_bundle { b".bundle" } else { b".git" };
126 let mut len = end - start;
127 if len >= suffix.len() && &bytes[start + len - suffix.len()..start + len] == suffix {
128 len -= suffix.len();
129 }
130
131 if len == 0 || (len == 1 && bytes[start] == b'/') {
132 return Err(NoDirectoryName);
133 }
134
135 let core = &repo[start..start + len];
136 let mut dir = if is_bare {
137 format!("{core}.git")
138 } else {
139 core.to_string()
140 };
141
142 dir = collapse_control_and_whitespace(&dir);
143 Ok(dir)
144}
145
146fn collapse_control_and_whitespace(dir: &str) -> String {
150 let mut out = String::with_capacity(dir.len());
151 let mut prev_space = true; for &b in dir.as_bytes() {
153 let ch = if b < 0x20 { b' ' } else { b };
154 if ch.is_ascii_whitespace() {
155 if prev_space {
156 continue;
157 }
158 prev_space = true;
159 } else {
160 prev_space = false;
161 }
162 out.push(ch as char);
163 }
164 if prev_space {
165 while out.ends_with(' ') {
166 out.pop();
167 }
168 }
169 out
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 fn basename(url: &str) -> String {
177 git_url_basename(url, false, false).expect("dir name")
178 }
179
180 #[test]
181 fn scp_style_basic() {
182 assert_eq!(basename("host:foo"), "foo");
183 assert_eq!(basename("host:foo.git"), "foo");
184 assert_eq!(basename("host:foo/.git"), "foo");
185 }
186
187 #[test]
188 fn ssh_url_basic() {
189 assert_eq!(basename("ssh://host/foo"), "foo");
190 assert_eq!(basename("ssh://host/foo.git"), "foo");
191 assert_eq!(basename("ssh://host/foo/.git"), "foo");
192 }
193
194 #[test]
195 fn trailing_slashes_and_git() {
196 assert_eq!(basename("ssh://host/foo/"), "foo");
197 assert_eq!(basename("ssh://host/foo///"), "foo");
198 assert_eq!(basename("ssh://host/foo/.git/"), "foo");
199 assert_eq!(basename("ssh://host/foo.git/"), "foo");
200 assert_eq!(basename("ssh://host/foo.git///"), "foo");
201 assert_eq!(basename("ssh://host/foo///.git/"), "foo");
202 assert_eq!(basename("ssh://host/foo/.git///"), "foo");
203
204 assert_eq!(basename("host:foo/"), "foo");
205 assert_eq!(basename("host:foo///"), "foo");
206 assert_eq!(basename("host:foo.git/"), "foo");
207 assert_eq!(basename("host:foo/.git/"), "foo");
208 assert_eq!(basename("host:foo.git///"), "foo");
209 assert_eq!(basename("host:foo///.git/"), "foo");
210 assert_eq!(basename("host:foo/.git///"), "foo");
211 }
212
213 #[test]
214 fn empty_path_defaults_to_hostname() {
215 assert_eq!(basename("ssh://host/"), "host");
216 assert_eq!(basename("ssh://host:1234/"), "host");
217 assert_eq!(basename("ssh://user@host/"), "host");
218 assert_eq!(basename("host:/"), "host");
219 }
220
221 #[test]
222 fn auth_material_is_redacted() {
223 assert_eq!(basename("ssh://user:password@host/"), "host");
224 assert_eq!(basename("ssh://user:password@host:1234/"), "host");
225 assert_eq!(basename("ssh://user:passw@rd@host:1234/"), "host");
226 assert_eq!(basename("user@host:/"), "host");
227 assert_eq!(basename("user:password@host:/"), "host");
228 assert_eq!(basename("user:passw@rd@host:/"), "host");
229 }
230
231 #[test]
232 fn auth_like_material_kept_in_path() {
233 assert_eq!(basename("ssh://host/foo@bar"), "foo@bar");
234 assert_eq!(basename("ssh://host/foo@bar.git"), "foo@bar");
235 assert_eq!(basename("ssh://user:password@host/foo@bar"), "foo@bar");
236 assert_eq!(basename("ssh://user:passw@rd@host/foo@bar.git"), "foo@bar");
237 assert_eq!(basename("host:/foo@bar"), "foo@bar");
238 assert_eq!(basename("host:/foo@bar.git"), "foo@bar");
239 assert_eq!(basename("user:password@host:/foo@bar"), "foo@bar");
240 assert_eq!(basename("user:passw@rd@host:/foo@bar.git"), "foo@bar");
241 }
242
243 #[test]
244 fn trailing_port_like_numbers_in_path_kept() {
245 assert_eq!(basename("ssh://user:password@host/test:1234"), "1234");
246 assert_eq!(basename("ssh://user:password@host/test:1234.git"), "1234");
247 }
248
249 #[test]
250 fn bare_appends_git() {
251 assert_eq!(
252 git_url_basename("host:foo", false, true).unwrap(),
253 "foo.git"
254 );
255 assert_eq!(
256 git_url_basename("host:foo.git", false, true).unwrap(),
257 "foo.git"
258 );
259 }
260
261 #[test]
262 fn empty_name_is_error() {
263 assert_eq!(git_url_basename("/", false, false), Err(NoDirectoryName));
264 assert_eq!(git_url_basename("", false, false), Err(NoDirectoryName));
265 }
266}