summaryrefslogtreecommitdiffhomepage
path: root/mullvad-problem-report/src/metadata.rs
blob: d176d66c45bb18df744e1caf4b3af169260066ec (plain)
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use std::collections::HashMap;
use std::process::Command;
use uuid;

pub const PRODUCT_VERSION: &str = concat!(
    include_str!(concat!(env!("OUT_DIR"), "/product-version.txt")),
    " ",
    include_str!(concat!(env!("OUT_DIR"), "/git-commit-date.txt"))
);

pub fn collect() -> HashMap<String, String> {
    let mut metadata = HashMap::new();
    metadata.insert("id".to_owned(), uuid::Uuid::new_v4().to_string());
    metadata.insert(
        "mullvad-product-version".to_owned(),
        PRODUCT_VERSION.to_owned(),
    );
    metadata.insert("os".to_owned(), os::version());
    metadata
}

#[cfg(target_os = "linux")]
mod os {
    extern crate rs_release;

    pub fn version() -> String {
        // The OS version information is obtained first from the os-release file. If that
        // information is incomplete or unavailable, an attempt is made to obtain the
        // version information from the lsb_release command. If that fails, any partial
        // information from os-release is used if available, or a fallback message if
        // reading from the os-release file produced
        // no version information.
        let version = read_os_release_file().unwrap_or_else(|incomplete_info| {
            parse_lsb_release().unwrap_or_else(|| {
                incomplete_info.unwrap_or_else(|| String::from("[Failed to detect version]"))
            })
        });

        format!("Linux {}", version)
    }

    fn read_os_release_file() -> Result<String, Option<String>> {
        let mut os_release_info = rs_release::get_os_release().map_err(|_| None)?;
        let os_name = os_release_info.remove("NAME");
        let os_version = os_release_info.remove("VERSION");

        if os_name.is_some() || os_version.is_some() {
            let full_info_available = os_name.is_some() && os_version.is_some();

            let gathered_info = format!(
                "{} {}",
                os_name.unwrap_or_else(|| "[unknown distribution]".to_owned()),
                os_version.unwrap_or_else(|| "[unknown version]".to_owned())
            );

            if full_info_available {
                Ok(gathered_info)
            } else {
                // Partial version information
                Err(Some(gathered_info))
            }
        } else {
            // No information was obtained
            Err(None)
        }
    }

    fn parse_lsb_release() -> Option<String> {
        super::command_stdout_lossy("lsb_release", &["-ds"]).and_then(|output| {
            if output.is_empty() {
                None
            } else {
                Some(output)
            }
        })
    }
}

#[cfg(target_os = "macos")]
mod os {
    pub fn version() -> String {
        format!(
            "macOS {}",
            super::command_stdout_lossy("sw_vers", &["-productVersion"])
                .unwrap_or(String::from("[Failed to detect version]"))
        )
    }
}

#[cfg(windows)]
mod os {
    pub fn version() -> String {
        let system_info =
            super::command_stdout_lossy("systeminfo", &["/FO", "LIST"]).unwrap_or_else(String::new);

        let mut version = None;
        let mut full_version = None;

        for info_line in system_info.lines() {
            let mut info_parts = info_line.split(":");

            match info_parts.next() {
                Some("OS Name") => {
                    version = info_parts
                        .next()
                        .map(|s| s.trim().trim_left_matches("Microsoft Windows "))
                }
                Some("OS Version") => full_version = info_parts.next().map(str::trim),
                _ => {}
            }
        }

        let version = version.unwrap_or("N/A");
        let full_version = full_version.unwrap_or("N/A");
        format!("Windows {} ({})", version, full_version)
    }
}

/// Helper for getting stdout of some command as a String. Ignores the exit code of the command.
fn command_stdout_lossy(cmd: &str, args: &[&str]) -> Option<String> {
    Command::new(cmd)
        .args(args)
        .output()
        .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
        .ok()
}