bugreport/
collector.rs

1//! Contains all builtin information collectors and the [`Collector`] trait to implement your own.
2
3use std::borrow::Cow;
4use std::ffi::{OsStr, OsString};
5use std::fmt::Write;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use super::CrateInfo;
11use super::Result;
12
13use crate::helper::StringExt;
14use crate::report::{Code, ReportEntry};
15
16mod directory_entries;
17pub use directory_entries::DirectoryEntries;
18
19/// Error that appeared while collecting bug report information.
20#[derive(Debug)]
21pub enum CollectionError {
22    CouldNotRetrieve(String),
23}
24
25impl CollectionError {
26    pub(crate) fn to_entry(&self) -> ReportEntry {
27        use CollectionError::*;
28
29        match self {
30            CouldNotRetrieve(reason) => ReportEntry::Text(reason.clone()),
31        }
32    }
33}
34
35/// Implement this trait to define customized information collectors.
36pub trait Collector {
37    fn description(&self) -> &str;
38    fn collect(&mut self, crate_info: &CrateInfo) -> Result<ReportEntry>;
39}
40
41/// The name of your crate and the current version.
42#[derive(Default)]
43pub struct SoftwareVersion {
44    version: Option<String>,
45}
46
47impl SoftwareVersion {
48    pub fn custom<S: AsRef<str>>(version: S) -> Self {
49        Self {
50            version: Some(version.as_ref().into()),
51        }
52    }
53}
54
55impl Collector for SoftwareVersion {
56    fn description(&self) -> &str {
57        "Software version"
58    }
59
60    fn collect(&mut self, crate_info: &CrateInfo) -> Result<ReportEntry> {
61        let git_hash_suffix = match crate_info.git_hash {
62            Some(git_hash) => format!(" ({})", git_hash),
63            None => String::new(),
64        };
65
66        Ok(ReportEntry::Text(format!(
67            "{} {}{}",
68            crate_info.pkg_name,
69            self.version.as_deref().unwrap_or(crate_info.pkg_version),
70            git_hash_suffix,
71        )))
72    }
73}
74
75/// Compile-time information such as the profile (release/debug) and the target triple.
76#[derive(Default)]
77pub struct CompileTimeInformation {}
78
79impl Collector for CompileTimeInformation {
80    fn description(&self) -> &str {
81        "Compile time information"
82    }
83
84    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
85        Ok(ReportEntry::List(vec![
86            ReportEntry::Text(format!("Profile: {}", env!("BUGREPORT_PROFILE"))),
87            ReportEntry::Text(format!("Target triple: {}", env!("BUGREPORT_TARGET"))),
88            ReportEntry::Text(format!(
89                "Family: {}",
90                env!("BUGREPORT_CARGO_CFG_TARGET_FAMILY")
91            )),
92            ReportEntry::Text(format!("OS: {}", env!("BUGREPORT_CARGO_CFG_TARGET_OS"))),
93            ReportEntry::Text(format!(
94                "Architecture: {}",
95                env!("BUGREPORT_CARGO_CFG_TARGET_ARCH")
96            )),
97            ReportEntry::Text(format!(
98                "Pointer width: {}",
99                env!("BUGREPORT_CARGO_CFG_TARGET_POINTER_WIDTH")
100            )),
101            ReportEntry::Text(format!(
102                "Endian: {}",
103                env!("BUGREPORT_CARGO_CFG_TARGET_ENDIAN")
104            )),
105            ReportEntry::Text(format!(
106                "CPU features: {}",
107                env!("BUGREPORT_CARGO_CFG_TARGET_FEATURE")
108            )),
109            ReportEntry::Text(format!("Host: {}", env!("BUGREPORT_HOST"))),
110        ]))
111    }
112}
113
114/// The full command-line: executable name and arguments to the program.
115#[derive(Default)]
116pub struct CommandLine {}
117
118impl Collector for CommandLine {
119    fn description(&self) -> &str {
120        "Command-line"
121    }
122
123    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
124        let mut result = String::new();
125
126        for arg in std::env::args_os() {
127            result += &shell_escape::escape(arg.to_string_lossy());
128            result += " ";
129        }
130
131        Ok(ReportEntry::Code(Code {
132            language: Some("bash".into()),
133            code: result,
134        }))
135    }
136}
137
138/// The operating system (type and version).
139#[cfg(feature = "collector_operating_system")]
140#[derive(Default)]
141pub struct OperatingSystem {}
142
143#[cfg(feature = "collector_operating_system")]
144impl Collector for OperatingSystem {
145    fn description(&self) -> &str {
146        "Operating system"
147    }
148
149    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
150        Ok(ReportEntry::List(vec![
151            ReportEntry::Text(format!(
152                "OS: {}",
153                sysinfo::System::long_os_version().unwrap_or_else(|| "Unknown".to_owned()),
154            )),
155            ReportEntry::Text(format!(
156                "Kernel: {}",
157                sysinfo::System::kernel_version().unwrap_or_else(|| "Unknown".to_owned()),
158            )),
159        ]))
160    }
161}
162
163/// The values of the specified environment variables (if set).
164pub struct EnvironmentVariables {
165    list: Vec<OsString>,
166}
167
168impl EnvironmentVariables {
169    pub fn list<S: AsRef<OsStr>>(list: &[S]) -> Self {
170        Self {
171            list: list.iter().map(|s| s.as_ref().to_os_string()).collect(),
172        }
173    }
174}
175
176impl Collector for EnvironmentVariables {
177    fn description(&self) -> &str {
178        "Environment variables"
179    }
180
181    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
182        let mut result = String::new();
183
184        for var in &self.list {
185            let value = std::env::var_os(var).map(|value| value.to_string_lossy().into_owned());
186            let value: Option<String> =
187                value.map(|v| shell_escape::escape(Cow::Borrowed(&v)).into());
188
189            let _ = writeln!(
190                result,
191                "{}={}",
192                var.to_string_lossy(),
193                value.unwrap_or_else(|| "<not set>".into())
194            );
195        }
196        result.pop();
197
198        Ok(ReportEntry::Code(Code {
199            language: Some("bash".into()),
200            code: result,
201        }))
202    }
203}
204
205/// The stdout and stderr output (+ exit code) of a custom command.
206pub struct CommandOutput<'a> {
207    title: &'a str,
208    cmd: OsString,
209    cmd_args: Vec<OsString>,
210}
211
212impl<'a> CommandOutput<'a> {
213    pub fn new<S, T>(title: &'a str, cmd: T, args: &[S]) -> Self
214    where
215        T: AsRef<OsStr>,
216        S: AsRef<OsStr>,
217    {
218        let mut cmd_args: Vec<OsString> = Vec::new();
219        for a in args {
220            cmd_args.push(a.into());
221        }
222
223        CommandOutput {
224            title,
225            cmd: cmd.as_ref().to_owned(),
226            cmd_args,
227        }
228    }
229}
230
231impl<'a> Collector for CommandOutput<'a> {
232    fn description(&self) -> &str {
233        self.title
234    }
235
236    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
237        let mut result = String::new();
238
239        result += "> ";
240        result += &self.cmd.to_string_lossy();
241        result += " ";
242        for arg in &self.cmd_args {
243            result += &shell_escape::escape(arg.to_string_lossy());
244            result += " ";
245        }
246
247        result += "\n";
248
249        let output = Command::new(&self.cmd)
250            .args(&self.cmd_args)
251            .output()
252            .map_err(|e| {
253                CollectionError::CouldNotRetrieve(format!(
254                    "Could not run command '{}': {}",
255                    self.cmd.to_string_lossy(),
256                    e
257                ))
258            })?;
259
260        let utf8_decoding_error = |_| {
261            CollectionError::CouldNotRetrieve(format!(
262                "Error while running command '{}': output is not valid UTF-8.",
263                self.cmd.to_string_lossy()
264            ))
265        };
266
267        let stdout = String::from_utf8(output.stdout).map_err(utf8_decoding_error)?;
268        let stderr = String::from_utf8(output.stderr).map_err(utf8_decoding_error)?;
269
270        result += &stdout;
271        result += &stderr;
272
273        result.trim_end_inplace();
274
275        let mut concat = vec![ReportEntry::Code(Code {
276            language: None,
277            code: result,
278        })];
279
280        if !output.status.success() {
281            concat.push(ReportEntry::Text(format!(
282                "Command failed{}.",
283                output
284                    .status
285                    .code()
286                    .map_or("".into(), |c| format!(" with exit code {}", c))
287            )));
288        }
289
290        Ok(ReportEntry::Concat(concat))
291    }
292}
293
294/// The full content of a text file.
295pub struct FileContent<'a> {
296    title: &'a str,
297    path: PathBuf,
298}
299
300impl<'a> FileContent<'a> {
301    pub fn new<P: AsRef<Path>>(title: &'a str, path: P) -> Self {
302        Self {
303            title,
304            path: path.as_ref().to_path_buf(),
305        }
306    }
307}
308
309impl<'a> Collector for FileContent<'a> {
310    fn description(&self) -> &str {
311        self.title
312    }
313
314    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
315        let mut result = fs::read_to_string(&self.path).map_err(|e| {
316            CollectionError::CouldNotRetrieve(format!(
317                "Could not read contents of '{}': {}.",
318                self.path.to_string_lossy(),
319                e
320            ))
321        })?;
322
323        result.trim_end_inplace();
324
325        Ok(ReportEntry::Code(Code {
326            language: None,
327            code: result,
328        }))
329    }
330}