cargo_export/
lib.rs

1pub fn target_file_name(file_name: &str, tag_name: Option<&str>) -> String {
2    let (name, _, ext) = split_file_name(file_name);
3    let mut result = String::new();
4    result.push_str(name);
5    if let Some(tag_name) = tag_name {
6        result.push('-');
7        result.push_str(tag_name);
8    }
9    if let Some(ext) = ext {
10        result.push('.');
11        result.push_str(ext);
12    }
13
14    result
15}
16
17/// Returns tuple of 3 strings: file name (without hash and etensions), hash and extension) 2 are optional
18///
19/// Filenames generated by cargo have the following format: `<name>-<hash>.<extension>`. The extension is optional and
20/// is only present for Windows executables. The hash is optional and is only present for tests and benchmarks.
21///
22/// This method strips the hash and extension from the file name and returns them as separate strings.
23/// If the hash or extension is not present, they are returned as `None`. Currently, only the `.exe` extension
24/// is supported.
25pub fn split_file_name(input: &str) -> (&str, Option<&str>, Option<&str>) {
26    const EXTENSIONS: [&str; 4] = [".exe", ".so", ".dylib", ".dll"];
27
28    const RUSTC_HASH_LENGTH: usize = 16;
29
30    let mut file_name = input;
31    let mut extension = None;
32    for ext in EXTENSIONS {
33        if let Some(name) = input.strip_suffix(ext) {
34            file_name = name;
35            extension = Some(&ext[1..]);
36            break;
37        }
38    }
39
40    let idx = match file_name.rfind('-') {
41        Some(idx) if idx > 0 => idx,
42        _ => return (file_name, None, extension),
43    };
44
45    let hash = &file_name[idx + 1..];
46    // it's safe to check number of bytes instead of chars here because we still
47    // check all the characters individually
48    if hash.len() == RUSTC_HASH_LENGTH && hash.chars().all(|c| c.is_ascii_hexdigit()) {
49        (&file_name[..idx], Some(hash), extension)
50    } else {
51        (file_name, None, extension)
52    }
53}
54
55#[cfg(test)]
56mod test {
57    use super::*;
58
59    #[test]
60    fn check_target_file_name() {
61        let cases = vec![
62            // Simple cases without a tag
63            ("app-ebb8dd5b587f73a1", None, "app"),
64            ("app-ebb8dd5b587f73a1", Some("tag"), "app-tag"),
65            // cases for windows with .exe extension without a tag
66            ("app-ebb8dd5b587f73a1.exe", None, "app.exe"),
67            ("app-ebb8dd5b587f73a1.dll", None, "app.dll"),
68            ("app-ebb8dd5b587f73a1.exe", Some("tag"), "app-tag.exe"),
69        ];
70
71        for (input, tag, expected) in cases {
72            assert_eq!(target_file_name(input, tag), expected);
73        }
74    }
75}