1use 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#[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
35pub trait Collector {
37 fn description(&self) -> &str;
38 fn collect(&mut self, crate_info: &CrateInfo) -> Result<ReportEntry>;
39}
40
41#[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#[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#[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#[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
168pub 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
210pub 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
298pub 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}