1use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use thiserror::Error;
8
9use crate::{
10 PluginArtifactTransport, PluginCatalog, PluginCatalogError, PluginCatalogRecord,
11 PluginProvision, PluginRequirement, PluginResolution, PluginResolutionError, PluginResolver,
12 PluginResolverPolicy, validate_plugin_provisions, validate_plugin_requirements,
13};
14
15pub const PLUGIN_PLAN_SCHEMA_VERSION: u32 = 1;
17pub const MAX_PLUGIN_PLAN_PROVIDERS: usize = crate::MAX_PLUGIN_RESOLUTION_CONSTRAINTS;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22pub struct PluginPlanPolicy {
23 transport: PluginArtifactTransport,
24 target: String,
25 architecture: String,
26 abi_major: u16,
27 abi_minor: u16,
28 plugin_priorities: BTreeMap<String, i32>,
29}
30
31impl PluginPlanPolicy {
32 pub const fn transport(&self) -> PluginArtifactTransport {
33 self.transport
34 }
35
36 pub fn target(&self) -> &str {
37 &self.target
38 }
39
40 pub fn architecture(&self) -> &str {
41 &self.architecture
42 }
43
44 pub const fn abi_major(&self) -> u16 {
45 self.abi_major
46 }
47
48 pub const fn abi_minor(&self) -> u16 {
49 self.abi_minor
50 }
51
52 pub fn plugin_priorities(&self) -> &BTreeMap<String, i32> {
53 &self.plugin_priorities
54 }
55
56 fn from_resolver_policy(policy: &PluginResolverPolicy) -> Self {
57 Self {
58 transport: policy.transport(),
59 target: policy.target().to_owned(),
60 architecture: policy.architecture().to_owned(),
61 abi_major: policy.abi_major(),
62 abi_minor: policy.abi_minor(),
63 plugin_priorities: policy.plugin_priorities().clone(),
64 }
65 }
66
67 fn to_resolver_policy(&self) -> Result<PluginResolverPolicy, PluginPlanError> {
68 let mut policy = PluginResolverPolicy::new(
69 self.transport,
70 self.target.clone(),
71 self.architecture.clone(),
72 self.abi_major,
73 self.abi_minor,
74 )?;
75 for (plugin_id, priority) in &self.plugin_priorities {
76 policy.set_plugin_priority(plugin_id.clone(), *priority)?;
77 }
78 Ok(policy)
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
84pub struct PluginPlanProvider {
85 service: String,
86 provided_version: String,
87 artifact_identity: String,
88}
89
90impl PluginPlanProvider {
91 pub fn service(&self) -> &str {
92 &self.service
93 }
94
95 pub fn provided_version(&self) -> &str {
96 &self.provided_version
97 }
98
99 pub fn artifact_identity(&self) -> &str {
100 &self.artifact_identity
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
106pub struct PluginPlan {
107 schema_version: u32,
108 catalog_fingerprint: String,
109 catalog: Vec<PluginCatalogRecord>,
110 policy: PluginPlanPolicy,
111 requirements: Vec<PluginRequirement>,
112 providers: Vec<PluginPlanProvider>,
113 artifacts: Vec<PluginCatalogRecord>,
114 fingerprint: String,
115}
116
117impl PluginPlan {
118 pub const fn schema_version(&self) -> u32 {
119 self.schema_version
120 }
121
122 pub fn catalog_fingerprint(&self) -> &str {
123 &self.catalog_fingerprint
124 }
125
126 pub fn catalog(&self) -> &[PluginCatalogRecord] {
127 &self.catalog
128 }
129
130 pub fn policy(&self) -> &PluginPlanPolicy {
131 &self.policy
132 }
133
134 pub fn requirements(&self) -> &[PluginRequirement] {
135 &self.requirements
136 }
137
138 pub fn providers(&self) -> &[PluginPlanProvider] {
139 &self.providers
140 }
141
142 pub fn artifacts(&self) -> &[PluginCatalogRecord] {
144 &self.artifacts
145 }
146
147 pub fn fingerprint(&self) -> &str {
148 &self.fingerprint
149 }
150
151 pub fn to_json(&self) -> Result<Vec<u8>, PluginPlanError> {
153 serde_json::to_vec(self).map_err(|error| PluginPlanError::Json {
154 message: error.to_string(),
155 })
156 }
157
158 pub fn from_json(bytes: &[u8]) -> Result<Self, PluginPlanError> {
160 let wire = serde_json::from_slice::<PluginPlanWire>(bytes).map_err(|error| {
161 PluginPlanError::Json {
162 message: error.to_string(),
163 }
164 })?;
165 Self::from_wire(wire)
166 }
167
168 pub(crate) fn from_resolution(
169 policy: &PluginResolverPolicy,
170 requirements: &[PluginRequirement],
171 catalog: &PluginCatalog,
172 resolution: PluginResolution,
173 ) -> Result<Self, PluginPlanError> {
174 let requirements = canonical_requirements(requirements)?;
175 let providers = providers_from_resolution(&resolution);
176 let mut plan = Self {
177 schema_version: PLUGIN_PLAN_SCHEMA_VERSION,
178 catalog_fingerprint: resolution.catalog_fingerprint().to_owned(),
179 catalog: catalog.records().to_vec(),
180 policy: PluginPlanPolicy::from_resolver_policy(policy),
181 requirements,
182 providers,
183 artifacts: resolution.artifacts().to_vec(),
184 fingerprint: String::new(),
185 };
186 plan.fingerprint = plan.compute_fingerprint()?;
187 Ok(plan)
188 }
189
190 fn from_wire(wire: PluginPlanWire) -> Result<Self, PluginPlanError> {
191 if wire.schema_version != PLUGIN_PLAN_SCHEMA_VERSION {
192 return Err(PluginPlanError::UnsupportedSchemaVersion {
193 expected: PLUGIN_PLAN_SCHEMA_VERSION,
194 actual: wire.schema_version,
195 });
196 }
197 if !is_lowercase_sha256(&wire.catalog_fingerprint) {
198 return Err(PluginPlanError::InvalidFingerprint {
199 field: "catalog_fingerprint".to_owned(),
200 });
201 }
202 if !is_lowercase_sha256(&wire.fingerprint) {
203 return Err(PluginPlanError::InvalidFingerprint {
204 field: "fingerprint".to_owned(),
205 });
206 }
207
208 let policy = PluginPlanPolicy {
209 transport: wire.policy.transport,
210 target: wire.policy.target,
211 architecture: wire.policy.architecture,
212 abi_major: wire.policy.abi_major,
213 abi_minor: wire.policy.abi_minor,
214 plugin_priorities: wire.policy.plugin_priorities,
215 };
216 policy.to_resolver_policy()?;
217
218 let requirements = canonical_requirements(&wire.requirements)?;
219 if requirements != wire.requirements {
220 return Err(PluginPlanError::NonCanonical {
221 field: "requirements".to_owned(),
222 });
223 }
224
225 let providers = wire
226 .providers
227 .into_iter()
228 .map(|provider| PluginPlanProvider {
229 service: provider.service,
230 provided_version: provider.provided_version,
231 artifact_identity: provider.artifact_identity,
232 })
233 .collect::<Vec<_>>();
234 validate_providers(&providers)?;
235 let mut canonical_providers = providers.clone();
236 canonical_providers.sort();
237 if canonical_providers != providers {
238 return Err(PluginPlanError::NonCanonical {
239 field: "providers".to_owned(),
240 });
241 }
242
243 let plan = Self {
244 schema_version: wire.schema_version,
245 catalog_fingerprint: wire.catalog_fingerprint,
246 catalog: wire.catalog,
247 policy,
248 requirements,
249 providers,
250 artifacts: wire.artifacts,
251 fingerprint: wire.fingerprint,
252 };
253 let canonical_catalog = PluginCatalog::from_records(plan.catalog.clone())?;
254 if canonical_catalog.records() != plan.catalog {
255 return Err(PluginPlanError::NonCanonical {
256 field: "catalog".to_owned(),
257 });
258 }
259 let actual_catalog_fingerprint = catalog_fingerprint_for_records(&plan.catalog)?;
260 if actual_catalog_fingerprint != plan.catalog_fingerprint {
261 return Err(PluginPlanError::CatalogFingerprintMismatch {
262 expected: actual_catalog_fingerprint,
263 actual: plan.catalog_fingerprint,
264 });
265 }
266 plan.validate_resolution_projection()?;
267 let expected = plan.compute_fingerprint()?;
268 if expected != plan.fingerprint {
269 return Err(PluginPlanError::FingerprintMismatch {
270 expected,
271 actual: plan.fingerprint,
272 });
273 }
274 Ok(plan)
275 }
276
277 fn validate_resolution_projection(&self) -> Result<(), PluginPlanError> {
278 let catalog = PluginCatalog::from_records(self.catalog.clone())?;
279 let policy = self.policy.to_resolver_policy()?;
280 let resolution = PluginResolver::new(&catalog, policy).resolve(&self.requirements)?;
281 if providers_from_resolution(&resolution) != self.providers {
282 return Err(PluginPlanError::ProjectionMismatch {
283 field: "providers".to_owned(),
284 });
285 }
286 if resolution.artifacts() != self.artifacts {
287 let mut expected = resolution.artifacts().to_vec();
288 let mut actual = self.artifacts.clone();
289 expected.sort_by_key(PluginCatalogRecord::canonical_identity_key);
290 actual.sort_by_key(PluginCatalogRecord::canonical_identity_key);
291 return Err(if expected == actual {
292 PluginPlanError::NonCanonical {
293 field: "artifacts".to_owned(),
294 }
295 } else {
296 PluginPlanError::ProjectionMismatch {
297 field: "artifacts".to_owned(),
298 }
299 });
300 }
301 Ok(())
302 }
303
304 fn compute_fingerprint(&self) -> Result<String, PluginPlanError> {
305 let payload = PluginPlanFingerprintPayload {
306 schema_version: self.schema_version,
307 catalog_fingerprint: &self.catalog_fingerprint,
308 catalog: &self.catalog,
309 policy: &self.policy,
310 requirements: &self.requirements,
311 providers: &self.providers,
312 artifacts: &self.artifacts,
313 };
314 let bytes = serde_json::to_vec(&payload).map_err(|error| PluginPlanError::Json {
315 message: error.to_string(),
316 })?;
317 Ok(hex::encode(Sha256::digest(bytes)))
318 }
319}
320
321#[derive(Debug, Error, Clone, PartialEq, Eq)]
322pub enum PluginPlanError {
323 #[error("failed to decode or encode plugin plan JSON: {message}")]
324 Json { message: String },
325 #[error("unsupported plugin plan schema version {actual}; expected {expected}")]
326 UnsupportedSchemaVersion { expected: u32, actual: u32 },
327 #[error("plugin plan field `{field}` is not canonical lowercase SHA-256")]
328 InvalidFingerprint { field: String },
329 #[error("plugin plan field `{field}` is not in canonical order")]
330 NonCanonical { field: String },
331 #[error("plugin plan field `{field}` does not match the resolver projection")]
332 ProjectionMismatch { field: String },
333 #[error("invalid plugin plan requirements: {message}")]
334 InvalidRequirements { message: String },
335 #[error("plugin plan contains more than {limit} selected providers")]
336 TooManyProviders { limit: usize },
337 #[error("plugin plan contains duplicate provider service `{service}`")]
338 DuplicateProvider { service: String },
339 #[error("invalid plugin plan provider `{service}`: {message}")]
340 InvalidProvider { service: String, message: String },
341 #[error("plugin plan fingerprint mismatch: expected {expected}, got {actual}")]
342 FingerprintMismatch { expected: String, actual: String },
343 #[error("plugin plan catalog fingerprint mismatch: expected {expected}, got {actual}")]
344 CatalogFingerprintMismatch { expected: String, actual: String },
345 #[error(transparent)]
346 Catalog(#[from] PluginCatalogError),
347 #[error(transparent)]
348 Resolution(#[from] PluginResolutionError),
349}
350
351#[derive(Debug, Deserialize)]
352#[serde(deny_unknown_fields)]
353struct PluginPlanWire {
354 schema_version: u32,
355 catalog_fingerprint: String,
356 catalog: Vec<PluginCatalogRecord>,
357 policy: PluginPlanPolicyWire,
358 requirements: Vec<PluginRequirement>,
359 providers: Vec<PluginPlanProviderWire>,
360 artifacts: Vec<PluginCatalogRecord>,
361 fingerprint: String,
362}
363
364#[derive(Debug, Deserialize)]
365#[serde(deny_unknown_fields)]
366struct PluginPlanPolicyWire {
367 transport: PluginArtifactTransport,
368 target: String,
369 architecture: String,
370 abi_major: u16,
371 abi_minor: u16,
372 plugin_priorities: BTreeMap<String, i32>,
373}
374
375#[derive(Debug, Deserialize)]
376#[serde(deny_unknown_fields)]
377struct PluginPlanProviderWire {
378 service: String,
379 provided_version: String,
380 artifact_identity: String,
381}
382
383#[derive(Serialize)]
384struct PluginPlanFingerprintPayload<'a> {
385 schema_version: u32,
386 catalog_fingerprint: &'a str,
387 catalog: &'a [PluginCatalogRecord],
388 policy: &'a PluginPlanPolicy,
389 requirements: &'a [PluginRequirement],
390 providers: &'a [PluginPlanProvider],
391 artifacts: &'a [PluginCatalogRecord],
392}
393
394fn canonical_requirements(
395 requirements: &[PluginRequirement],
396) -> Result<Vec<PluginRequirement>, PluginPlanError> {
397 validate_plugin_requirements(requirements).map_err(|error| {
398 PluginPlanError::InvalidRequirements {
399 message: error.to_string(),
400 }
401 })?;
402 let mut requirements = requirements.to_vec();
403 requirements.sort();
404 Ok(requirements)
405}
406
407fn providers_from_resolution(resolution: &PluginResolution) -> Vec<PluginPlanProvider> {
408 resolution
409 .providers()
410 .iter()
411 .map(|provider| PluginPlanProvider {
412 service: provider.service().to_owned(),
413 provided_version: provider.provided_version().to_owned(),
414 artifact_identity: provider.artifact().canonical_identity_key(),
415 })
416 .collect()
417}
418
419fn catalog_fingerprint_for_records(
420 records: &[PluginCatalogRecord],
421) -> Result<String, PluginPlanError> {
422 Ok(PluginCatalog::from_records(records.to_vec())?
423 .fingerprint()
424 .to_owned())
425}
426
427fn validate_providers(providers: &[PluginPlanProvider]) -> Result<(), PluginPlanError> {
428 if providers.len() > MAX_PLUGIN_PLAN_PROVIDERS {
429 return Err(PluginPlanError::TooManyProviders {
430 limit: MAX_PLUGIN_PLAN_PROVIDERS,
431 });
432 }
433 let mut services = BTreeSet::new();
434 for provider in providers {
435 let provision = PluginProvision {
436 service: provider.service.clone(),
437 version: provider.provided_version.clone(),
438 };
439 validate_plugin_provisions(&[provision]).map_err(|error| {
440 PluginPlanError::InvalidProvider {
441 service: provider.service.clone(),
442 message: error.to_string(),
443 }
444 })?;
445 if !services.insert(provider.service.clone()) {
446 return Err(PluginPlanError::DuplicateProvider {
447 service: provider.service.clone(),
448 });
449 }
450 if provider.artifact_identity.is_empty() {
451 return Err(PluginPlanError::InvalidProvider {
452 service: provider.service.clone(),
453 message: "artifact identity must not be empty".to_owned(),
454 });
455 }
456 }
457 Ok(())
458}
459
460fn is_lowercase_sha256(value: &str) -> bool {
461 value.len() == 64
462 && value
463 .bytes()
464 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
465}