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
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#[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
163pub 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
205pub 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
294pub 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}