1use crate::derived_metadata::RegistryDerivedMetadata;
2use crate::manifest::ManifestPackage;
3use crate::package_integrity::{artifact_content_digest, require_remote_integrity_fields};
4use crate::paths::remove_path_if_exists;
5use crate::registry_http::{http_get, http_post, join_url};
6use crate::registry_select::{apply_yank_policy, select_package_version};
7use crate::registry_ssh::submit_confession_ssh;
8use crate::resolve_context::PackageResolutionContext;
9use crate::{PrayError, PrayResult};
10use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::{Path, PathBuf};
13
14pub use crate::registry_select::{
15 highest_registry_version, set_package_version_yanked, version_is_greater_than,
16};
17pub use crate::registry_upload::{
18 publish_authorization_header, upload_registry_artifact,
19 upload_registry_artifact_with_authorization,
20};
21
22#[derive(Debug, Clone)]
23pub struct RegistryPackageResolution {
24 pub root: PathBuf,
25 pub signer_fingerprint: Option<String>,
26 pub registry_latest_version: Option<String>,
28}
29
30pub fn lockfile_signer_fingerprint(version: &RegistryPackageVersion) -> Option<String> {
31 version
32 .signer_fingerprint
33 .as_deref()
34 .filter(|value| crate::ssh_identity::looks_like_ssh_fingerprint(value))
35 .map(crate::ssh_identity::normalize_identity)
36}
37
38#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
39pub struct RegistryIndex {
40 pub spec: String,
41 #[serde(default)]
42 pub packages: Vec<String>,
43}
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
46pub struct RegistryPackageMetadata {
47 pub name: String,
48 #[serde(default)]
49 pub versions: Vec<RegistryPackageVersion>,
50}
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
53pub struct RegistryPackageVersion {
54 pub version: String,
55 pub artifact: String,
56 #[serde(default)]
57 pub artifact_hash: Option<String>,
58 #[serde(default)]
59 pub tree_hash: Option<String>,
60 #[serde(default)]
61 pub yanked: bool,
62 #[serde(default)]
63 pub targets: Vec<String>,
64 #[serde(default)]
65 pub exports: Vec<String>,
66 #[serde(default)]
67 pub signer: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub signer_fingerprint: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub signer_public_key: Option<String>,
72 #[serde(
73 default,
74 skip_serializing_if = "Option::is_none",
75 deserialize_with = "crate::registry_timestamp::deserialize_optional_publish_timestamp"
76 )]
77 pub published_at: Option<u64>,
78 #[serde(default)]
79 pub signature: Option<String>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub derived_metadata: Option<RegistryDerivedMetadata>,
82}
83
84impl RegistryPackageVersion {
85 pub fn same_identity(&self, other: &Self) -> bool {
86 self.version == other.version
87 && self.artifact == other.artifact
88 && self.artifact_hash == other.artifact_hash
89 && self.tree_hash == other.tree_hash
90 && self.yanked == other.yanked
91 && self.targets == other.targets
92 && self.exports == other.exports
93 && self.signer == other.signer
94 && self.signer_fingerprint == other.signer_fingerprint
95 && self.signer_public_key == other.signer_public_key
96 && self.signature == other.signature
97 }
98
99 pub fn merge_annotations_from(&mut self, other: &Self) {
100 if self.derived_metadata.is_none() {
101 self.derived_metadata = other.derived_metadata.clone();
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
107pub struct ConfessionSubmission {
108 pub package: String,
109 pub version: String,
110 pub status: String,
111 #[serde(default)]
112 pub note: Option<String>,
113 #[serde(default)]
114 pub lockfile: Option<String>,
115 #[serde(default)]
116 pub distribution_point: Option<String>,
117 #[serde(default)]
118 pub signer: Option<String>,
119 #[serde(default)]
120 pub timestamp: Option<String>,
121 #[serde(default)]
122 pub signature: Option<String>,
123}
124
125pub fn resolve_registry_package_root(
126 project_root: &Path,
127 source_url: &str,
128 declaration: &ManifestPackage,
129 context: &PackageResolutionContext,
130) -> PrayResult<RegistryPackageResolution> {
131 if let Some(resolved) = crate::registry_local::resolve_non_http_registry(
132 project_root,
133 source_url,
134 declaration,
135 context,
136 )? {
137 return Ok(resolved);
138 }
139
140 let metadata = fetch_registry_package_metadata(source_url, &declaration.name)?;
141 let registry_latest_version = registry_latest_version_label(&metadata);
142 let selected = select_package_version(
143 &metadata,
144 &declaration.constraint,
145 context.preferred_version.as_deref(),
146 )?;
147 apply_yank_policy(&declaration.name, &selected, context.fail_on_yanked)?;
148 let signer_fingerprint = lockfile_signer_fingerprint(&selected);
149 require_remote_integrity_fields(&declaration.name, &selected.version, &selected)?;
150 crate::client_trust::enforce_require_signed_packages(source_url, &declaration.name, &selected)?;
151 crate::client_trust::gate_pray_ssh_publisher_optional(
152 source_url,
153 signer_fingerprint.as_deref(),
154 )?;
155 if let Some(vendored_root) = crate::registry_cache::try_vendored_package_root(
156 project_root,
157 &declaration.name,
158 &selected,
159 )? {
160 return Ok(RegistryPackageResolution {
161 root: vendored_root,
162 signer_fingerprint,
163 registry_latest_version,
164 });
165 }
166 let cache_directory = crate::registry_cache::registry_cache_directory(
167 project_root,
168 source_url,
169 &declaration.name,
170 &selected.version,
171 )?;
172
173 if let Some(mut cached) = crate::registry_cache::try_reuse_cached_registry_package(
174 &cache_directory,
175 &selected,
176 signer_fingerprint.clone(),
177 )? {
178 cached.registry_latest_version = registry_latest_version.clone();
179 return Ok(cached);
180 }
181 if context.offline {
182 return Err(crate::registry_cache::offline_package_error(
183 &declaration.name,
184 &selected.version,
185 ));
186 }
187
188 crate::registry_cache::install_registry_artifact_to_cache(
189 &cache_directory,
190 source_url,
191 declaration,
192 &selected,
193 )?;
194
195 Ok(RegistryPackageResolution {
196 root: cache_directory,
197 signer_fingerprint,
198 registry_latest_version,
199 })
200}
201
202pub fn resolve_local_registry_package_root(
203 project_root: &Path,
204 source_key: &str,
205 source_root: &Path,
206 declaration: &ManifestPackage,
207 context: &PackageResolutionContext,
208) -> PrayResult<RegistryPackageResolution> {
209 let metadata_path = source_root.join(format!("v1/packages/{}.json", declaration.name));
210 let metadata_text = fs::read_to_string(&metadata_path).map_err(|error| {
211 if error.kind() == std::io::ErrorKind::NotFound {
212 PrayError::Resolution(format!(
213 "package {} not found in distribution {:?}. \
214 Missing {}. Check the package name, version constraint `{}`, and that the source publishes registry metadata.",
215 declaration.name,
216 source_root,
217 metadata_path.display(),
218 declaration.constraint
219 ))
220 } else {
221 PrayError::Resolution(format!(
222 "failed to read package metadata {}: {error}",
223 metadata_path.display()
224 ))
225 }
226 })?;
227 let metadata: RegistryPackageMetadata =
228 serde_json::from_str(&metadata_text).map_err(|error| PrayError::Parse {
229 kind: "registry metadata",
230 message: error.to_string(),
231 })?;
232 let registry_latest_version = registry_latest_version_label(&metadata);
233 let selected = select_package_version(
234 &metadata,
235 &declaration.constraint,
236 context.preferred_version.as_deref(),
237 )?;
238 apply_yank_policy(&declaration.name, &selected, context.fail_on_yanked)?;
239 let signer_fingerprint = lockfile_signer_fingerprint(&selected);
240 require_remote_integrity_fields(&declaration.name, &selected.version, &selected)?;
241 crate::client_trust::enforce_require_signed_packages(source_key, &declaration.name, &selected)?;
242 crate::client_trust::gate_pray_ssh_publisher_optional(
243 source_key,
244 signer_fingerprint.as_deref(),
245 )?;
246 if let Some(vendored_root) = crate::registry_cache::try_vendored_package_root(
247 project_root,
248 &declaration.name,
249 &selected,
250 )? {
251 return Ok(RegistryPackageResolution {
252 root: vendored_root,
253 signer_fingerprint,
254 registry_latest_version,
255 });
256 }
257 let cache_directory = crate::registry_cache::registry_cache_directory(
258 project_root,
259 source_key,
260 &declaration.name,
261 &selected.version,
262 )?;
263
264 if let Some(mut cached) = crate::registry_cache::try_reuse_cached_registry_package(
265 &cache_directory,
266 &selected,
267 signer_fingerprint.clone(),
268 )? {
269 cached.registry_latest_version = registry_latest_version.clone();
270 return Ok(cached);
271 }
272 if context.offline {
273 return Err(crate::registry_cache::offline_package_error(
274 &declaration.name,
275 &selected.version,
276 ));
277 }
278
279 if cache_directory.exists() {
280 remove_path_if_exists(&cache_directory)?;
281 }
282 fs::create_dir_all(&cache_directory)?;
283
284 let artifact_bytes =
285 crate::registry_cache::read_local_registry_artifact_bytes(source_root, &selected.artifact)?;
286 crate::registry_cache::validate_and_unpack_registry_package(
287 &cache_directory,
288 declaration,
289 &selected,
290 &artifact_bytes,
291 )?;
292
293 Ok(RegistryPackageResolution {
294 root: cache_directory,
295 signer_fingerprint,
296 registry_latest_version,
297 })
298}
299
300pub fn registry_package_signing_identity(version: &RegistryPackageVersion) -> Option<String> {
301 crate::ssh_identity::package_signing_identity(
302 version.signer.as_deref(),
303 version.signer_fingerprint.as_deref(),
304 )
305}
306
307pub fn registry_artifact_signature(artifact_bytes: &[u8], tree_hash: &str, signer: &str) -> String {
308 artifact_content_digest(artifact_bytes, tree_hash, signer)
309}
310
311pub fn submit_confession(source_url: &str, confession: &ConfessionSubmission) -> PrayResult<()> {
312 if crate::ssh_client::is_pray_ssh_url(source_url) {
313 return submit_confession_ssh(source_url, confession);
314 }
315 let endpoint = join_url(source_url, "v1/confessions");
316 let payload =
317 serde_json::to_vec(confession).map_err(|error| PrayError::Manifest(error.to_string()))?;
318 let response = http_post(&endpoint, "application/json", &payload)?;
319 if response.status / 100 != 2 {
320 return Err(PrayError::Resolution(format!(
321 "confession submission failed with HTTP {}",
322 response.status
323 )));
324 }
325 Ok(())
326}
327
328fn fetch_registry_package_metadata(
329 source_url: &str,
330 package_name: &str,
331) -> PrayResult<RegistryPackageMetadata> {
332 let url = join_url(source_url, &format!("v1/packages/{}.json", package_name));
333 let response = http_get(&url)?;
334 serde_json::from_slice(&response).map_err(|error| PrayError::Parse {
335 kind: "registry metadata",
336 message: error.to_string(),
337 })
338}
339
340pub fn registry_latest_version_label(metadata: &RegistryPackageMetadata) -> Option<String> {
341 highest_registry_version(metadata)
342 .ok()
343 .flatten()
344 .map(|version| version.version)
345}
346
347#[doc(hidden)]
348pub fn select_package_version_for_test(
349 metadata: &RegistryPackageMetadata,
350 constraint: &str,
351 preferred_version: Option<&str>,
352) -> PrayResult<RegistryPackageVersion> {
353 select_package_version(metadata, constraint, preferred_version)
354}
355
356pub fn fetch_optional_distribution_bytes(
357 source_url: &str,
358 relative_path: &str,
359) -> PrayResult<Option<Vec<u8>>> {
360 if !source_url.starts_with("http://") && !source_url.starts_with("https://") {
361 return Err(PrayError::Unsupported(format!(
362 "distribution fetch requires http or https source, got {source_url}"
363 )));
364 }
365 let url = join_url(source_url, relative_path);
366 match http_get(&url) {
367 Ok(bytes) => Ok(Some(bytes)),
368 Err(PrayError::Resolution(message)) if message.contains("404") => Ok(None),
369 Err(error) => Err(error),
370 }
371}