use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::fmt::Write;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use super::CrateInfo;
use super::Result;
use crate::helper::StringExt;
use crate::report::{Code, ReportEntry};
mod directory_entries;
pub use directory_entries::DirectoryEntries;
#[derive(Debug)]
pub enum CollectionError {
CouldNotRetrieve(String),
}
impl CollectionError {
pub(crate) fn to_entry(&self) -> ReportEntry {
use CollectionError::*;
match self {
CouldNotRetrieve(reason) => ReportEntry::Text(reason.clone()),
}
}
}
pub trait Collector {
fn description(&self) -> &str;
fn collect(&mut self, crate_info: &CrateInfo) -> Result<ReportEntry>;
}
#[derive(Default)]
pub struct SoftwareVersion {
version: Option<String>,
}
impl SoftwareVersion {
pub fn custom<S: AsRef<str>>(version: S) -> Self {
Self {
version: Some(version.as_ref().into()),
}
}
}
impl Collector for SoftwareVersion {
fn description(&self) -> &str {
"Software version"
}
fn collect(&mut self, crate_info: &CrateInfo) -> Result<ReportEntry> {
let git_hash_suffix = match crate_info.git_hash {
Some(git_hash) => format!(" ({})", git_hash),
None => String::new(),
};
Ok(ReportEntry::Text(format!(
"{} {}{}",
crate_info.pkg_name,
self.version.as_deref().unwrap_or(crate_info.pkg_version),
git_hash_suffix,
)))
}
}
#[derive(Default)]
pub struct CompileTimeInformation {}
impl Collector for CompileTimeInformation {
fn description(&self) -> &str {
"Compile time information"
}
fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
Ok(ReportEntry::List(vec![
ReportEntry::Text(format!("Profile: {}", env!("BUGREPORT_PROFILE"))),
ReportEntry::Text(format!("Target triple: {}", env!("BUGREPORT_TARGET"))),
ReportEntry::Text(format!(
"Family: {}",
env!("BUGREPORT_CARGO_CFG_TARGET_FAMILY")
)),
ReportEntry::Text(format!("OS: {}", env!("BUGREPORT_CARGO_CFG_TARGET_OS"))),
ReportEntry::Text(format!(
"Architecture: {}",
env!("BUGREPORT_CARGO_CFG_TARGET_ARCH")
)),
ReportEntry::Text(format!(
"Pointer width: {}",
env!("BUGREPORT_CARGO_CFG_TARGET_POINTER_WIDTH")
)),
ReportEntry::Text(format!(
"Endian: {}",
env!("BUGREPORT_CARGO_CFG_TARGET_ENDIAN")
)),
ReportEntry::Text(format!(
"CPU features: {}",
env!("BUGREPORT_CARGO_CFG_TARGET_FEATURE")
)),
ReportEntry::Text(format!("Host: {}", env!("BUGREPORT_HOST"))),
]))
}
}
#[derive(Default)]
pub struct CommandLine {}
impl Collector for CommandLine {
fn description(&self) -> &str {
"Command-line"
}
fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
let mut result = String::new();
for arg in std::env::args_os() {
result += &shell_escape::escape(arg.to_string_lossy());
result += " ";
}
Ok(ReportEntry::Code(Code {
language: Some("bash".into()),
code: result,
}))
}
}
#[cfg(feature = "collector_operating_system")]
#[derive(Default)]
pub struct OperatingSystem {}
#[cfg(feature = "collector_operating_system")]
impl Collector for OperatingSystem {
fn description(&self) -> &str {
"Operating system"
}
fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
Ok(ReportEntry::List(vec![
ReportEntry::Text(format!(
"OS: {}",
sysinfo::System::long_os_version().unwrap_or_else(|| "Unknown".to_owned()),
)),
ReportEntry::Text(format!(
"Kernel: {}",
sysinfo::System::kernel_version().unwrap_or_else(|| "Unknown".to_owned()),
)),
]))
}
}
pub struct EnvironmentVariables {
list: Vec<OsString>,
}
impl EnvironmentVariables {
pub fn list<S: AsRef<OsStr>>(list: &[S]) -> Self {
Self {
list: list.iter().map(|s| s.as_ref().to_os_string()).collect(),
}
}
}
impl Collector for EnvironmentVariables {
fn description(&self) -> &str {
"Environment variables"
}
fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
let mut result = String::new();
for var in &self.list {
let value = std::env::var_os(var).map(|value| value.to_string_lossy().into_owned());
let value: Option<String> =
value.map(|v| shell_escape::escape(Cow::Borrowed(&v)).into());
let _ = writeln!(
result,
"{}={}",
var.to_string_lossy(),
value.unwrap_or_else(|| "<not set>".into())
);
}
result.pop();
Ok(ReportEntry::Code(Code {
language: Some("bash".into()),
code: result,
}))
}
}
pub struct CommandOutput<'a> {
title: &'a str,
cmd: OsString,
cmd_args: Vec<OsString>,
}
impl<'a> CommandOutput<'a> {
pub fn new<S, T>(title: &'a str, cmd: T, args: &[S]) -> Self
where
T: AsRef<OsStr>,
S: AsRef<OsStr>,
{
let mut cmd_args: Vec<OsString> = Vec::new();
for a in args {
cmd_args.push(a.into());
}
CommandOutput {
title,
cmd: cmd.as_ref().to_owned(),
cmd_args,
}
}
}
impl<'a> Collector for CommandOutput<'a> {
fn description(&self) -> &str {
self.title
}
fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
let mut result = String::new();
result += "> ";
result += &self.cmd.to_string_lossy();
result += " ";
for arg in &self.cmd_args {
result += &shell_escape::escape(arg.to_string_lossy());
result += " ";
}
result += "\n";
let output = Command::new(&self.cmd)
.args(&self.cmd_args)
.output()
.map_err(|e| {
CollectionError::CouldNotRetrieve(format!(
"Could not run command '{}': {}",
self.cmd.to_string_lossy(),
e
))
})?;
let utf8_decoding_error = |_| {
CollectionError::CouldNotRetrieve(format!(
"Error while running command '{}': output is not valid UTF-8.",
self.cmd.to_string_lossy()
))
};
let stdout = String::from_utf8(output.stdout).map_err(utf8_decoding_error)?;
let stderr = String::from_utf8(output.stderr).map_err(utf8_decoding_error)?;
result += &stdout;
result += &stderr;
result.trim_end_inplace();
let mut concat = vec![ReportEntry::Code(Code {
language: None,
code: result,
})];
if !output.status.success() {
concat.push(ReportEntry::Text(format!(
"Command failed{}.",
output
.status
.code()
.map_or("".into(), |c| format!(" with exit code {}", c))
)));
}
Ok(ReportEntry::Concat(concat))
}
}
pub struct FileContent<'a> {
title: &'a str,
path: PathBuf,
}
impl<'a> FileContent<'a> {
pub fn new<P: AsRef<Path>>(title: &'a str, path: P) -> Self {
Self {
title,
path: path.as_ref().to_path_buf(),
}
}
}
impl<'a> Collector for FileContent<'a> {
fn description(&self) -> &str {
self.title
}
fn collect(&mut self, _: &CrateInfo) -> Result<ReportEntry> {
let mut result = fs::read_to_string(&self.path).map_err(|e| {
CollectionError::CouldNotRetrieve(format!(
"Could not read contents of '{}': {}.",
self.path.to_string_lossy(),
e
))
})?;
result.trim_end_inplace();
Ok(ReportEntry::Code(Code {
language: None,
code: result,
}))
}
}