1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
pub fn target_file_name(file_name: &str, tag_name: Option<&str>) -> String {
    let (name, _, ext) = split_file_name(file_name);
    let mut result = String::new();
    result.push_str(name);
    if let Some(tag_name) = tag_name {
        result.push('-');
        result.push_str(tag_name);
    }
    if let Some(ext) = ext {
        result.push('.');
        result.push_str(ext);
    }

    result
}

/// Returns tuple of 3 strings: file name (without hash and etensions), hash and extension) 2 are optional
///
/// Filenames generated by cargo have the following format: `<name>-<hash>.<extension>`. The extension is optional and
/// is only present for Windows executables. The hash is optional and is only present for tests and benchmarks.
///
/// This method strips the hash and extension from the file name and returns them as separate strings.
/// If the hash or extension is not present, they are returned as `None`. Currently, only the `.exe` extension
/// is supported.
pub fn split_file_name(input: &str) -> (&str, Option<&str>, Option<&str>) {
    const EXTENSIONS: [&str; 4] = [".exe", ".so", ".dylib", ".dll"];

    const RUSTC_HASH_LENGTH: usize = 16;

    let mut file_name = input;
    let mut extension = None;
    for ext in EXTENSIONS {
        if let Some(name) = input.strip_suffix(ext) {
            file_name = name;
            extension = Some(&ext[1..]);
            break;
        }
    }

    let idx = match file_name.rfind('-') {
        Some(idx) if idx > 0 => idx,
        _ => return (file_name, None, extension),
    };

    let hash = &file_name[idx + 1..];
    // it's safe to check number of bytes instead of chars here because we still
    // check all the characters individually
    if hash.len() == RUSTC_HASH_LENGTH && hash.chars().all(|c| c.is_ascii_hexdigit()) {
        (&file_name[..idx], Some(hash), extension)
    } else {
        (file_name, None, extension)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn check_target_file_name() {
        let cases = vec![
            // Simple cases without a tag
            ("app-ebb8dd5b587f73a1", None, "app"),
            ("app-ebb8dd5b587f73a1", Some("tag"), "app-tag"),
            // cases for windows with .exe extension without a tag
            ("app-ebb8dd5b587f73a1.exe", None, "app.exe"),
            ("app-ebb8dd5b587f73a1.dll", None, "app.dll"),
            ("app-ebb8dd5b587f73a1.exe", Some("tag"), "app-tag.exe"),
        ];

        for (input, tag, expected) in cases {
            assert_eq!(target_file_name(input, tag), expected);
        }
    }
}