Skip to main content

upstream_ontologist/
vcs_command.rs

1use crate::vcs;
2use crate::GuesserSettings;
3use log::warn;
4
5fn parse_command_bytes(command: &[u8]) -> Option<Vec<String>> {
6    if command.ends_with(b"\\") {
7        warn!(
8            "Ignoring command with line break: {}",
9            String::from_utf8_lossy(command)
10        );
11        return None;
12    }
13    let command_str = match String::from_utf8(command.to_vec()) {
14        Ok(s) => s,
15        Err(_) => {
16            warn!(
17                "Ignoring command with non-UTF-8: {}",
18                String::from_utf8_lossy(command)
19            );
20            return None;
21        }
22    };
23    let args: Vec<_> = shlex::split(command_str.as_str())?
24        .into_iter()
25        .filter(|arg| !arg.trim().is_empty())
26        .collect();
27
28    if args.is_empty() {
29        None
30    } else {
31        Some(args)
32    }
33}
34
35/// Extract the upstream repository URL from a command line that looks like
36/// `git clone <url>`, `fossil clone <url>`, `cvs -d <cvsroot> co <module>` or
37/// `svn co <url>`.
38pub fn url_from_vcs_command(command: &[u8]) -> Option<String> {
39    if let Some(url) = url_from_git_clone_command(command) {
40        return Some(url);
41    }
42    if let Some(url) = url_from_fossil_clone_command(command) {
43        return Some(url);
44    }
45    #[cfg(feature = "breezy")]
46    if let Some(url) = url_from_cvs_co_command(command) {
47        return Some(url);
48    }
49    if let Some(url) = url_from_svn_co_command(command) {
50        return Some(url);
51    }
52    None
53}
54
55/// Extract the upstream repository URL from a command line that looks like
56/// `git clone <url>`.
57pub fn url_from_git_clone_command(command: &[u8]) -> Option<String> {
58    let mut args = parse_command_bytes(command)?;
59    if args.remove(0) != "git" || args.remove(0) != "clone" {
60        return None;
61    }
62    let mut i = 0;
63    while i < args.len() {
64        if !args[i].starts_with('-') {
65            i += 1;
66            continue;
67        }
68        if args[i].contains('=') {
69            args.remove(i);
70            continue;
71        }
72        // arguments that take a parameter
73        if args[i] == "-b" || args[i] == "--depth" || args[i] == "--branch" {
74            args.remove(i);
75            args.remove(i);
76            continue;
77        }
78        args.remove(i);
79    }
80    let url = args
81        .get(2)
82        .cloned()
83        .unwrap_or_else(|| args.first().cloned().unwrap_or_default());
84    if vcs::plausible_url(&url) {
85        Some(url)
86    } else {
87        None
88    }
89}
90
91#[test]
92fn test_url_from_git_clone_command() {
93    assert_eq!(
94        url_from_git_clone_command(b"git clone https://github.com/foo/bar foo"),
95        Some("https://github.com/foo/bar".to_string())
96    );
97
98    assert_eq!(
99        Some("https://github.com/jelmer/blah".to_string()),
100        url_from_git_clone_command(b"git clone https://github.com/jelmer/blah"),
101    );
102
103    assert_eq!(
104        Some("https://github.com/jelmer/blah".to_string()),
105        url_from_git_clone_command(b"git clone https://github.com/jelmer/blah target"),
106    );
107
108    assert_eq!(
109        Some("https://github.com/jelmer/blah".to_string()),
110        url_from_git_clone_command(b"git clone -b foo https://github.com/jelmer/blah target"),
111    );
112
113    assert_eq!(None, url_from_git_clone_command(b"git ls-tree"));
114}
115
116/// Get the upstream source from a command line that looks like
117/// `fossil clone <url>`.
118pub fn url_from_fossil_clone_command(command: &[u8]) -> Option<String> {
119    let mut args = parse_command_bytes(command)?;
120    if args.remove(0) != "fossil" || args.remove(0) != "clone" {
121        return None;
122    }
123    let mut i = 0;
124    while i < args.len() {
125        if !args[i].starts_with('-') {
126            i += 1;
127            continue;
128        }
129        if args[i].contains('=') {
130            args.remove(i);
131            continue;
132        }
133        args.remove(i);
134    }
135    let url = args
136        .get(2)
137        .cloned()
138        .unwrap_or_else(|| args.first().cloned().unwrap_or_default());
139    if vcs::plausible_url(&url) {
140        Some(url)
141    } else {
142        None
143    }
144}
145
146#[test]
147fn test_url_from_fossil_clone_command() {
148    assert_eq!(
149        Some("https://example.com/repo/blah".to_string()),
150        url_from_fossil_clone_command(b"fossil clone https://example.com/repo/blah blah.fossil"),
151    );
152}
153
154/// Get the upstream source from a command line that looks like
155/// `cvs -d <cvsroot> co <module>`.
156#[cfg(feature = "breezy")]
157pub fn url_from_cvs_co_command(command: &[u8]) -> Option<String> {
158    let mut args = parse_command_bytes(command)?;
159    let i = 0;
160    let mut cvsroot = None;
161    let mut module = None;
162    let mut command_seen = false;
163    if args.remove(0) != "cvs" {
164        return None;
165    }
166    while i < args.len() {
167        if args[i] == "-d" {
168            args.remove(i);
169            cvsroot = Some(args.remove(i));
170            continue;
171        }
172        if args[i].starts_with("-d") {
173            cvsroot = Some(args.remove(i)[2..].to_string());
174            continue;
175        }
176        if command_seen && !args[i].starts_with('-') {
177            module = Some(args[i].clone());
178        } else if args[i] == "co" || args[i] == "checkout" {
179            command_seen = true;
180        }
181        args.remove(i);
182    }
183    if let Some(cvsroot) = cvsroot {
184        if breezyshim::try_init().is_err() {
185            return None;
186        }
187        let url = breezyshim::location::cvs_to_url(&cvsroot);
188        if let Some(module) = module {
189            return Some(url.join(module.as_str()).unwrap().to_string());
190        }
191        return Some(url.to_string());
192    }
193    None
194}
195
196/// Get the upstream source from a command line that looks like
197/// `svn co <url>`.
198pub fn url_from_svn_co_command(command: &[u8]) -> Option<String> {
199    let args = parse_command_bytes(command)?;
200    if args[0] != "svn" || args[1] != "co" {
201        return None;
202    }
203    let url_schemes = ["svn+ssh", "http", "https", "svn"];
204    args.into_iter().find(|arg| {
205        url_schemes
206            .iter()
207            .any(|scheme| arg.starts_with(&format!("{}://", scheme)))
208    })
209}
210
211/// Guess upstream data from a Makefile or other file that contains a
212/// command to get the source code.
213pub fn guess_from_get_orig_source(
214    path: &std::path::Path,
215    _settings: &GuesserSettings,
216) -> Result<Vec<crate::UpstreamDatumWithMetadata>, crate::ProviderError> {
217    let text = std::fs::read(path)?;
218    let mut result = Vec::new();
219
220    for line in text.split(|b| *b == b'\n') {
221        if let Some(url) = url_from_vcs_command(line) {
222            let certainty = if url.contains('$') {
223                crate::Certainty::Possible
224            } else {
225                crate::Certainty::Likely
226            };
227
228            result.push(crate::UpstreamDatumWithMetadata {
229                datum: crate::UpstreamDatum::Repository(url),
230                certainty: Some(certainty),
231                origin: Some(path.into()),
232            });
233        }
234    }
235
236    Ok(result)
237}