zoi_cli/cmd/package/
verify.rs1use std::path::PathBuf;
8
9use anyhow::Result;
10use clap::ValueEnum;
11use colored::Colorize;
12use serde_json::json;
13
14use crate::pkg::install::verify;
15use crate::pkg::{local, types};
16
17#[derive(ValueEnum, Debug, Clone)]
19pub enum VerifyTarget {
20 Installed,
22 Archive
24}
25
26#[derive(clap::Parser, Debug)]
28pub struct VerifyCommand {
29 #[arg(default_value = "installed")]
31 pub target: VerifyTarget,
32
33 #[arg(value_name = "PACKAGES")]
36 pub packages: Vec<String>,
37
38 #[arg(long = "file")]
41 pub archive: Option<PathBuf>,
42
43 #[arg(long, value_enum)]
45 pub scope: Option<crate::cli::SetupScope>,
46
47 #[arg(long)]
49 pub json: bool
50}
51
52pub fn run(cmd: &VerifyCommand) -> Result<()> {
59 match cmd.target {
60 VerifyTarget::Archive => run_archive(cmd),
61 VerifyTarget::Installed => run_installed(cmd)
62 }
63}
64
65fn run_archive(cmd: &VerifyCommand) -> Result<()> {
67 let Some(archive) = cmd.archive.as_ref() else {
68 return Err(anyhow::anyhow!(
69 "The --file option is required when verifying archives"
70 ));
71 };
72
73 let report = verify::verify_archive(archive)?;
74
75 if cmd.json {
76 println!("{}", serde_json::to_string_pretty(&report)?);
77 } else {
78 println!(
79 "{} Verifying archive {}",
80 "::".bold().blue(),
81 report.archive
82 );
83 if report.signed_embed {
84 println!(" Embedded signature: present");
85 }
86 println!(" Pooled files checked: {}", report.checked);
87
88 if report.ok {
89 println!("{}", "Archive integrity verified.".green());
90 } else {
91 for issue in &report.issues {
92 println!(" {}: {issue}", "PROBLEM".red().bold());
93 }
94 }
95 }
96
97 if !report.ok {
98 std::process::exit(1);
99 }
100 Ok(())
101}
102
103fn run_installed(cmd: &VerifyCommand) -> Result<()> {
105 let all = local::get_installed_packages()?;
106 let manifests: Vec<_> = if cmd.packages.is_empty() {
107 all
108 } else {
109 let wanted = &cmd.packages;
110 let mut found = Vec::new();
111 for manifest in all {
112 let matches_name = wanted.iter().any(|p| {
113 p.eq_ignore_ascii_case(&manifest.name)
114 || p.eq_ignore_ascii_case(&manifest.repo)
115 || manifest.sub_package.as_deref() == Some(p.as_str())
116 || format!("@{}/{}", manifest.repo, manifest.name)
117 .eq_ignore_ascii_case(p)
118 });
119 if matches_name {
120 found.push(manifest);
121 } else {
122 eprintln!(
123 "{}: Package not installed: {}",
124 "Warning".yellow().bold(),
125 local::installed_manifest_source(&manifest)
128 );
129 }
130 }
131 found
132 };
133
134 let manifests: Vec<_> = manifests
135 .into_iter()
136 .filter(|m| {
137 cmd.scope.is_none_or(|s| {
138 matches!(
139 (s, m.scope),
140 (crate::cli::SetupScope::User, types::Scope::User)
141 | (
142 crate::cli::SetupScope::System,
143 types::Scope::System
144 )
145 )
146 })
147 })
148 .collect();
149
150 if manifests.is_empty() {
151 if !cmd.json {
152 println!("No installed packages matched.");
153 }
154 return Ok(());
155 }
156
157 let mut total_bad = 0usize;
158 let mut json_reports = Vec::new();
159
160 for manifest in &manifests {
161 let statuses = verify::verify_installed(manifest)?;
162 let bad: Vec<_> = statuses
163 .iter()
164 .filter(|s| {
165 !matches!(
166 s.status,
167 verify::VerifyStatus::Ok | verify::VerifyStatus::Unverified
168 )
169 })
170 .collect();
171
172 if cmd.json {
173 json_reports.push(json!({
174 "name": manifest.name,
175 "version": manifest.version,
176 "sub_package": manifest.sub_package,
177 "scope": format!("{:?}", manifest.scope).to_lowercase(),
178 "files": statuses,
179 "ok": bad.is_empty()
180 }));
181 } else {
182 let display_name = match &manifest.sub_package {
183 Some(sub) => format!("{}:{sub}", manifest.name),
184 None => manifest.name.clone()
185 };
186 let scope_tag = match manifest.scope {
187 types::Scope::User => "",
188 types::Scope::System => " [system]",
189 types::Scope::Project => " [project]"
190 };
191 println!(
192 "{} {} v{}{scope_tag}",
193 "::".bold().blue(),
194 display_name.cyan(),
195 manifest.version.yellow()
196 );
197
198 for status in &statuses {
199 if !matches!(
200 status.status,
201 verify::VerifyStatus::Ok | verify::VerifyStatus::Unverified
202 ) {
203 println!(
204 " {}: {}",
205 status.status.as_tag().red().bold(),
206 status.path
207 );
208 }
209 }
210
211 if bad.is_empty() {
212 let unverified = statuses
213 .iter()
214 .filter(|s| s.status == verify::VerifyStatus::Unverified)
215 .count();
216 if unverified > 0 {
217 println!(
218 " {} ({unverified} file(s) unverified)",
219 "no problems detected".green()
220 );
221 } else {
222 println!("{}", "all files verified".green());
223 }
224 } else {
225 total_bad += bad.len();
226 }
227 }
228 }
229
230 if cmd.json {
231 println!("{}", serde_json::to_string_pretty(&json_reports)?);
232 } else if total_bad > 0 {
233 eprintln!(
234 "{}: {total_bad} file(s) failed verification",
235 "FAILED".red().bold()
236 );
237 std::process::exit(1);
238 }
239
240 Ok(())
241}