Skip to main content

runmat_package/source/
registry_policy.rs

1use super::{SourceAcquisitionIntent, SourceAcquisitionPolicy, SourceLockAction};
2use crate::{CanonicalPackageId, RegistryId, RegistrySourceId};
3use semver::VersionReq;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6use url::Url;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct RegistryAcquisitionPlan {
11    pub source_registry: RegistryId,
12    pub index: String,
13    pub package: CanonicalPackageId,
14    pub requirement: VersionReq,
15    pub allow_network: bool,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub expected: Option<RegistrySourceId>,
18    pub lock_action: SourceLockAction,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct RegistryCandidatePlan {
24    pub source_registry: RegistryId,
25    pub index: String,
26    pub package: CanonicalPackageId,
27    pub allow_network: bool,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Error)]
31pub enum RegistryPolicyError {
32    #[error("registry index URL is invalid")]
33    InvalidIndex,
34    #[error("frozen mode cannot update registry sources")]
35    FrozenUpdate,
36    #[error("locked mode cannot update registry sources")]
37    LockedUpdate,
38    #[error("locked or frozen mode requires an exact registry release in runmat.lock")]
39    MissingLock,
40    #[error("locked registry release does not match the requested package")]
41    LockPackageMismatch,
42    #[error("locked registry release does not satisfy the manifest version requirement")]
43    LockVersionMismatch,
44    #[error("registry provider returned {found:?}; expected locked source {expected:?}")]
45    LockedSourceMismatch {
46        expected: Box<RegistrySourceId>,
47        found: Box<RegistrySourceId>,
48    },
49    #[error("registry provider returned a different package or incompatible version")]
50    AcquiredPackageMismatch,
51}
52
53pub fn plan_registry_acquisition(
54    source_registry: RegistryId,
55    index: &str,
56    package: CanonicalPackageId,
57    requirement: VersionReq,
58    locked_source: Option<&RegistrySourceId>,
59    intent: SourceAcquisitionIntent,
60    policy: SourceAcquisitionPolicy,
61) -> Result<RegistryAcquisitionPlan, RegistryPolicyError> {
62    let index = normalize_index(index)?;
63    if intent == SourceAcquisitionIntent::Update {
64        if policy.frozen {
65            return Err(RegistryPolicyError::FrozenUpdate);
66        }
67        if policy.locked {
68            return Err(RegistryPolicyError::LockedUpdate);
69        }
70    }
71    if let Some(locked) = locked_source {
72        if locked.package != package {
73            return Err(RegistryPolicyError::LockPackageMismatch);
74        }
75        if !requirement.matches(locked.version.as_semver()) {
76            return Err(RegistryPolicyError::LockVersionMismatch);
77        }
78    }
79    let use_locked = intent != SourceAcquisitionIntent::Update && locked_source.is_some();
80    if !use_locked && (policy.locked || policy.frozen) {
81        return Err(RegistryPolicyError::MissingLock);
82    }
83    Ok(RegistryAcquisitionPlan {
84        source_registry,
85        index,
86        package,
87        requirement,
88        allow_network: !policy.offline && !policy.frozen,
89        expected: use_locked.then(|| locked_source.cloned().expect("checked locked source")),
90        lock_action: if use_locked {
91            SourceLockAction::Preserve
92        } else {
93            match intent {
94                SourceAcquisitionIntent::Update => SourceLockAction::Replace,
95                SourceAcquisitionIntent::Execute | SourceAcquisitionIntent::Fetch => {
96                    SourceLockAction::Write
97                }
98            }
99        },
100    })
101}
102
103pub fn plan_registry_candidates(
104    source_registry: RegistryId,
105    index: &str,
106    package: CanonicalPackageId,
107    policy: SourceAcquisitionPolicy,
108) -> Result<RegistryCandidatePlan, RegistryPolicyError> {
109    Ok(RegistryCandidatePlan {
110        source_registry,
111        index: normalize_index(index)?,
112        package,
113        allow_network: !policy.offline && !policy.frozen,
114    })
115}
116
117pub fn plan_selected_registry_acquisition(
118    source_registry: RegistryId,
119    index: &str,
120    source: RegistrySourceId,
121    intent: SourceAcquisitionIntent,
122    policy: SourceAcquisitionPolicy,
123) -> Result<RegistryAcquisitionPlan, RegistryPolicyError> {
124    if policy.frozen && intent == SourceAcquisitionIntent::Update {
125        return Err(RegistryPolicyError::FrozenUpdate);
126    }
127    if policy.locked && intent == SourceAcquisitionIntent::Update {
128        return Err(RegistryPolicyError::LockedUpdate);
129    }
130    let requirement = VersionReq::parse(&format!("={}", source.version))
131        .expect("package versions always form exact requirements");
132    Ok(RegistryAcquisitionPlan {
133        source_registry,
134        index: normalize_index(index)?,
135        package: source.package.clone(),
136        requirement,
137        allow_network: !policy.offline && !policy.frozen,
138        expected: Some(source),
139        lock_action: match intent {
140            SourceAcquisitionIntent::Update => SourceLockAction::Replace,
141            SourceAcquisitionIntent::Execute | SourceAcquisitionIntent::Fetch => {
142                SourceLockAction::Write
143            }
144        },
145    })
146}
147
148pub fn validate_registry_acquisition(
149    plan: &RegistryAcquisitionPlan,
150    acquired: &RegistrySourceId,
151) -> Result<(), RegistryPolicyError> {
152    if acquired.package != plan.package || !plan.requirement.matches(acquired.version.as_semver()) {
153        return Err(RegistryPolicyError::AcquiredPackageMismatch);
154    }
155    if let Some(expected) = &plan.expected {
156        if acquired != expected {
157            return Err(RegistryPolicyError::LockedSourceMismatch {
158                expected: Box::new(expected.clone()),
159                found: Box::new(acquired.clone()),
160            });
161        }
162    }
163    Ok(())
164}
165
166fn normalize_index(value: &str) -> Result<String, RegistryPolicyError> {
167    let mut url = Url::parse(value).map_err(|_| RegistryPolicyError::InvalidIndex)?;
168    if url.scheme() != "https"
169        || !url.username().is_empty()
170        || url.password().is_some()
171        || url.query().is_some()
172        || url.fragment().is_some()
173    {
174        return Err(RegistryPolicyError::InvalidIndex);
175    }
176    url.set_query(None);
177    url.set_fragment(None);
178    Ok(url.to_string().trim_end_matches('/').to_string())
179}