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}