Skip to main content

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        let mut past_first = false;
126
127        for arg in std::env::args_os() {
128            if past_first {
129                result += " ";
130            } else {
131                past_first = true;
132            }
133            result += &shell_escape::escape(arg.to_string_lossy());
134        }
135
136        Ok(ReportEntry::Code(Code {
137            language: Some("bash".into()),
138            code: result,
139        }))
140    }
141}
142
143/// The operating system (type and version).
144#[cfg(feature = "collector_operating_system")]
145#[derive(Default)]
146pub struct OperatingSystem {}
147
148#[cfg(feature = "collector_operating_system")]
149impl Collector for OperatingSystem {
150    fn description(&self) -> &str {
151        "Operating system"
152    }
153
154    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
155        Ok(ReportEntry::List(vec![
156            ReportEntry::Text(format!(
157                "OS: {}",
158                sysinfo::System::long_os_version().unwrap_or_else(|| "Unknown".to_owned()),
159            )),
160            ReportEntry::Text(format!(
161                "Kernel: {}",
162                sysinfo::System::kernel_version().unwrap_or_else(|| "Unknown".to_owned()),
163            )),
164        ]))
165    }
166}
167
168/// The values of the specified environment variables (if set).
169pub struct EnvironmentVariables {
170    list: Vec<OsString>,
171}
172
173impl EnvironmentVariables {
174    pub fn list<S: AsRef<OsStr>>(list: &[S]) -> Self {
175        Self {
176            list: list.iter().map(|s| s.as_ref().to_os_string()).collect(),
177        }
178    }
179}
180
181impl Collector for EnvironmentVariables {
182    fn description(&self) -> &str {
183        "Environment variables"
184    }
185
186    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
187        let mut result = String::new();
188
189        for var in &self.list {
190            let value = std::env::var_os(var).map(|value| value.to_string_lossy().into_owned());
191            let value: Option<String> =
192                value.map(|v| shell_escape::escape(Cow::Borrowed(&v)).into());
193
194            let _ = writeln!(
195                result,
196                "{}={}",
197                var.to_string_lossy(),
198                value.unwrap_or_else(|| "<not set>".into())
199            );
200        }
201        result.pop();
202
203        Ok(ReportEntry::Code(Code {
204            language: Some("bash".into()),
205            code: result,
206        }))
207    }
208}
209
210/// The stdout and stderr output (+ exit code) of a custom command.
211pub struct CommandOutput<'a> {
212    title: &'a str,
213    cmd: OsString,
214    cmd_args: Vec<OsString>,
215}
216
217impl<'a> CommandOutput<'a> {
218    pub fn new<S, T>(title: &'a str, cmd: T, args: &[S]) -> Self
219    where
220        T: AsRef<OsStr>,
221        S: AsRef<OsStr>,
222    {
223        let mut cmd_args: Vec<OsString> = Vec::new();
224        for a in args {
225            cmd_args.push(a.into());
226        }
227
228        CommandOutput {
229            title,
230            cmd: cmd.as_ref().to_owned(),
231            cmd_args,
232        }
233    }
234}
235
236impl Collector for CommandOutput<'_> {
237    fn description(&self) -> &str {
238        self.title
239    }
240
241    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
242        let mut result = String::new();
243
244        result += "> ";
245        result += &self.cmd.to_string_lossy();
246        for arg in &self.cmd_args {
247            result += " ";
248            result += &shell_escape::escape(arg.to_string_lossy());
249        }
250
251        result += "\n";
252
253        let output = Command::new(&self.cmd)
254            .args(&self.cmd_args)
255            .output()
256            .map_err(|e| {
257                CollectionError::CouldNotRetrieve(format!(
258                    "Could not run command '{}': {}",
259                    self.cmd.to_string_lossy(),
260                    e
261                ))
262            })?;
263
264        let utf8_decoding_error = |_| {
265            CollectionError::CouldNotRetrieve(format!(
266                "Error while running command '{}': output is not valid UTF-8.",
267                self.cmd.to_string_lossy()
268            ))
269        };
270
271        let stdout = String::from_utf8(output.stdout).map_err(utf8_decoding_error)?;
272        let stderr = String::from_utf8(output.stderr).map_err(utf8_decoding_error)?;
273
274        result += &stdout;
275        result += &stderr;
276
277        result.trim_end_inplace();
278
279        let mut concat = vec![ReportEntry::Code(Code {
280            language: None,
281            code: result,
282        })];
283
284        if !output.status.success() {
285            concat.push(ReportEntry::Text(format!(
286                "Command failed{}.",
287                output
288                    .status
289                    .code()
290                    .map_or("".into(), |c| format!(" with exit code {}", c))
291            )));
292        }
293
294        Ok(ReportEntry::Concat(concat))
295    }
296}
297
298/// The full content of a text file.
299pub struct FileContent<'a> {
300    title: &'a str,
301    path: PathBuf,
302}
303
304impl<'a> FileContent<'a> {
305    pub fn new<P: AsRef<Path>>(title: &'a str, path: P) -> Self {
306        Self {
307            title,
308            path: path.as_ref().to_path_buf(),
309        }
310    }
311}
312
313impl Collector for FileContent<'_> {
314    fn description(&self) -> &str {
315        self.title
316    }
317
318    fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
319        let mut result = fs::read_to_string(&self.path).map_err(|e| {
320            CollectionError::CouldNotRetrieve(format!(
321                "Could not read contents of '{}': {}.",
322                self.path.to_string_lossy(),
323                e
324            ))
325        })?;
326
327        result.trim_end_inplace();
328
329        Ok(ReportEntry::Code(Code {
330            language: None,
331            code: result,
332        }))
333    }
334}