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