Skip to main content

pray_core/
registry.rs

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    /// Highest non-yanked version published in registry metadata, regardless of Prayfile constraint.
27    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}