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