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