harn_cli/package/registry/
verify.rs1use crate::package::*;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6pub(crate) struct RegistryVerificationReceipt {
7 pub schema_version: &'static str,
8 pub source: String,
9 pub index_version: u32,
10 pub package_count: usize,
11 pub version_count: usize,
12 pub remote_resolution: bool,
13 pub resolved_git_versions: usize,
14 pub ok: bool,
15 pub errors: Vec<String>,
16}
17
18fn failed_receipt(source: &str, remote: bool, error: impl ToString) -> RegistryVerificationReceipt {
19 RegistryVerificationReceipt {
20 schema_version: "harn.package_registry_verification.v1",
21 source: source.to_string(),
22 index_version: REGISTRY_INDEX_VERSION,
23 package_count: 0,
24 version_count: 0,
25 remote_resolution: remote,
26 resolved_git_versions: 0,
27 ok: false,
28 errors: vec![error.to_string()],
29 }
30}
31
32fn resolve_git_tag(git: &str, tag: &str) -> Result<String, PackageError> {
33 let output = git_output(
34 [
35 "ls-remote".to_string(),
36 "--exit-code".to_string(),
37 "--tags".to_string(),
38 git.to_string(),
39 format!("refs/tags/{tag}"),
40 format!("refs/tags/{tag}^{{}}"),
41 ],
42 Cwd::Detached,
43 )?;
44 if !output.status.success() {
45 return Err(format!(
46 "{git} tag {tag} does not resolve: {}",
47 String::from_utf8_lossy(&output.stderr).trim()
48 )
49 .into());
50 }
51 let stdout = String::from_utf8(output.stdout)
52 .map_err(|error| format!("git ls-remote returned non-UTF-8 output: {error}"))?;
53 let mut direct = None;
54 let mut peeled = None;
55 for line in stdout.lines() {
56 let mut fields = line.split_whitespace();
57 let Some(commit) = fields.next() else {
58 continue;
59 };
60 match fields.next() {
61 Some(reference) if reference.ends_with("^{}") => peeled = Some(commit.to_owned()),
62 Some(_) => direct = Some(commit.to_owned()),
63 None => {}
64 }
65 }
66 peeled
67 .or(direct)
68 .ok_or_else(|| format!("{git} tag {tag} did not return a commit").into())
69}
70
71pub(crate) fn verify_package_registry_impl(
72 source: &str,
73 remote: bool,
74) -> RegistryVerificationReceipt {
75 let content = match read_registry_source(source) {
76 Ok(content) => content,
77 Err(error) => return failed_receipt(source, remote, error),
78 };
79 let index = match parse_package_registry_index(source, &content) {
80 Ok(index) => index,
81 Err(error) => return failed_receipt(source, remote, error),
82 };
83 let package_count = index.packages.len();
84 let version_count = index
85 .packages
86 .iter()
87 .map(|package| package.versions.len())
88 .sum();
89 let mut errors = Vec::new();
90 let mut resolved_git_versions = 0;
91 if remote {
92 for package in &index.packages {
93 for version in &package.versions {
94 let (Some(git), Some(tag), Some(expected)) = (
95 version.git.as_deref(),
96 version.tag.as_deref(),
97 version.rev.as_deref(),
98 ) else {
99 continue;
100 };
101 match resolve_git_tag(git, tag) {
102 Ok(actual) if actual.eq_ignore_ascii_case(expected) => {
103 resolved_git_versions += 1;
104 }
105 Ok(actual) => errors.push(format!(
106 "{}@{} tag {} resolves to {}, expected {}",
107 package.name, version.version, tag, actual, expected
108 )),
109 Err(error) => errors.push(format!(
110 "{}@{} remote identity failed: {error}",
111 package.name, version.version
112 )),
113 }
114 }
115 }
116 }
117 RegistryVerificationReceipt {
118 schema_version: "harn.package_registry_verification.v1",
119 source: source.to_string(),
120 index_version: index.version,
121 package_count,
122 version_count,
123 remote_resolution: remote,
124 resolved_git_versions,
125 ok: errors.is_empty(),
126 errors,
127 }
128}
129
130pub fn verify_package_registry(source: &str, remote: bool, json: bool, receipt_out: Option<&Path>) {
131 let receipt = verify_package_registry_impl(source, remote);
132 let rendered = serde_json::to_string_pretty(&receipt)
133 .unwrap_or_else(|error| format!(r#"{{"ok":false,"errors":["{error}"]}}"#));
134 if let Some(path) = receipt_out {
135 if let Err(error) = fs::write(path, format!("{rendered}\n")) {
136 eprintln!(
137 "error: failed to write registry verification receipt {}: {error}",
138 path.display()
139 );
140 process::exit(1);
141 }
142 }
143 if json {
144 println!("{rendered}");
145 } else if receipt.ok {
146 println!(
147 "Verified registry v{}: {} packages, {} versions{}.",
148 receipt.index_version,
149 receipt.package_count,
150 receipt.version_count,
151 if remote {
152 format!(", {} Git tag identities", receipt.resolved_git_versions)
153 } else {
154 String::new()
155 }
156 );
157 } else {
158 for error in &receipt.errors {
159 eprintln!("error: {error}");
160 }
161 }
162 if !receipt.ok {
163 process::exit(1);
164 }
165}