refactor: enforce layered crates and Rust audit gates

This commit is contained in:
Horldsence
2026-09-24 13:00:03 +08:00
parent af151525d1
commit 8a582fc102
67 changed files with 3918 additions and 581 deletions
+961
View File
@@ -0,0 +1,961 @@
#!/usr/bin/env -S cargo +nightly -Zscript
---
[package]
edition = "2024"
[dependencies]
regex = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.9"
---
use regex::Regex;
use serde::Serialize;
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::env;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use toml::{Table, Value as TomlValue};
type AnyError = Box<dyn Error>;
const POLICY_RELATIVE: &str = "config/dependency-policy.toml";
#[derive(Debug, Clone, Serialize)]
struct Issue {
code: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
package: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
}
impl Issue {
fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
package: None,
path: None,
}
}
fn package(mut self, package: impl Into<String>) -> Self {
self.package = Some(package.into());
self
}
fn path(mut self, path: impl AsRef<Path>) -> Self {
self.path = Some(path.as_ref().display().to_string());
self
}
}
#[derive(Debug, Clone)]
struct Package {
name: String,
manifest_path: PathBuf,
package_dir: PathBuf,
metadata: Value,
dependencies: Vec<Value>,
}
#[derive(Debug, Default)]
struct Graph {
normal: BTreeMap<String, BTreeSet<String>>,
dev: BTreeMap<String, BTreeSet<String>>,
}
fn root_dir() -> Result<PathBuf, AnyError> {
let start = env::current_dir()?;
start
.ancestors()
.find(|directory| directory.join(POLICY_RELATIVE).is_file())
.map(Path::to_path_buf)
.ok_or_else(|| "run from inside the sentry-chassis workspace".into())
}
fn read_toml(path: &Path) -> Result<Table, AnyError> {
let text = fs::read_to_string(path)?;
Ok(text.parse::<Table>()?)
}
fn run(command: &mut Command) -> Result<Output, AnyError> {
let output = command.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("command failed: {stderr}").into());
}
Ok(output)
}
fn cargo_metadata(root: &Path) -> Result<Value, AnyError> {
let output = run(Command::new("cargo")
.current_dir(root)
.args(["metadata", "--locked", "--format-version", "1", "--no-deps"]))?;
Ok(serde_json::from_slice(&output.stdout)?)
}
fn string_array(value: Option<&TomlValue>) -> BTreeSet<String> {
value
.and_then(TomlValue::as_array)
.into_iter()
.flatten()
.filter_map(TomlValue::as_str)
.map(str::to_owned)
.collect()
}
fn toml_string<'a>(table: &'a Table, key: &str) -> Result<&'a str, AnyError> {
table
.get(key)
.and_then(TomlValue::as_str)
.ok_or_else(|| format!("policy field {key} must be a string").into())
}
fn toml_i64(table: &Table, key: &str) -> Result<i64, AnyError> {
table
.get(key)
.and_then(TomlValue::as_integer)
.ok_or_else(|| format!("policy field {key} must be an integer").into())
}
fn toml_table<'a>(table: &'a Table, key: &str) -> Result<&'a Table, AnyError> {
table
.get(key)
.and_then(TomlValue::as_table)
.ok_or_else(|| format!("policy field {key} must be a table").into())
}
fn package_map(metadata: &Value) -> Result<BTreeMap<String, Package>, AnyError> {
let members: HashSet<&str> = metadata["workspace_members"]
.as_array()
.ok_or("workspace_members must be an array")?
.iter()
.filter_map(Value::as_str)
.collect();
let mut packages = BTreeMap::new();
for item in metadata["packages"]
.as_array()
.ok_or("packages must be an array")?
{
if !members.contains(item["id"].as_str().unwrap_or_default()) {
continue;
}
let name = item["name"]
.as_str()
.ok_or("workspace package has no name")?
.to_owned();
let manifest_path = PathBuf::from(
item["manifest_path"]
.as_str()
.ok_or_else(|| format!("package {name} has no manifest_path"))?,
);
let package_dir = manifest_path
.parent()
.ok_or_else(|| format!("package {name} has no package directory"))?
.to_path_buf();
packages.insert(
name.clone(),
Package {
name,
package_dir,
manifest_path,
metadata: item.clone(),
dependencies: item["dependencies"]
.as_array()
.cloned()
.unwrap_or_default(),
},
);
}
Ok(packages)
}
fn package_name_by_path(packages: &BTreeMap<String, Package>) -> BTreeMap<PathBuf, String> {
packages
.values()
.map(|package| (package.package_dir.clone(), package.name.clone()))
.collect()
}
fn dependency_target(
dependency: &Value,
by_path: &BTreeMap<PathBuf, String>,
) -> Option<String> {
let path = dependency.get("path")?.as_str()?;
by_path.get(&PathBuf::from(path)).cloned()
}
fn build_graph(
packages: &BTreeMap<String, Package>,
) -> Graph {
let by_path = package_name_by_path(packages);
let mut graph = Graph::default();
for package in packages.values() {
graph.normal.entry(package.name.clone()).or_default();
graph.dev.entry(package.name.clone()).or_default();
for dependency in &package.dependencies {
let Some(target) = dependency_target(dependency, &by_path) else {
continue;
};
let kind = dependency["kind"].as_str().unwrap_or("normal");
let edges = if kind == "dev" {
&mut graph.dev
} else {
&mut graph.normal
};
edges
.entry(package.name.clone())
.or_default()
.insert(target);
}
}
graph
}
fn check_rank(source: &str, target: &str, ranks: &BTreeMap<String, i64>) -> bool {
match (ranks.get(source), ranks.get(target)) {
(Some(source_rank), Some(target_rank)) => target_rank < source_rank,
_ => false,
}
}
fn check_layer_path(layer: &str, relative_path: &str) -> bool {
let parts: Vec<String> = Path::new(relative_path)
.components()
.map(|part| part.as_os_str().to_string_lossy().into_owned())
.collect();
parts.len() == 3 && parts[0] == "crates" && format!("{}/{}", parts[1], parts[2]) == layer
}
fn contains_forbidden_marker(text: &str) -> bool {
let marker = Regex::new(r"\bunsafe\b|libc|no_std").expect("valid marker regex");
marker.is_match(text)
}
fn declares_trait(text: &str, symbol: &str) -> bool {
let pattern = format!(r"\bpub trait {}\b", regex::escape(symbol));
Regex::new(&pattern)
.expect("valid trait regex")
.is_match(text)
}
fn contains_forbidden_symbol(text: &str, symbol: &str) -> bool {
let pattern = format!(r"\b{}\b", regex::escape(symbol));
Regex::new(&pattern)
.expect("valid symbol regex")
.is_match(text)
}
fn dependency_issues(
kind: &str,
source: &str,
actual: &BTreeSet<String>,
allowed: &BTreeSet<String>,
) -> Vec<Issue> {
let mut issues = actual
.difference(allowed)
.map(|target| {
Issue::new(
"dependency-edge",
format!("{kind} dependency {source} -> {target} is not declared"),
)
.package(source)
})
.collect::<Vec<_>>();
issues.extend(allowed.difference(actual).map(|target| {
Issue::new(
"dependency-edge",
format!("declared {kind} dependency {source} -> {target} is absent"),
)
.package(source)
}));
issues
}
fn layer_declaration_matches(declared: Option<&str>, expected: &str) -> bool {
declared == Some(expected)
}
fn find_cycles(edges: &BTreeMap<String, BTreeSet<String>>) -> Vec<Vec<String>> {
#[derive(Clone, Copy, PartialEq)]
enum Color {
White,
Gray,
Black,
}
fn visit(
node: &str,
edges: &BTreeMap<String, BTreeSet<String>>,
colors: &mut BTreeMap<String, Color>,
trail: &mut Vec<String>,
cycles: &mut Vec<Vec<String>>,
) {
colors.insert(node.to_owned(), Color::Gray);
trail.push(node.to_owned());
for target in edges.get(node).into_iter().flatten() {
match colors.get(target).copied().unwrap_or(Color::White) {
Color::Gray => {
let mut cycle = trail.clone();
cycle.push(target.clone());
cycles.push(cycle);
}
Color::White => visit(target, edges, colors, trail, cycles),
Color::Black => {}
}
}
trail.pop();
colors.insert(node.to_owned(), Color::Black);
}
let mut colors = BTreeMap::new();
let mut cycles = Vec::new();
for node in edges.keys() {
if colors.get(node).copied().unwrap_or(Color::White) == Color::White {
visit(node, edges, &mut colors, &mut Vec::new(), &mut cycles);
}
}
cycles
}
fn package_declared_layer(package: &Package) -> Option<String> {
package
.metadata
.get("metadata")
.and_then(|metadata| metadata.get("sentry-chassis"))
.and_then(|metadata| metadata.get("layer"))
.and_then(Value::as_str)
.map(str::to_owned)
}
fn policy_packages(policy: &Table) -> Result<&Table, AnyError> {
toml_table(policy, "packages")
}
fn policy_package<'a>(policy: &'a Table, name: &str) -> Result<&'a Table, AnyError> {
policy_packages(policy)?
.get(name)
.and_then(TomlValue::as_table)
.ok_or_else(|| format!("policy package {name} is missing").into())
}
fn rust_files_in(directory: &Path) -> Vec<PathBuf> {
fs::read_dir(directory)
.into_iter()
.flatten()
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|extension| extension == "rs"))
.collect()
}
fn check_layout_and_markers(
root: &Path,
packages: &BTreeMap<String, Package>,
) -> Result<Vec<Issue>, AnyError> {
let mut issues = Vec::new();
for package in packages.values() {
let src = package.package_dir.join("src");
let tests = package.package_dir.join("tests");
let mut rust_files = rust_files_in(&src);
rust_files.extend(rust_files_in(&tests));
if !src.is_dir() || rust_files.iter().all(|path| path.parent() != Some(src.as_path())) {
issues.push(
Issue::new("layout", "package must have src/*.rs")
.package(&package.name)
.path(&src),
);
}
for directory in [&src, &tests] {
if !directory.is_dir() {
continue;
}
for entry in fs::read_dir(directory)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
issues.push(
Issue::new("layout", "source/test directories must remain flat")
.package(&package.name)
.path(entry.path()),
);
}
}
}
let mut scan_paths = vec![package.manifest_path.clone()];
scan_paths.extend(rust_files);
for path in scan_paths {
let text = fs::read_to_string(&path)?;
if contains_forbidden_marker(&text) {
issues.push(
Issue::new("forbidden-marker", "unsafe/libc/no_std marker found")
.package(&package.name)
.path(path),
);
}
}
}
let _ = root;
Ok(issues)
}
fn dependency_tables(manifest: &Table) -> Vec<&Table> {
let mut tables = Vec::new();
for key in ["dependencies", "dev-dependencies"] {
if let Some(table) = manifest.get(key).and_then(TomlValue::as_table) {
tables.push(table);
}
}
if let Some(targets) = manifest.get("target").and_then(TomlValue::as_table) {
for target in targets.values().filter_map(TomlValue::as_table) {
for key in ["dependencies", "dev-dependencies"] {
if let Some(table) = target.get(key).and_then(TomlValue::as_table) {
tables.push(table);
}
}
}
}
tables
}
fn check_workspace_inheritance(
root: &Path,
packages: &BTreeMap<String, Package>,
) -> Result<Vec<Issue>, AnyError> {
let root_manifest = read_toml(&root.join("Cargo.toml"))?;
let workspace = toml_table(&root_manifest, "workspace")?;
let aliases = toml_table(workspace, "dependencies")?
.keys()
.cloned()
.collect::<BTreeSet<_>>();
let mut issues = Vec::new();
for package in packages.values() {
let manifest = read_toml(&package.manifest_path)?;
for table in dependency_tables(&manifest) {
for (dependency, specification) in table {
if !aliases.contains(dependency) {
issues.push(
Issue::new(
"workspace-inheritance",
format!("{dependency} is not declared in workspace.dependencies"),
)
.package(&package.name),
);
}
let Some(specification) = specification.as_table() else {
continue;
};
if !specification
.get("workspace")
.and_then(TomlValue::as_bool)
.unwrap_or(false)
{
issues.push(
Issue::new(
"workspace-inheritance",
format!("{dependency} must use workspace = true"),
)
.package(&package.name),
);
}
if specification.contains_key("git") || specification.contains_key("path") {
issues.push(
Issue::new(
"external-source",
format!("{dependency} cannot use git/path directly"),
)
.package(&package.name),
);
}
if specification.get("version").and_then(TomlValue::as_str) == Some("*") {
issues.push(
Issue::new(
"version-policy",
format!("{dependency} cannot use wildcard version"),
)
.package(&package.name),
);
}
}
}
}
Ok(issues)
}
fn check_trait_boundaries(root: &Path, policy: &Table) -> Result<Vec<Issue>, AnyError> {
let boundaries = toml_table(policy, "trait_boundaries")?;
let required = [
("protocol_motor", root.join("crates/protocol/motor/src/lib.rs")),
("device_motor", root.join("crates/device/motor/src/lib.rs")),
("model_motor", root.join("crates/model/motor/src/lib.rs")),
];
let mut issues = Vec::new();
for (key, path) in required {
let text = fs::read_to_string(&path)?;
for symbol in string_array(boundaries.get(key)) {
if !declares_trait(&text, &symbol) {
issues.push(
Issue::new(
"trait-boundary",
format!("{symbol} must be declared in {}", path.display()),
)
.path(&path),
);
}
}
}
let esc_path = root.join("crates/device/esc/src/lib.rs");
let esc_text = fs::read_to_string(&esc_path)?;
for symbol in string_array(boundaries.get("esc_bus_forbidden_symbols")) {
if contains_forbidden_symbol(&esc_text, &symbol) {
issues.push(
Issue::new(
"trait-boundary",
format!("{symbol} must not branch in esc-bus"),
)
.path(&esc_path),
);
}
}
Ok(issues)
}
fn external_dependencies(package: &Package) -> (BTreeSet<String>, BTreeSet<String>) {
let mut normal = BTreeSet::new();
let mut dev = BTreeSet::new();
for dependency in &package.dependencies {
if !dependency
.get("source")
.is_some_and(|source| !source.is_null())
{
continue;
}
let Some(name) = dependency["name"].as_str() else {
continue;
};
if dependency["kind"].as_str() == Some("dev") {
dev.insert(name.to_owned());
} else {
normal.insert(name.to_owned());
}
}
(normal, dev)
}
fn audit(root: &Path) -> Result<Vec<Issue>, AnyError> {
let policy = read_toml(&root.join(POLICY_RELATIVE))?;
let metadata = cargo_metadata(root)?;
let packages = package_map(&metadata)?;
let graph = build_graph(&packages);
let mut issues = Vec::new();
let expected = policy_packages(&policy)?
.keys()
.cloned()
.collect::<BTreeSet<_>>();
let actual = packages.keys().cloned().collect::<BTreeSet<_>>();
for name in expected.difference(&actual) {
issues.push(
Issue::new("workspace", format!("missing workspace package {name}"))
.package(name),
);
}
for name in actual.difference(&expected) {
issues.push(
Issue::new("workspace", format!("undeclared workspace package {name}"))
.package(name),
);
}
let layer_ranks = toml_table(&policy, "layers")?
.iter()
.map(|(layer, rank)| {
Ok((
layer.clone(),
rank.as_integer()
.ok_or_else(|| format!("layer {layer} rank must be an integer"))?,
))
})
.collect::<Result<BTreeMap<String, i64>, AnyError>>()?;
let package_ranks = policy_packages(&policy)?
.iter()
.map(|(name, specification)| {
let specification = specification
.as_table()
.ok_or_else(|| format!("package {name} policy must be a table"))?;
Ok((name.clone(), toml_i64(specification, "rank")?))
})
.collect::<Result<BTreeMap<String, i64>, AnyError>>()?;
for package in packages.values() {
let specification = match policy_package(&policy, &package.name) {
Ok(specification) => specification,
Err(error) => {
issues.push(Issue::new("workspace", error.to_string()).package(&package.name));
continue;
}
};
let expected_layer = toml_string(specification, "layer")?;
let declared_layer = package_declared_layer(package);
if !layer_declaration_matches(declared_layer.as_deref(), expected_layer) {
issues.push(
Issue::new(
"layer-declaration",
format!(
"declared layer {:?} != policy {expected_layer}",
declared_layer
),
)
.package(&package.name),
);
}
let relative = package
.package_dir
.strip_prefix(root)
.map_err(|_| format!("package path escapes workspace root"))?
.display()
.to_string();
let expected_path = toml_string(specification, "path")?;
if relative != expected_path {
issues.push(
Issue::new(
"layer-path",
format!("path {relative} != policy {expected_path}"),
)
.package(&package.name),
);
}
if !check_layer_path(expected_layer, &relative) {
issues.push(
Issue::new(
"layer-path",
format!("path does not match exactly two-part layer {expected_layer}"),
)
.package(&package.name),
);
}
let rank = toml_i64(specification, "rank")?;
if layer_ranks.get(expected_layer) != Some(&rank) {
issues.push(
Issue::new("layer-rank", "package rank differs from layer rank")
.package(&package.name),
);
}
for (kind, actual_edges) in [
("normal", graph.normal.get(&package.name).cloned().unwrap_or_default()),
("dev", graph.dev.get(&package.name).cloned().unwrap_or_default()),
] {
let allowed = string_array(specification.get(kind));
issues.extend(dependency_issues(
kind,
&package.name,
&actual_edges,
&allowed,
));
for target in &actual_edges {
if !check_rank(&package.name, target, &package_ranks) {
issues.push(
Issue::new(
"layer-order",
format!("{kind} dependency {} -> {target} is not downward", package.name),
)
.package(&package.name),
);
}
}
}
let (external, dev_external) = external_dependencies(package);
for (key, actual_external) in [
("external", external.clone()),
("dev_external", dev_external.clone()),
] {
let allowed = string_array(specification.get(key));
for dependency in actual_external.difference(&allowed) {
issues.push(
Issue::new(
"external-edge",
format!("{key} dependency {dependency} is not declared"),
)
.package(&package.name),
);
}
for dependency in allowed.difference(&actual_external) {
issues.push(
Issue::new(
"external-edge",
format!("declared {key} dependency {dependency} is absent"),
)
.package(&package.name),
);
}
}
let top_layer = expected_layer
.split('/')
.next()
.ok_or("layer must contain a slash")?;
let roles = toml_table(&policy, "external_roles")?;
for dependency in external.union(&dev_external) {
let Some(allowed_layers) = roles.get(dependency).and_then(TomlValue::as_array) else {
issues.push(
Issue::new(
"external-role",
format!("external dependency {dependency} has no role declaration"),
)
.package(&package.name),
);
continue;
};
let allowed = allowed_layers
.iter()
.filter_map(TomlValue::as_str)
.any(|layer| layer == top_layer);
if !allowed {
issues.push(
Issue::new(
"external-role",
format!(
"external dependency {dependency} is not allowed in {expected_layer}"
),
)
.package(&package.name),
);
}
}
}
let forbidden = toml_table(&policy, "forbidden")?;
let forbidden_normal = toml_table(forbidden, "normal")?;
for (source, targets) in forbidden_normal {
let source = source.replace('_', "-");
for target in string_array(Some(targets)) {
if graph
.normal
.get(&source)
.is_some_and(|edges| edges.contains(&target))
{
issues.push(
Issue::new(
"forbidden-edge",
format!("{source} -> {target} is forbidden"),
)
.package(&source),
);
}
}
}
let mut all_edges = graph.normal.clone();
for (source, targets) in &graph.dev {
all_edges.entry(source.clone()).or_default().extend(targets.clone());
}
for cycle in find_cycles(&all_edges) {
issues.push(Issue::new(
"dependency-cycle",
cycle.join(" -> "),
));
}
issues.extend(check_layout_and_markers(root, &packages)?);
issues.extend(check_workspace_inheritance(root, &packages)?);
issues.extend(check_trait_boundaries(root, &policy)?);
Ok(issues)
}
fn git_lines(root: &Path, args: &[&str]) -> Result<Vec<String>, AnyError> {
let output = run(Command::new("git").current_dir(root).args(args))?;
Ok(String::from_utf8(output.stdout)?
.lines()
.map(str::to_owned)
.collect())
}
fn is_audit_path(path: &str) -> bool {
const EXACT: &[&str] = &[
"Cargo.toml",
"Cargo.lock",
"justfile",
"ARCHITECTURE.md",
"README.md",
];
const PREFIXES: &[&str] = &[
"crates/",
"config/",
"scripts/",
".githooks/",
".github/",
"docs/",
];
EXACT.contains(&path) || PREFIXES.iter().any(|prefix| path.starts_with(prefix))
}
fn staged_guard(root: &Path) -> Result<Vec<Issue>, AnyError> {
let staged: BTreeSet<String> = git_lines(
root,
&["diff", "--cached", "--name-only", "--diff-filter=ACMR"],
)?
.into_iter()
.collect();
let unstaged: BTreeSet<String> = git_lines(
root,
&["diff", "--name-only", "--diff-filter=ACMR"],
)?
.into_iter()
.collect();
Ok(staged
.intersection(&unstaged)
.filter(|path| is_audit_path(path))
.map(|path| {
Issue::new(
"staged-mixed",
format!("{path} has both staged and unstaged changes; stage or restore it before committing"),
)
.path(path)
})
.collect())
}
fn self_test() -> Result<(), AnyError> {
let mut ranks = BTreeMap::new();
ranks.insert("device/esc".to_owned(), 4_i64);
ranks.insert("control/motor".to_owned(), 6);
ranks.insert("device/motor".to_owned(), 3);
assert!(check_rank("device/esc", "device/motor", &ranks));
assert!(!check_rank("device/motor", "device/esc", &ranks));
assert!(!check_rank("device/esc", "device/esc", &ranks));
assert!(check_layer_path("protocol/motor", "crates/protocol/motor"));
assert!(!check_layer_path(
"protocol/motor",
"crates/protocol/motor/motor-protocol"
));
let mut edges = BTreeMap::new();
edges.insert("a".to_owned(), BTreeSet::from(["b".to_owned()]));
edges.insert("b".to_owned(), BTreeSet::from(["a".to_owned()]));
assert_eq!(find_cycles(&edges), vec![vec!["a", "b", "a"]]);
let issues = dependency_issues(
"dev",
"device/esc",
&BTreeSet::from(["control/motor".to_owned()]),
&BTreeSet::new(),
);
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].code, "dependency-edge");
assert!(layer_declaration_matches(Some("protocol/motor"), "protocol/motor"));
assert!(!layer_declaration_matches(Some("device/motor"), "protocol/motor"));
assert!(contains_forbidden_marker("use libc::c_int;"));
assert!(!contains_forbidden_marker("use std::num::NonZeroU8;"));
assert!(declares_trait("pub trait MotorProtocol {}", "MotorProtocol"));
assert!(!declares_trait("struct MotorProtocol;", "MotorProtocol"));
assert!(contains_forbidden_symbol("match EscModel {}", "EscModel"));
println!("audit-rules self-test: ok");
Ok(())
}
fn parse_args() -> Result<(bool, bool, String), AnyError> {
let mut staged = false;
let mut self_test_flag = false;
let mut format = "text".to_owned();
let mut args = env::args().skip(1);
while let Some(argument) = args.next() {
match argument.as_str() {
"--staged" => staged = true,
"--self-test" => self_test_flag = true,
"--format" => {
format = args
.next()
.ok_or("--format requires text or json")?;
if format != "text" && format != "json" {
return Err("--format must be text or json".into());
}
}
other => return Err(format!("unknown argument: {other}").into()),
}
}
Ok((staged, self_test_flag, format))
}
fn main() -> Result<(), AnyError> {
let (staged, self_test_flag, format) = parse_args()?;
if self_test_flag {
return self_test();
}
let root = root_dir()?;
let mut issues = if staged { staged_guard(&root)? } else { Vec::new() };
match audit(&root) {
Ok(mut audit_issues) => issues.append(&mut audit_issues),
Err(error) => issues.push(Issue::new("audit-failed", error.to_string())),
}
if format == "json" {
println!("{}", serde_json::to_string_pretty(&issues)?);
} else {
for issue in &issues {
let package = issue
.package
.as_ref()
.map(|package| format!(" ({package})"))
.unwrap_or_default();
let path = issue
.path
.as_ref()
.map(|path| format!(" [{path}]"))
.unwrap_or_default();
println!("{}{package}{path}: {}", issue.code, issue.message);
}
}
if issues.is_empty() {
Ok(())
} else {
Err("audit rules found violations".into())
}
}