1use super::GitError;
2use regex::Regex;
3use std::str::FromStr;
4use std::{fmt, fmt::{Display, Formatter}, result::Result as stdResult};
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Deserializer, de};
7
8pub type Result<A> = stdResult<A, GitError>;
9
10#[derive(Debug)]
11pub struct GitUrl {
12 pub(crate) value: String,
13}
14
15impl FromStr for GitUrl {
16 type Err = GitError;
17
18 fn from_str(value: &str) -> Result<Self> {
19 let re =
21 Regex::new("(?:git|ssh|https?|git@[-\\w.]+):(//)?(.*?)(\\.git)(/?|\\#[-\\d\\w._]+?)$")
22 .unwrap();
23 if re.is_match(value) {
24 Ok(GitUrl {
25 value: String::from(value),
26 })
27 } else {
28 Err(GitError::InvalidUrl)
29 }
30 }
31}
32
33impl Display for GitUrl {
34 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
35 write!(f, "{}", self.value)
36 }
37}
38
39#[derive(Debug)]
40pub struct BranchName {
41 pub(crate) value: String
42}
43
44impl FromStr for BranchName {
45 type Err = GitError;
46 fn from_str(s: &str) -> Result<Self> {
47 if is_valid_reference_name(s) {
48 Ok(BranchName {
49 value: String::from(s)
50 })
51 } else {
52 Err(GitError::InvalidRefName)
53 }
54 }
55
56}
57
58impl Display for BranchName {
59 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
60 write!(f, "{}", self.value)
61 }
62}
63
64#[cfg(feature = "serde")]
65impl<'de> Deserialize<'de> for BranchName {
66 fn deserialize<D>(deserializer: D) -> stdResult<BranchName, D::Error>
67 where
68 D: Deserializer<'de>,
69 {
70 let s = String::deserialize(deserializer)?;
71 BranchName::from_str(&s).map_err(de::Error::custom)
72 }
73}
74
75const INVALID_REFERENCE_CHARS: [char; 5] = [' ', '~', '^', ':', '\\'];
76const INVALID_REFERENCE_START: &str = "-";
77const INVALID_REFERENCE_END: &str = ".";
78
79fn is_valid_reference_name(name: &str) -> bool {
80 !name.starts_with(INVALID_REFERENCE_START)
81 && !name.ends_with(INVALID_REFERENCE_END)
82 && name.chars().all(|c| {
83 !c.is_ascii_control() && INVALID_REFERENCE_CHARS.iter().all(|invalid| &c != invalid)
84 })
85 && !name.contains("/.")
86 && !name.contains("@{")
87 && !name.contains("..")
88 && name != "@"
89}
90
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn test_valid_git_urls() {
98
99 let valid_urls = vec!(
100 "git://github.com/ember-cli/ember-cli.git#ff786f9f",
101 "git://github.com/ember-cli/ember-cli.git#gh-pages",
102 "git://github.com/ember-cli/ember-cli.git#master",
103 "git://github.com/ember-cli/ember-cli.git#Quick-Fix",
104 "git://github.com/ember-cli/ember-cli.git#quick_fix",
105 "git://github.com/ember-cli/ember-cli.git#v0.1.0",
106 "git://host.xz/path/to/repo.git/",
107 "git://host.xz/~user/path/to/repo.git/",
108 "git@192.168.101.127:user/project.git",
109 "git@github.com:user/project.git",
110 "git@github.com:user/some-project.git",
111 "git@github.com:user/some-project.git",
112 "git@github.com:user/some_project.git",
113 "git@github.com:user/some_project.git",
114 "http://192.168.101.127/user/project.git",
115 "http://github.com/user/project.git",
116 "http://host.xz/path/to/repo.git/",
117 "https://192.168.101.127/user/project.git",
118 "https://github.com/user/project.git",
119 "https://host.xz/path/to/repo.git/",
120 "https://username::;*%$:@github.com/username/repository.git",
121 "https://username:$fooABC@:@github.com/username/repository.git",
122 "https://username:password@github.com/username/repository.git",
123 "ssh://host.xz/path/to/repo.git/",
124 "ssh://host.xz/path/to/repo.git/",
125 "ssh://host.xz/~/path/to/repo.git",
126 "ssh://host.xz/~user/path/to/repo.git/",
127 "ssh://host.xz:port/path/to/repo.git/",
128 "ssh://user@host.xz/path/to/repo.git/",
129 "ssh://user@host.xz/path/to/repo.git/",
130 "ssh://user@host.xz/~/path/to/repo.git",
131 "ssh://user@host.xz/~user/path/to/repo.git/",
132 "ssh://user@host.xz:port/path/to/repo.git/",
133 );
134
135 for url in valid_urls.iter() {
136 assert!(GitUrl::from_str(url).is_ok())
137 }
138 }
139
140
141 #[test]
142 fn test_invalid_git_urls() {
143 let invalid_urls = vec!(
144 "/path/to/repo.git/",
145 "file:///path/to/repo.git/",
146 "file://~/path/to/repo.git/",
147 "git@github.com:user/some_project.git/foo",
148 "git@github.com:user/some_project.gitfoo",
149 "host.xz:/path/to/repo.git/",
150 "host.xz:path/to/repo.git",
151 "host.xz:~user/path/to/repo.git/",
152 "path/to/repo.git/",
153 "rsync://host.xz/path/to/repo.git/",
154 "user@host.xz:/path/to/repo.git/",
155 "user@host.xz:path/to/repo.git",
156 "user@host.xz:~user/path/to/repo.git/",
157 "~/path/to/repo.git"
158 );
159
160 for url in invalid_urls.iter() {
161 assert!(GitUrl::from_str(url).is_err())
162 }
163 }
164
165 #[test]
166 fn test_valid_reference_names() {
167 let valid_reference = "avalidreference";
168
169 assert!(is_valid_reference_name(valid_reference))
170 }
171
172 #[test]
173 fn test_invalid_reference_names() {
174 let invalid_references = vec!(
175 "double..dot",
176 "inavlid^character",
177 "invalid~character",
178 "invalid:character",
179 "invalid\\character",
180 "@",
181 "inavlid@{sequence"
182 );
183
184 for reference_name in invalid_references.iter() {
185 assert!(!is_valid_reference_name(reference_name))
186 }
187 }
188}