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::{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    /// 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(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}