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