aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-core')
-rw-r--r--crates/mozart-core/src/console.rs194
-rw-r--r--crates/mozart-core/src/installer/suggested_packages_reporter.rs12
-rw-r--r--crates/mozart-core/src/repository/advisory.rs127
3 files changed, 245 insertions, 88 deletions
diff --git a/crates/mozart-core/src/console.rs b/crates/mozart-core/src/console.rs
index e036b11..3379307 100644
--- a/crates/mozart-core/src/console.rs
+++ b/crates/mozart-core/src/console.rs
@@ -67,6 +67,46 @@ pub fn hyperlink(url: &str, text: &str, decorated: bool) -> String {
}
// ---------------------------------------------------------------------------
+// IoInterface
+// ---------------------------------------------------------------------------
+
+/// The central IO abstraction, mirroring `\Composer\IO\IOInterface`.
+///
+/// All Mozart commands and library functions that need to produce output
+/// or interact with the user accept `&dyn IoInterface` (or an
+/// `Arc<Mutex<Box<dyn IoInterface>>>` at the top-level command boundary).
+pub trait IoInterface: Send + Sync {
+ fn write(&self, msg: &str, required: Verbosity);
+ fn write_stdout(&self, msg: &str, required: Verbosity);
+ fn write_error(&self, msg: &str);
+
+ fn info(&self, msg: &str);
+ fn verbose(&self, msg: &str);
+ fn very_verbose(&self, msg: &str);
+ fn debug(&self, msg: &str);
+ fn error(&self, msg: &str);
+
+ fn is_interactive(&self) -> bool;
+ fn is_decorated(&self) -> bool;
+ fn verbosity(&self) -> Verbosity;
+
+ fn is_verbose(&self) -> bool;
+ fn is_very_verbose(&self) -> bool;
+ fn is_debug(&self) -> bool;
+ fn is_quiet(&self) -> bool;
+
+ fn ask(&self, prompt: &str, default: &str) -> String;
+ #[allow(clippy::type_complexity)]
+ fn ask_validated(
+ &self,
+ prompt: &str,
+ default: &str,
+ validator: Box<dyn Fn(&str) -> Result<(), String>>,
+ ) -> Result<String, String>;
+ fn confirm(&self, prompt: &str) -> bool;
+}
+
+// ---------------------------------------------------------------------------
// Verbosity
// ---------------------------------------------------------------------------
@@ -212,6 +252,18 @@ impl Console {
// Query methods
// -----------------------------------------------------------------------
+ pub fn verbosity(&self) -> Verbosity {
+ self.verbosity
+ }
+
+ pub fn is_interactive(&self) -> bool {
+ self.interactive
+ }
+
+ pub fn is_decorated(&self) -> bool {
+ self.decorated
+ }
+
pub fn is_verbose(&self) -> bool {
self.verbosity >= Verbosity::Verbose
}
@@ -245,15 +297,13 @@ impl Console {
.unwrap_or_else(|_| default.to_string())
}
- pub fn ask_validated<F>(
+ #[allow(clippy::type_complexity)]
+ pub fn ask_validated(
&self,
prompt: &str,
default: &str,
- validator: F,
- ) -> Result<String, String>
- where
- F: Fn(&str) -> Result<(), String>,
- {
+ validator: Box<dyn Fn(&str) -> Result<(), String>>,
+ ) -> Result<String, String> {
if !self.interactive {
validator(default)?;
return Ok(default.to_string());
@@ -289,6 +339,130 @@ impl Console {
}
}
+impl<T: IoInterface + ?Sized> IoInterface for Box<T> {
+ fn write(&self, msg: &str, required: Verbosity) {
+ (**self).write(msg, required)
+ }
+ fn write_stdout(&self, msg: &str, required: Verbosity) {
+ (**self).write_stdout(msg, required)
+ }
+ fn write_error(&self, msg: &str) {
+ (**self).write_error(msg)
+ }
+ fn info(&self, msg: &str) {
+ (**self).info(msg)
+ }
+ fn verbose(&self, msg: &str) {
+ (**self).verbose(msg)
+ }
+ fn very_verbose(&self, msg: &str) {
+ (**self).very_verbose(msg)
+ }
+ fn debug(&self, msg: &str) {
+ (**self).debug(msg)
+ }
+ fn error(&self, msg: &str) {
+ (**self).error(msg)
+ }
+ fn is_interactive(&self) -> bool {
+ (**self).is_interactive()
+ }
+ fn is_decorated(&self) -> bool {
+ (**self).is_decorated()
+ }
+ fn verbosity(&self) -> Verbosity {
+ (**self).verbosity()
+ }
+ fn is_verbose(&self) -> bool {
+ (**self).is_verbose()
+ }
+ fn is_very_verbose(&self) -> bool {
+ (**self).is_very_verbose()
+ }
+ fn is_debug(&self) -> bool {
+ (**self).is_debug()
+ }
+ fn is_quiet(&self) -> bool {
+ (**self).is_quiet()
+ }
+ fn ask(&self, prompt: &str, default: &str) -> String {
+ (**self).ask(prompt, default)
+ }
+ fn ask_validated(
+ &self,
+ prompt: &str,
+ default: &str,
+ validator: Box<dyn Fn(&str) -> Result<(), String>>,
+ ) -> Result<String, String> {
+ (**self).ask_validated(prompt, default, validator)
+ }
+ fn confirm(&self, prompt: &str) -> bool {
+ (**self).confirm(prompt)
+ }
+}
+
+impl IoInterface for Console {
+ fn write(&self, msg: &str, required: Verbosity) {
+ self.write(msg, required)
+ }
+ fn write_stdout(&self, msg: &str, required: Verbosity) {
+ self.write_stdout(msg, required)
+ }
+ fn write_error(&self, msg: &str) {
+ self.write_error(msg)
+ }
+ fn info(&self, msg: &str) {
+ self.info(msg)
+ }
+ fn verbose(&self, msg: &str) {
+ self.verbose(msg)
+ }
+ fn very_verbose(&self, msg: &str) {
+ self.very_verbose(msg)
+ }
+ fn debug(&self, msg: &str) {
+ self.debug(msg)
+ }
+ fn error(&self, msg: &str) {
+ self.error(msg)
+ }
+ fn is_interactive(&self) -> bool {
+ self.is_interactive()
+ }
+ fn is_decorated(&self) -> bool {
+ self.is_decorated()
+ }
+ fn verbosity(&self) -> Verbosity {
+ self.verbosity()
+ }
+ fn is_verbose(&self) -> bool {
+ self.is_verbose()
+ }
+ fn is_very_verbose(&self) -> bool {
+ self.is_very_verbose()
+ }
+ fn is_debug(&self) -> bool {
+ self.is_debug()
+ }
+ fn is_quiet(&self) -> bool {
+ self.is_quiet()
+ }
+ fn ask(&self, prompt: &str, default: &str) -> String {
+ self.ask(prompt, default)
+ }
+ fn ask_validated(
+ &self,
+ prompt: &str,
+ default: &str,
+ validator: Box<dyn Fn(&str) -> Result<(), String>>,
+ ) -> Result<String, String> {
+ self.ask_validated(prompt, default, validator)
+ }
+ fn confirm(&self, prompt: &str) -> bool {
+ self.confirm(prompt)
+ }
+}
+
/// Writes a message to the output.
///
/// ref: \Composer\IO\IOInterface::write()
@@ -304,7 +478,7 @@ macro_rules! console_writeln {
$crate::console_writeln!($console, $verbosity, $fmt, $($arg)*,)
};
($console:expr, $verbosity:expr, $fmt:literal, $($arg:tt)*) => {
- if ($console).verbosity >= $verbosity {
+ if $console.lock().unwrap().verbosity() >= $verbosity {
::std::println!("{}", &::mozart_console_macros::console_format!($fmt, $($arg)*));
}
};
@@ -325,7 +499,7 @@ macro_rules! console_write {
$crate::console_writeln!($console, $verbosity, $fmt, $($arg)*,)
};
($console:expr, $verbosity:expr, $fmt:literal, $($arg:tt)*) => {
- if ($console).verbosity >= $verbosity {
+ if $console.lock().unwrap().verbosity() >= $verbosity {
::std::print!("{}", &::mozart_console_macros::console_format!($fmt, $($arg)*));
}
};
@@ -346,7 +520,7 @@ macro_rules! console_writeln_error {
$crate::console_writeln!($console, $verbosity, $fmt, $($arg)*,)
};
($console:expr, $verbosity:expr, $fmt:literal, $($arg:tt)*) => {
- if ($console).verbosity >= $verbosity {
+ if $console.lock().unwrap().verbosity() >= $verbosity {
::std::eprintln!("{}", &::mozart_console_macros::console_format!($fmt, $($arg)*));
}
};
@@ -367,7 +541,7 @@ macro_rules! console_write_error {
$crate::console_writeln!($console, $verbosity, $fmt, $($arg)*,)
};
($console:expr, $verbosity:expr, $fmt:literal, $($arg:tt)*) => {
- if ($console).verbosity >= $verbosity {
+ if $console.lock().unwrap().verbosity() >= $verbosity {
::std::eprint!("{}", &::mozart_console_macros::console_format!($fmt, $($arg)*));
}
};
diff --git a/crates/mozart-core/src/installer/suggested_packages_reporter.rs b/crates/mozart-core/src/installer/suggested_packages_reporter.rs
index 2a356fc..43ef8c4 100644
--- a/crates/mozart-core/src/installer/suggested_packages_reporter.rs
+++ b/crates/mozart-core/src/installer/suggested_packages_reporter.rs
@@ -5,7 +5,7 @@
//! Composer's reporter exposes so other entry points (install/update) can
//! emit a minimalistic post-install hint with the same code path.
-use crate::console::{Console, Verbosity};
+use crate::console::{IoInterface, Verbosity};
use crate::console_format;
use crate::installer::installed_repo::InstalledRepoLite;
use indexmap::IndexSet;
@@ -82,14 +82,14 @@ impl RootInfo {
/// install/update one-liner).
pub struct SuggestedPackagesReporter<'a> {
suggested_packages: Vec<Suggestion>,
- console: &'a Console,
+ io: &'a dyn IoInterface,
}
impl<'a> SuggestedPackagesReporter<'a> {
- pub fn new(console: &'a Console) -> Self {
+ pub fn new(io: &'a dyn IoInterface) -> Self {
Self {
suggested_packages: Vec::new(),
- console,
+ io,
}
}
@@ -204,7 +204,7 @@ impl<'a> SuggestedPackagesReporter<'a> {
) {
let suggestions = self.get_filtered_suggestions(installed_repo, only_dependents_of);
if !suggestions.is_empty() {
- self.console.write(
+ self.io.write(
&console_format!(
"<info>{} package suggestions were added by new dependencies, use `composer suggest` to see details.</info>",
suggestions.len()
@@ -215,7 +215,7 @@ impl<'a> SuggestedPackagesReporter<'a> {
}
fn write_line(&self, msg: &str) {
- if self.console.verbosity >= Verbosity::Normal {
+ if self.io.verbosity() >= Verbosity::Normal {
println!("{msg}");
}
}
diff --git a/crates/mozart-core/src/repository/advisory.rs b/crates/mozart-core/src/repository/advisory.rs
index 02a6e1a..08f59a1 100644
--- a/crates/mozart-core/src/repository/advisory.rs
+++ b/crates/mozart-core/src/repository/advisory.rs
@@ -1,7 +1,7 @@
use super::packagist::SecurityAdvisory;
use super::repository::RepositorySet;
use crate::advisory::{AbandonedHandling, AuditFormat};
-use crate::console::Console;
+use crate::console::IoInterface;
use crate::{console_writeln, console_writeln_error};
use indexmap::IndexMap;
use std::collections::BTreeMap;
@@ -88,7 +88,7 @@ impl Auditor {
/// Returns a bitmask: 0=ok, 1=vulnerable, 2=abandoned, 3=both.
pub async fn audit(
&self,
- console: &Console,
+ io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
repo_set: &RepositorySet,
packages: &[PackageInfo],
options: &AuditOptions<'_>,
@@ -132,7 +132,7 @@ impl Auditor {
&ignored_advisories,
&unreachable_repos,
&abandoned_packages,
- console,
+ &io,
);
return Ok(bitmask);
}
@@ -152,8 +152,8 @@ impl Auditor {
let msg = format!(
"Found {ignored_total} ignored security vulnerability advisor{plurality} affecting {ignored_pkg_count} package{pkg_plurality}{punctuation}"
);
- console_writeln_error!(console, "<info>{msg}</info>");
- self.output_advisories_ignored(console, &ignored_advisories, format);
+ console_writeln_error!(io, "<info>{msg}</info>");
+ self.output_advisories_ignored(&io, &ignored_advisories, format);
}
if active_pkg_count > 0 {
@@ -168,38 +168,35 @@ impl Auditor {
"Found {active_total} security vulnerability advisor{plurality} affecting {active_pkg_count} package{pkg_plurality}{punctuation}"
);
if options.warning_only {
- console_writeln_error!(console, "<warning>{msg}</warning>");
+ console_writeln_error!(io, "<warning>{msg}</warning>");
} else {
- console_writeln_error!(console, "<error>{msg}</error>");
+ console_writeln_error!(io, "<error>{msg}</error>");
}
- self.output_advisories(console, &advisories, format);
+ self.output_advisories(&io, &advisories, format);
}
if format == AuditFormat::Summary {
- console_writeln_error!(
- console,
- "Run \"mozart audit\" for a full list of advisories."
- );
+ console_writeln_error!(io, "Run \"mozart audit\" for a full list of advisories.");
}
} else {
console_writeln_error!(
- console,
+ io,
"<info>No security vulnerability advisories found.</info>",
);
}
if !unreachable_repos.is_empty() {
console_writeln_error!(
- console,
+ io,
"<warning>The following repositories were unreachable:</warning>",
);
for repo in &unreachable_repos {
- console_writeln_error!(console, " - {repo}");
+ console_writeln_error!(io, " - {repo}");
}
}
if !abandoned_packages.is_empty() && format != AuditFormat::Summary {
- self.output_abandoned_packages(console, &abandoned_packages, format);
+ self.output_abandoned_packages(&io, &abandoned_packages, format);
}
Ok(bitmask)
@@ -364,13 +361,13 @@ impl Auditor {
fn output_advisories(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
advisories: &BTreeMap<String, Vec<MatchedAdvisory>>,
format: AuditFormat,
) {
match format {
- AuditFormat::Table => self.output_advisories_table(console, advisories),
- AuditFormat::Plain => self.output_advisories_plain(console, advisories),
+ AuditFormat::Table => self.output_advisories_table(io, advisories),
+ AuditFormat::Plain => self.output_advisories_plain(io, advisories),
AuditFormat::Summary => {}
AuditFormat::Json => unreachable!(),
}
@@ -378,13 +375,13 @@ impl Auditor {
fn output_advisories_ignored(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
advisories: &BTreeMap<String, Vec<IgnoredAdvisory>>,
format: AuditFormat,
) {
match format {
- AuditFormat::Table => self.output_ignored_advisories_table(console, advisories),
- AuditFormat::Plain => self.output_ignored_advisories_plain(console, advisories),
+ AuditFormat::Table => self.output_ignored_advisories_table(io, advisories),
+ AuditFormat::Plain => self.output_ignored_advisories_plain(io, advisories),
AuditFormat::Summary => {}
AuditFormat::Json => unreachable!(),
}
@@ -392,30 +389,25 @@ impl Auditor {
fn output_advisories_table(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
advisories: &BTreeMap<String, Vec<MatchedAdvisory>>,
) {
for pkg_advisories in advisories.values() {
for matched in pkg_advisories {
- self.render_advisory_table(
- console,
- &matched.advisory,
- &matched.installed_version,
- None,
- );
+ self.render_advisory_table(io, &matched.advisory, &matched.installed_version, None);
}
}
}
fn output_ignored_advisories_table(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
advisories: &BTreeMap<String, Vec<IgnoredAdvisory>>,
) {
for pkg_advisories in advisories.values() {
for ignored in pkg_advisories {
self.render_advisory_table(
- console,
+ io,
&ignored.advisory,
&ignored.installed_version,
ignored.ignore_reason.as_deref(),
@@ -426,7 +418,7 @@ impl Auditor {
fn render_advisory_table(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
adv: &SecurityAdvisory,
installed_version: &str,
ignore_reason: Option<&str>,
@@ -459,10 +451,10 @@ impl Auditor {
vw = value_width
);
- console_writeln_error!(console, "{}", separator);
+ console_writeln_error!(io, "{}", separator);
for (label, value) in &rows {
console_writeln_error!(
- console,
+ io,
"| {:<lw$} | {:<vw$} |",
label,
value,
@@ -470,27 +462,22 @@ impl Auditor {
vw = value_width,
);
}
- console_writeln_error!(console, "{}", &separator);
- console_writeln_error!(console, "");
+ console_writeln_error!(io, "{}", &separator);
+ console_writeln_error!(io, "");
}
fn output_advisories_plain(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
advisories: &BTreeMap<String, Vec<MatchedAdvisory>>,
) {
let mut first = true;
for pkg_advisories in advisories.values() {
for matched in pkg_advisories {
if !first {
- console_writeln_error!(console, "--------");
+ console_writeln_error!(io, "--------");
}
- self.render_advisory_plain(
- console,
- &matched.advisory,
- &matched.installed_version,
- None,
- );
+ self.render_advisory_plain(io, &matched.advisory, &matched.installed_version, None);
first = false;
}
}
@@ -498,17 +485,17 @@ impl Auditor {
fn output_ignored_advisories_plain(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
advisories: &BTreeMap<String, Vec<IgnoredAdvisory>>,
) {
let mut first = true;
for pkg_advisories in advisories.values() {
for ignored in pkg_advisories {
if !first {
- console_writeln_error!(console, "--------");
+ console_writeln_error!(io, "--------");
}
self.render_advisory_plain(
- console,
+ io,
&ignored.advisory,
&ignored.installed_version,
ignored.ignore_reason.as_deref(),
@@ -520,39 +507,35 @@ impl Auditor {
fn render_advisory_plain(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
adv: &SecurityAdvisory,
installed_version: &str,
ignore_reason: Option<&str>,
) {
- console_writeln_error!(console, "Package: {}", adv.package_name);
- console_writeln_error!(console, "Version: {installed_version}");
- console_writeln_error!(
- console,
- "Severity: {}",
- adv.severity.as_deref().unwrap_or(""),
- );
- console_writeln_error!(console, "Advisory ID: {}", adv.advisory_id);
- console_writeln_error!(console, "CVE: {}", adv.cve.as_deref().unwrap_or("NO CVE"));
- console_writeln_error!(console, "Title: {}", adv.title);
- console_writeln_error!(console, "URL: {}", adv.link.as_deref().unwrap_or(""));
- console_writeln_error!(console, "Affected versions: {}", adv.affected_versions);
- console_writeln_error!(console, "Reported at: {}", adv.reported_at);
+ console_writeln_error!(io, "Package: {}", adv.package_name);
+ console_writeln_error!(io, "Version: {installed_version}");
+ console_writeln_error!(io, "Severity: {}", adv.severity.as_deref().unwrap_or(""),);
+ console_writeln_error!(io, "Advisory ID: {}", adv.advisory_id);
+ console_writeln_error!(io, "CVE: {}", adv.cve.as_deref().unwrap_or("NO CVE"));
+ console_writeln_error!(io, "Title: {}", adv.title);
+ console_writeln_error!(io, "URL: {}", adv.link.as_deref().unwrap_or(""));
+ console_writeln_error!(io, "Affected versions: {}", adv.affected_versions);
+ console_writeln_error!(io, "Reported at: {}", adv.reported_at);
if let Some(reason) = ignore_reason {
- console_writeln_error!(console, "Ignore reason: {reason}");
+ console_writeln_error!(io, "Ignore reason: {reason}");
}
}
fn output_abandoned_packages(
&self,
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
packages: &[AbandonedPackage],
format: AuditFormat,
) {
let count = packages.len();
let plurality = if count == 1 { "" } else { "s" };
console_writeln_error!(
- console,
+ io,
"<error>Found {count} abandoned package{plurality}:</error>",
);
@@ -560,14 +543,14 @@ impl Auditor {
for pkg in packages {
match &pkg.replacement {
Some(repl) => console_writeln_error!(
- console,
+ io,
"{} ({}) is abandoned. Use {} instead.",
pkg.name,
pkg.version,
repl,
),
None => console_writeln_error!(
- console,
+ io,
"{} ({}) is abandoned. No replacement was suggested.",
pkg.name,
pkg.version,
@@ -598,7 +581,7 @@ impl Auditor {
.max("Suggested Replacement".len());
console_writeln_error!(
- console,
+ io,
"| {:<nw$} | {:<vw$} | {:<rw$} |",
"Abandoned Package",
"Version",
@@ -608,7 +591,7 @@ impl Auditor {
rw = repl_width,
);
console_writeln_error!(
- console,
+ io,
"+-{:-<nw$}-+-{:-<vw$}-+-{:-<rw$}-+",
"",
"",
@@ -623,7 +606,7 @@ impl Auditor {
.as_deref()
.unwrap_or("No replacement suggested");
console_writeln_error!(
- console,
+ io,
"| {:<nw$} | {:<vw$} | {:<rw$} |",
pkg.name,
pkg.version,
@@ -633,7 +616,7 @@ impl Auditor {
rw = repl_width,
);
}
- console_writeln_error!(console, "");
+ console_writeln_error!(io, "");
}
fn render_json(
@@ -642,7 +625,7 @@ impl Auditor {
ignored_advisories: &BTreeMap<String, Vec<IgnoredAdvisory>>,
unreachable_repos: &[String],
abandoned_packages: &[AbandonedPackage],
- console: &Console,
+ io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>,
) {
let mut advisories_map: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
for (pkg_name, matched_list) in advisories {
@@ -720,7 +703,7 @@ impl Auditor {
}
let json_str = serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string());
- console_writeln!(console, "{}", &json_str);
+ console_writeln!(io, "{}", &json_str);
}
}