1use std::cmp::Ordering;
4use std::collections::{BTreeMap, BTreeSet};
5
6use semver::{Version, VersionReq};
7use thiserror::Error;
8
9use crate::{
10 MAX_PLUGIN_ARCHITECTURE_BYTES, MAX_PLUGIN_CATALOG_RECORDS, MAX_PLUGIN_REQUIREMENTS,
11 MAX_PLUGIN_TARGET_BYTES, PluginArtifactTransport, PluginCatalog, PluginCatalogRecord,
12 PluginPlan, PluginPlanError, PluginRequirement, PluginTransport, validate_plugin_requirements,
13};
14
15pub const MAX_PLUGIN_RESOLUTION_CONSTRAINTS: usize =
17 MAX_PLUGIN_CATALOG_RECORDS * MAX_PLUGIN_REQUIREMENTS;
18pub const MAX_PLUGIN_RESOLUTION_STATES: usize = MAX_PLUGIN_CATALOG_RECORDS * 4;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct PluginResolverPolicy {
24 transport: PluginArtifactTransport,
25 target: String,
26 architecture: String,
27 abi_major: u16,
28 abi_minor: u16,
29 plugin_priorities: BTreeMap<String, i32>,
30}
31
32impl PluginResolverPolicy {
33 pub fn new(
34 transport: PluginArtifactTransport,
35 target: impl Into<String>,
36 architecture: impl Into<String>,
37 abi_major: u16,
38 abi_minor: u16,
39 ) -> Result<Self, PluginResolutionError> {
40 let target = target.into();
41 let architecture = architecture.into();
42 validate_policy_text("target", &target, MAX_PLUGIN_TARGET_BYTES)?;
43 validate_policy_text("architecture", &architecture, MAX_PLUGIN_ARCHITECTURE_BYTES)?;
44 if abi_major == 0 {
45 return Err(PluginResolutionError::InvalidPolicy {
46 field: "abi_major".to_owned(),
47 message: "must be greater than zero".to_owned(),
48 });
49 }
50 Ok(Self {
51 transport,
52 target,
53 architecture,
54 abi_major,
55 abi_minor,
56 plugin_priorities: BTreeMap::new(),
57 })
58 }
59
60 pub const fn transport(&self) -> PluginArtifactTransport {
61 self.transport
62 }
63
64 pub fn target(&self) -> &str {
65 &self.target
66 }
67
68 pub fn architecture(&self) -> &str {
69 &self.architecture
70 }
71
72 pub const fn abi_major(&self) -> u16 {
73 self.abi_major
74 }
75
76 pub const fn abi_minor(&self) -> u16 {
77 self.abi_minor
78 }
79
80 pub fn set_plugin_priority(
81 &mut self,
82 plugin_id: impl Into<String>,
83 priority: i32,
84 ) -> Result<(), PluginResolutionError> {
85 let plugin_id = plugin_id.into();
86 let transport = match self.transport {
87 PluginArtifactTransport::Native => PluginTransport::Native,
88 PluginArtifactTransport::Wasm => PluginTransport::Wasm,
89 };
90 crate::PluginReference::new(plugin_id.clone(), None, transport).map_err(|error| {
91 PluginResolutionError::InvalidPolicy {
92 field: "plugin_priorities.plugin_id".to_owned(),
93 message: error.to_string(),
94 }
95 })?;
96 if !self.plugin_priorities.contains_key(&plugin_id)
97 && self.plugin_priorities.len() >= MAX_PLUGIN_CATALOG_RECORDS
98 {
99 return Err(PluginResolutionError::InvalidPolicy {
100 field: "plugin_priorities".to_owned(),
101 message: format!("must contain at most {MAX_PLUGIN_CATALOG_RECORDS} entries"),
102 });
103 }
104 self.plugin_priorities.insert(plugin_id, priority);
105 Ok(())
106 }
107
108 pub fn plugin_priority(&self, plugin_id: &str) -> i32 {
109 self.plugin_priorities
110 .get(plugin_id)
111 .copied()
112 .unwrap_or_default()
113 }
114
115 pub fn plugin_priorities(&self) -> &BTreeMap<String, i32> {
116 &self.plugin_priorities
117 }
118
119 fn accepts(&self, record: &PluginCatalogRecord) -> bool {
120 let descriptor = record.descriptor();
121 descriptor.transport == self.transport
122 && descriptor.target == self.target
123 && descriptor.architecture == self.architecture
124 && descriptor.abi_major == self.abi_major
125 && descriptor.abi_minor_min <= self.abi_minor
126 && descriptor.abi_minor_max >= self.abi_minor
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct PluginResolvedProvider {
133 service: String,
134 provided_version: String,
135 artifact: PluginCatalogRecord,
136}
137
138impl PluginResolvedProvider {
139 pub fn service(&self) -> &str {
140 &self.service
141 }
142
143 pub fn provided_version(&self) -> &str {
144 &self.provided_version
145 }
146
147 pub fn artifact(&self) -> &PluginCatalogRecord {
148 &self.artifact
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct PluginResolution {
155 catalog_fingerprint: String,
156 providers: Vec<PluginResolvedProvider>,
157 artifacts: Vec<PluginCatalogRecord>,
158}
159
160impl PluginResolution {
161 pub fn catalog_fingerprint(&self) -> &str {
162 &self.catalog_fingerprint
163 }
164
165 pub fn providers(&self) -> &[PluginResolvedProvider] {
166 &self.providers
167 }
168
169 pub fn artifacts(&self) -> &[PluginCatalogRecord] {
170 &self.artifacts
171 }
172}
173
174#[derive(Debug, Error, Clone, PartialEq, Eq)]
175pub enum PluginResolutionError {
176 #[error("invalid plugin resolver policy field `{field}`: {message}")]
177 InvalidPolicy { field: String, message: String },
178 #[error("invalid root plugin requirements: {message}")]
179 InvalidRequirements { message: String },
180 #[error("catalog invariant failed for `{artifact_identity}`: {message}")]
181 InvalidCatalogRecord {
182 artifact_identity: String,
183 message: String,
184 },
185 #[error(
186 "no provider for service `{service}` matches the host policy; requirements={requirements:?}, catalog_candidates={catalog_candidates}, policy_candidates={policy_candidates}"
187 )]
188 MissingProvider {
189 service: String,
190 requirements: Vec<String>,
191 catalog_candidates: usize,
192 policy_candidates: usize,
193 },
194 #[error(
195 "no version of service `{service}` satisfies {requirements:?}; available versions={available_versions:?}"
196 )]
197 VersionConflict {
198 service: String,
199 requirements: Vec<String>,
200 available_versions: Vec<String>,
201 },
202 #[error(
203 "plugin `{plugin_id}` would resolve to conflicting artifacts `{selected_identity}` and `{candidate_identity}`"
204 )]
205 PluginIdentityConflict {
206 plugin_id: String,
207 selected_identity: String,
208 candidate_identity: String,
209 },
210 #[error("plugin dependency cycle: {artifact_identities:?}")]
211 DependencyCycle { artifact_identities: Vec<String> },
212 #[error("plugin resolution exceeded {limit} accumulated constraints")]
213 ConstraintLimitExceeded { limit: usize },
214 #[error("plugin resolution exceeded {limit} deterministic search states")]
215 SearchLimitExceeded { limit: usize },
216}
217
218pub struct PluginResolver<'a> {
220 catalog: &'a PluginCatalog,
221 policy: PluginResolverPolicy,
222}
223
224impl<'a> PluginResolver<'a> {
225 pub fn new(catalog: &'a PluginCatalog, policy: PluginResolverPolicy) -> Self {
226 Self { catalog, policy }
227 }
228
229 pub fn policy(&self) -> &PluginResolverPolicy {
230 &self.policy
231 }
232
233 pub fn catalog(&self) -> &PluginCatalog {
234 self.catalog
235 }
236
237 pub fn resolve(
238 &self,
239 root_requirements: &[PluginRequirement],
240 ) -> Result<PluginResolution, PluginResolutionError> {
241 validate_plugin_requirements(root_requirements).map_err(|error| {
242 PluginResolutionError::InvalidRequirements {
243 message: error.to_string(),
244 }
245 })?;
246
247 let mut initial = ResolverState::default();
248 for requirement in root_requirements {
249 initial.add_constraint(requirement, RequirementOwner::Host)?;
250 }
251 let mut pending = vec![initial];
252 let mut examined = 0_usize;
253 let mut first_failure = None;
254
255 while let Some(state) = pending.pop() {
256 if examined >= MAX_PLUGIN_RESOLUTION_STATES {
257 return Err(PluginResolutionError::SearchLimitExceeded {
258 limit: MAX_PLUGIN_RESOLUTION_STATES,
259 });
260 }
261 examined += 1;
262 let Some(service) = state.next_unresolved_service() else {
263 match self.finish(state) {
264 Ok(resolution) => return Ok(resolution),
265 Err(error) => {
266 remember_first(&mut first_failure, error);
267 continue;
268 }
269 }
270 };
271
272 let candidates = match self.candidates_for(&state, &service) {
273 Ok(candidates) => candidates,
274 Err(error) => {
275 remember_first(&mut first_failure, error);
276 continue;
277 }
278 };
279 let mut next_states = Vec::with_capacity(candidates.len());
280 let mut candidate_failure = None;
281 for candidate in candidates {
282 let mut next = state.clone();
283 match self.apply_candidate(&mut next, &service, candidate) {
284 Ok(()) => next_states.push(next),
285 Err(error) => remember_first(&mut candidate_failure, error),
286 }
287 }
288 if next_states.is_empty()
289 && let Some(error) = candidate_failure
290 {
291 remember_first(&mut first_failure, error);
292 }
293 for next in next_states.into_iter().rev() {
294 if pending.len() + examined >= MAX_PLUGIN_RESOLUTION_STATES {
295 return Err(PluginResolutionError::SearchLimitExceeded {
296 limit: MAX_PLUGIN_RESOLUTION_STATES,
297 });
298 }
299 pending.push(next);
300 }
301 }
302
303 Err(
304 first_failure.unwrap_or_else(|| PluginResolutionError::InvalidRequirements {
305 message: "resolution ended without a result or typed provider failure".to_owned(),
306 }),
307 )
308 }
309
310 pub fn resolve_plan(
312 &self,
313 root_requirements: &[PluginRequirement],
314 ) -> Result<PluginPlan, PluginPlanError> {
315 let resolution = self.resolve(root_requirements)?;
316 PluginPlan::from_resolution(&self.policy, root_requirements, self.catalog, resolution)
317 }
318
319 fn candidates_for(
320 &self,
321 state: &ResolverState,
322 service: &str,
323 ) -> Result<Vec<Candidate>, PluginResolutionError> {
324 let constraints = state.requirements.get(service).ok_or_else(|| {
325 PluginResolutionError::InvalidRequirements {
326 message: format!("unresolved service `{service}` has no constraints"),
327 }
328 })?;
329 let mut catalog_candidates = 0_usize;
330 let mut policy_candidates = 0_usize;
331 let mut available_versions = Vec::new();
332 let mut candidates = Vec::new();
333
334 for (record_index, record) in self.catalog.records().iter().enumerate() {
335 let plugin_version = Version::parse(&record.descriptor().version).map_err(|error| {
336 PluginResolutionError::InvalidCatalogRecord {
337 artifact_identity: record.canonical_identity_key(),
338 message: format!("invalid plugin version: {error}"),
339 }
340 })?;
341 for (provision_index, provision) in record.descriptor().provides.iter().enumerate() {
342 if provision.service != service {
343 continue;
344 }
345 catalog_candidates += 1;
346 if !self.policy.accepts(record) {
347 continue;
348 }
349 policy_candidates += 1;
350 let service_version = Version::parse(&provision.version).map_err(|error| {
351 PluginResolutionError::InvalidCatalogRecord {
352 artifact_identity: record.canonical_identity_key(),
353 message: format!("invalid provided service version: {error}"),
354 }
355 })?;
356 available_versions.push((service_version.clone(), provision.version.clone()));
357 if constraints
358 .iter()
359 .all(|constraint| constraint.parsed.matches(&service_version))
360 {
361 candidates.push(Candidate {
362 record_index,
363 provision_index,
364 service_version,
365 plugin_version: plugin_version.clone(),
366 priority: self.policy.plugin_priority(&record.descriptor().plugin_id),
367 plugin_id: record.descriptor().plugin_id.clone(),
368 artifact_identity: record.canonical_identity_key(),
369 });
370 }
371 }
372 }
373
374 if catalog_candidates == 0 || policy_candidates == 0 {
375 return Err(PluginResolutionError::MissingProvider {
376 service: service.to_owned(),
377 requirements: describe_constraints(constraints),
378 catalog_candidates,
379 policy_candidates,
380 });
381 }
382 if candidates.is_empty() {
383 available_versions.sort();
384 available_versions.dedup_by(|left, right| left.1 == right.1);
385 return Err(PluginResolutionError::VersionConflict {
386 service: service.to_owned(),
387 requirements: describe_constraints(constraints),
388 available_versions: available_versions
389 .into_iter()
390 .map(|(_, version)| version)
391 .collect(),
392 });
393 }
394 candidates.sort_by(candidate_order);
395 Ok(candidates)
396 }
397
398 fn apply_candidate(
399 &self,
400 state: &mut ResolverState,
401 service: &str,
402 candidate: Candidate,
403 ) -> Result<(), PluginResolutionError> {
404 let record = self
405 .catalog
406 .records()
407 .get(candidate.record_index)
408 .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
409 artifact_identity: candidate.artifact_identity.clone(),
410 message: "candidate record index is outside the catalog".to_owned(),
411 })?;
412 let artifact_identity = record.canonical_identity_key();
413 if let Some(selected_identity) = state.plugin_artifacts.get(&record.descriptor().plugin_id)
414 && selected_identity != &artifact_identity
415 {
416 return Err(PluginResolutionError::PluginIdentityConflict {
417 plugin_id: record.descriptor().plugin_id.clone(),
418 selected_identity: selected_identity.clone(),
419 candidate_identity: artifact_identity,
420 });
421 }
422
423 state.selected_services.insert(
424 service.to_owned(),
425 SelectedProvider {
426 record_index: candidate.record_index,
427 provision_index: candidate.provision_index,
428 },
429 );
430 if state
431 .selected_artifacts
432 .insert(artifact_identity.clone(), candidate.record_index)
433 .is_none()
434 {
435 state.plugin_artifacts.insert(
436 record.descriptor().plugin_id.clone(),
437 artifact_identity.clone(),
438 );
439 for requirement in &record.descriptor().requires {
440 state.add_constraint(
441 requirement,
442 RequirementOwner::Artifact(artifact_identity.clone()),
443 )?;
444 if let Some(selected) = state.selected_services.get(&requirement.service) {
445 self.validate_selected_service(state, &requirement.service, *selected)?;
446 }
447 }
448 }
449 Ok(())
450 }
451
452 fn validate_selected_service(
453 &self,
454 state: &ResolverState,
455 service: &str,
456 selected: SelectedProvider,
457 ) -> Result<(), PluginResolutionError> {
458 let record = self
459 .catalog
460 .records()
461 .get(selected.record_index)
462 .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
463 artifact_identity: format!("record-index:{}", selected.record_index),
464 message: "selected provider index is outside the catalog".to_owned(),
465 })?;
466 let provision = record
467 .descriptor()
468 .provides
469 .get(selected.provision_index)
470 .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
471 artifact_identity: record.canonical_identity_key(),
472 message: "selected provision index is outside the descriptor".to_owned(),
473 })?;
474 let version = Version::parse(&provision.version).map_err(|error| {
475 PluginResolutionError::InvalidCatalogRecord {
476 artifact_identity: record.canonical_identity_key(),
477 message: format!("invalid selected service version: {error}"),
478 }
479 })?;
480 let constraints = state.requirements.get(service).ok_or_else(|| {
481 PluginResolutionError::InvalidRequirements {
482 message: format!("selected service `{service}` has no constraints"),
483 }
484 })?;
485 if constraints
486 .iter()
487 .all(|constraint| constraint.parsed.matches(&version))
488 {
489 return Ok(());
490 }
491 Err(PluginResolutionError::VersionConflict {
492 service: service.to_owned(),
493 requirements: describe_constraints(constraints),
494 available_versions: vec![provision.version.clone()],
495 })
496 }
497
498 fn finish(&self, state: ResolverState) -> Result<PluginResolution, PluginResolutionError> {
499 let artifact_order = self.topological_order(&state)?;
500 let providers = state
501 .selected_services
502 .iter()
503 .map(|(service, selected)| {
504 let record = self
505 .catalog
506 .records()
507 .get(selected.record_index)
508 .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
509 artifact_identity: format!("record-index:{}", selected.record_index),
510 message: "selected provider index is outside the catalog".to_owned(),
511 })?;
512 let provision = record
513 .descriptor()
514 .provides
515 .get(selected.provision_index)
516 .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
517 artifact_identity: record.canonical_identity_key(),
518 message: "selected provision index is outside the descriptor".to_owned(),
519 })?;
520 Ok(PluginResolvedProvider {
521 service: service.clone(),
522 provided_version: provision.version.clone(),
523 artifact: record.clone(),
524 })
525 })
526 .collect::<Result<Vec<_>, PluginResolutionError>>()?;
527 let artifacts = artifact_order
528 .iter()
529 .map(|identity| {
530 let index = state.selected_artifacts.get(identity).ok_or_else(|| {
531 PluginResolutionError::InvalidCatalogRecord {
532 artifact_identity: identity.clone(),
533 message: "topological artifact is absent from selection".to_owned(),
534 }
535 })?;
536 self.catalog.records().get(*index).cloned().ok_or_else(|| {
537 PluginResolutionError::InvalidCatalogRecord {
538 artifact_identity: identity.clone(),
539 message: "topological artifact index is outside the catalog".to_owned(),
540 }
541 })
542 })
543 .collect::<Result<Vec<_>, _>>()?;
544 Ok(PluginResolution {
545 catalog_fingerprint: self.catalog.fingerprint().to_owned(),
546 providers,
547 artifacts,
548 })
549 }
550
551 fn topological_order(
552 &self,
553 state: &ResolverState,
554 ) -> Result<Vec<String>, PluginResolutionError> {
555 let mut adjacency = state
556 .selected_artifacts
557 .keys()
558 .map(|identity| (identity.clone(), BTreeSet::new()))
559 .collect::<BTreeMap<_, _>>();
560 let mut indegree = state
561 .selected_artifacts
562 .keys()
563 .map(|identity| (identity.clone(), 0_usize))
564 .collect::<BTreeMap<_, _>>();
565 let mut order_keys = BTreeMap::new();
566 for (identity, record_index) in &state.selected_artifacts {
567 let record = self.catalog.records().get(*record_index).ok_or_else(|| {
568 PluginResolutionError::InvalidCatalogRecord {
569 artifact_identity: identity.clone(),
570 message: "selected artifact index is outside the catalog".to_owned(),
571 }
572 })?;
573 order_keys.insert(identity.clone(), ArtifactOrderKey::from_record(record)?);
574 }
575
576 for (dependent_identity, record_index) in &state.selected_artifacts {
577 let record = self.catalog.records().get(*record_index).ok_or_else(|| {
578 PluginResolutionError::InvalidCatalogRecord {
579 artifact_identity: dependent_identity.clone(),
580 message: "selected artifact index is outside the catalog".to_owned(),
581 }
582 })?;
583 for requirement in &record.descriptor().requires {
584 let provider = state
585 .selected_services
586 .get(&requirement.service)
587 .ok_or_else(|| PluginResolutionError::MissingProvider {
588 service: requirement.service.clone(),
589 requirements: vec![requirement.requirement.clone()],
590 catalog_candidates: 0,
591 policy_candidates: 0,
592 })?;
593 let provider_record = self
594 .catalog
595 .records()
596 .get(provider.record_index)
597 .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
598 artifact_identity: format!("record-index:{}", provider.record_index),
599 message: "dependency provider index is outside the catalog".to_owned(),
600 })?;
601 let provider_identity = provider_record.canonical_identity_key();
602 let inserted = adjacency
603 .entry(provider_identity.clone())
604 .or_default()
605 .insert(dependent_identity.clone());
606 if inserted {
607 let value = indegree.get_mut(dependent_identity).ok_or_else(|| {
608 PluginResolutionError::InvalidCatalogRecord {
609 artifact_identity: dependent_identity.clone(),
610 message: "dependency target is absent from selection".to_owned(),
611 }
612 })?;
613 *value = value.saturating_add(1);
614 }
615 }
616 }
617
618 let mut ready = indegree
619 .iter()
620 .filter_map(|(identity, count)| {
621 (*count == 0)
622 .then(|| order_keys.get(identity).cloned())
623 .flatten()
624 })
625 .collect::<BTreeSet<_>>();
626 let mut order = Vec::with_capacity(indegree.len());
627 while let Some(key) = ready.pop_first() {
628 let identity = key.identity;
629 order.push(identity.clone());
630 if let Some(dependents) = adjacency.get(&identity) {
631 for dependent in dependents {
632 let count = indegree.get_mut(dependent).ok_or_else(|| {
633 PluginResolutionError::InvalidCatalogRecord {
634 artifact_identity: dependent.clone(),
635 message: "dependency target is absent from indegree map".to_owned(),
636 }
637 })?;
638 *count = count.saturating_sub(1);
639 if *count == 0 {
640 let key = order_keys.get(dependent).cloned().ok_or_else(|| {
641 PluginResolutionError::InvalidCatalogRecord {
642 artifact_identity: dependent.clone(),
643 message: "dependency target has no stable order key".to_owned(),
644 }
645 })?;
646 ready.insert(key);
647 }
648 }
649 }
650 }
651 if order.len() == indegree.len() {
652 return Ok(order);
653 }
654 let adjacency = adjacency
655 .into_iter()
656 .map(|(identity, dependents)| {
657 let mut dependents = dependents.into_iter().collect::<Vec<_>>();
658 dependents.sort_by(|left, right| {
659 order_keys
660 .get(left)
661 .cmp(&order_keys.get(right))
662 .then_with(|| left.cmp(right))
663 });
664 (identity, dependents)
665 })
666 .collect::<BTreeMap<String, Vec<String>>>();
667 Err(PluginResolutionError::DependencyCycle {
668 artifact_identities: find_cycle(&adjacency, &order_keys),
669 })
670 }
671}
672
673#[derive(Debug, Clone)]
674struct RequirementConstraint {
675 requirement: String,
676 parsed: VersionReq,
677 owner: RequirementOwner,
678}
679
680#[derive(Debug, Clone)]
681enum RequirementOwner {
682 Host,
683 Artifact(String),
684}
685
686impl RequirementOwner {
687 fn label(&self) -> &str {
688 match self {
689 Self::Host => "host",
690 Self::Artifact(identity) => identity,
691 }
692 }
693}
694
695#[derive(Debug, Clone, Copy)]
696struct SelectedProvider {
697 record_index: usize,
698 provision_index: usize,
699}
700
701#[derive(Debug, Clone, Default)]
702struct ResolverState {
703 requirements: BTreeMap<String, Vec<RequirementConstraint>>,
704 selected_services: BTreeMap<String, SelectedProvider>,
705 selected_artifacts: BTreeMap<String, usize>,
706 plugin_artifacts: BTreeMap<String, String>,
707 constraint_count: usize,
708}
709
710impl ResolverState {
711 fn add_constraint(
712 &mut self,
713 requirement: &PluginRequirement,
714 owner: RequirementOwner,
715 ) -> Result<(), PluginResolutionError> {
716 if self.constraint_count >= MAX_PLUGIN_RESOLUTION_CONSTRAINTS {
717 return Err(PluginResolutionError::ConstraintLimitExceeded {
718 limit: MAX_PLUGIN_RESOLUTION_CONSTRAINTS,
719 });
720 }
721 let parsed = VersionReq::parse(&requirement.requirement).map_err(|error| {
722 PluginResolutionError::InvalidRequirements {
723 message: format!(
724 "service `{}` has invalid semver requirement: {error}",
725 requirement.service
726 ),
727 }
728 })?;
729 self.requirements
730 .entry(requirement.service.clone())
731 .or_default()
732 .push(RequirementConstraint {
733 requirement: requirement.requirement.clone(),
734 parsed,
735 owner,
736 });
737 self.constraint_count += 1;
738 Ok(())
739 }
740
741 fn next_unresolved_service(&self) -> Option<String> {
742 self.requirements
743 .keys()
744 .find(|service| !self.selected_services.contains_key(*service))
745 .cloned()
746 }
747}
748
749#[derive(Debug)]
750struct Candidate {
751 record_index: usize,
752 provision_index: usize,
753 service_version: Version,
754 plugin_version: Version,
755 priority: i32,
756 plugin_id: String,
757 artifact_identity: String,
758}
759
760#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
761struct ArtifactOrderKey {
762 plugin_id: String,
763 plugin_version: Version,
764 transport: PluginArtifactTransport,
765 target: String,
766 architecture: String,
767 format: crate::PluginArtifactFormat,
768 identity: String,
769}
770
771impl ArtifactOrderKey {
772 fn from_record(record: &PluginCatalogRecord) -> Result<Self, PluginResolutionError> {
773 let descriptor = record.descriptor();
774 let plugin_version = Version::parse(&descriptor.version).map_err(|error| {
775 PluginResolutionError::InvalidCatalogRecord {
776 artifact_identity: record.canonical_identity_key(),
777 message: format!("invalid plugin version: {error}"),
778 }
779 })?;
780 Ok(Self {
781 plugin_id: descriptor.plugin_id.clone(),
782 plugin_version,
783 transport: descriptor.transport,
784 target: descriptor.target.clone(),
785 architecture: descriptor.architecture.clone(),
786 format: descriptor.format,
787 identity: record.canonical_identity_key(),
788 })
789 }
790}
791
792fn candidate_order(left: &Candidate, right: &Candidate) -> Ordering {
793 right
794 .service_version
795 .cmp(&left.service_version)
796 .then_with(|| right.priority.cmp(&left.priority))
797 .then_with(|| right.plugin_version.cmp(&left.plugin_version))
798 .then_with(|| left.plugin_id.cmp(&right.plugin_id))
799 .then_with(|| left.artifact_identity.cmp(&right.artifact_identity))
800}
801
802fn describe_constraints(constraints: &[RequirementConstraint]) -> Vec<String> {
803 constraints
804 .iter()
805 .map(|constraint| {
806 format!(
807 "{} required by {}",
808 constraint.requirement,
809 constraint.owner.label()
810 )
811 })
812 .collect()
813}
814
815fn remember_first(target: &mut Option<PluginResolutionError>, error: PluginResolutionError) {
816 if target.is_none() {
817 *target = Some(error);
818 }
819}
820
821fn validate_policy_text(
822 field: &str,
823 value: &str,
824 maximum_bytes: usize,
825) -> Result<(), PluginResolutionError> {
826 if value.is_empty() || value.len() > maximum_bytes {
827 return Err(PluginResolutionError::InvalidPolicy {
828 field: field.to_owned(),
829 message: format!("must contain 1 to {maximum_bytes} UTF-8 bytes"),
830 });
831 }
832 Ok(())
833}
834
835fn find_cycle(
836 adjacency: &BTreeMap<String, Vec<String>>,
837 order_keys: &BTreeMap<String, ArtifactOrderKey>,
838) -> Vec<String> {
839 let mut colors = adjacency
840 .keys()
841 .map(|identity| (identity.clone(), 0_u8))
842 .collect::<BTreeMap<_, _>>();
843 let starts = order_keys.values().cloned().collect::<BTreeSet<_>>();
844 for start in starts.iter().map(|key| &key.identity) {
845 if colors.get(start).copied().unwrap_or_default() != 0 {
846 continue;
847 }
848 colors.insert(start.clone(), 1);
849 let mut path = vec![start.clone()];
850 let mut stack = vec![(start.clone(), 0_usize)];
851 while let Some((identity, next_index)) = stack.last_mut() {
852 let neighbors = adjacency
853 .get(identity)
854 .map(Vec::as_slice)
855 .unwrap_or_default();
856 if *next_index >= neighbors.len() {
857 colors.insert(identity.clone(), 2);
858 stack.pop();
859 path.pop();
860 continue;
861 }
862 let neighbor = neighbors[*next_index].clone();
863 *next_index += 1;
864 match colors.get(&neighbor).copied().unwrap_or_default() {
865 0 => {
866 colors.insert(neighbor.clone(), 1);
867 path.push(neighbor.clone());
868 stack.push((neighbor, 0));
869 }
870 1 => {
871 if let Some(start_index) = path.iter().position(|node| node == &neighbor) {
872 let mut cycle = path[start_index..].to_vec();
873 cycle.push(neighbor);
874 return cycle;
875 }
876 }
877 _ => {}
878 }
879 }
880 }
881 adjacency.keys().cloned().collect()
882}