1use crate::constraint::{Constraint, Op};
7use crate::intervals;
8use crate::package::{Origin, Package};
9use crate::platform::is_platform_package;
10use crate::repository::{is_package_acceptable, ComposerRepository, RepoError};
11use crate::root::RootAlias;
12use crate::version::{regex, stability_rank};
13use pcre2::bytes::Regex;
14use std::collections::{BTreeMap, BTreeSet, HashMap};
15use std::sync::OnceLock;
16
17#[derive(Debug, thiserror::Error)]
18#[error("{0}")]
19pub struct PoolError(pub String);
20
21impl From<RepoError> for PoolError {
22 fn from(e: RepoError) -> PoolError {
23 PoolError(e.0)
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct OrderedMap<V>(pub Vec<(String, V)>);
30
31impl<V> Default for OrderedMap<V> {
32 fn default() -> OrderedMap<V> {
33 OrderedMap(Vec::new())
34 }
35}
36
37impl<V> OrderedMap<V> {
38 pub fn get(&self, key: &str) -> Option<&V> {
39 self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
40 }
41 pub fn contains(&self, key: &str) -> bool {
42 self.0.iter().any(|(k, _)| k == key)
43 }
44 pub fn insert(&mut self, key: &str, value: V) {
45 if let Some(slot) = self.0.iter_mut().find(|(k, _)| k == key) {
46 slot.1 = value;
47 } else {
48 self.0.push((key.to_owned(), value));
49 }
50 }
51 pub fn remove(&mut self, key: &str) -> Option<V> {
52 let pos = self.0.iter().position(|(k, _)| k == key)?;
53 Some(self.0.remove(pos).1)
54 }
55 pub fn len(&self) -> usize {
56 self.0.len()
57 }
58 pub fn is_empty(&self) -> bool {
59 self.0.is_empty()
60 }
61 pub fn iter(&self) -> impl Iterator<Item = (&String, &V)> {
62 self.0.iter().map(|(k, v)| (k, v))
63 }
64 pub fn keys(&self) -> impl Iterator<Item = &String> {
65 self.0.iter().map(|(k, _)| k)
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum UpdateMode {
72 OnlyListed,
73 ListedWithTransitiveDepsNoRootRequire,
74 ListedWithTransitiveDeps,
75}
76
77#[derive(Debug, Clone, Default)]
81pub struct Request {
82 pub locked_repository: Option<Vec<usize>>,
83 pub requires: OrderedMap<Constraint>,
84 pub pretty_requires: OrderedMap<String>,
86 pub fixed_packages: Vec<usize>,
87 pub locked_packages: Vec<usize>,
88 pub fixed_locked_packages: Vec<usize>,
89 pub update_allow_list: Vec<String>,
90 pub update_mode: Option<UpdateMode>,
91 pub restricted_packages: Option<Vec<String>>,
92}
93
94fn push_unique(list: &mut Vec<usize>, idx: usize) {
95 if !list.contains(&idx) {
96 list.push(idx);
97 }
98}
99
100impl Request {
101 pub fn new(locked_repository: Option<Vec<usize>>) -> Request {
102 Request {
103 locked_repository,
104 ..Request::default()
105 }
106 }
107
108 pub fn require_name(
109 &mut self,
110 name: &str,
111 constraint: Option<Constraint>,
112 ) -> Result<(), PoolError> {
113 let pretty = constraint
114 .as_ref()
115 .map_or_else(|| "*".to_owned(), |c| c.to_string());
116 self.require_name_pretty(name, constraint, &pretty)
117 }
118
119 pub fn require_name_pretty(
122 &mut self,
123 name: &str,
124 constraint: Option<Constraint>,
125 pretty: &str,
126 ) -> Result<(), PoolError> {
127 let name = name.to_lowercase();
128 let constraint = constraint.unwrap_or(Constraint::MatchAll);
129 if let Some(existing) = self.requires.get(&name) {
130 return Err(PoolError(format!(
131 "Overwriting requires seems like a bug ({name} {existing} => {constraint}, check why it is happening, might be a root alias"
132 )));
133 }
134 self.requires.insert(&name, constraint);
135 self.pretty_requires.insert(&name, pretty.to_owned());
136 Ok(())
137 }
138
139 pub fn pretty_require(&self, name: &str, filtered: &Constraint) -> String {
144 match (self.requires.get(name), self.pretty_requires.get(name)) {
145 (Some(original), Some(pretty)) if original == filtered => pretty.clone(),
146 _ => filtered.to_string(),
147 }
148 }
149
150 pub fn fix_package(&mut self, idx: usize) {
151 push_unique(&mut self.fixed_packages, idx);
152 }
153
154 pub fn lock_package(&mut self, idx: usize) {
155 push_unique(&mut self.locked_packages, idx);
156 }
157
158 pub fn fix_locked_package(&mut self, idx: usize) {
159 push_unique(&mut self.fixed_packages, idx);
160 push_unique(&mut self.fixed_locked_packages, idx);
161 }
162
163 pub fn unlock_package(&mut self, idx: usize) {
164 self.locked_packages.retain(|i| *i != idx);
165 }
166
167 pub fn set_update_allow_list(&mut self, list: Vec<String>, mode: UpdateMode) {
168 self.update_allow_list = list;
169 self.update_mode = Some(mode);
170 }
171
172 pub fn update_allow_transitive_dependencies(&self) -> bool {
173 self.update_mode != Some(UpdateMode::OnlyListed)
176 }
177
178 pub fn update_allow_transitive_root_dependencies(&self) -> bool {
179 self.update_mode == Some(UpdateMode::ListedWithTransitiveDeps)
180 }
181
182 pub fn is_fixed_package(&self, idx: usize) -> bool {
183 self.fixed_packages.contains(&idx)
184 }
185
186 pub fn is_locked_package(&self, idx: usize) -> bool {
187 self.locked_packages.contains(&idx) || self.fixed_locked_packages.contains(&idx)
188 }
189
190 pub fn locked_packages_all(&self) -> Vec<usize> {
192 let mut out = self.locked_packages.clone();
193 out.extend(self.fixed_locked_packages.iter().copied());
194 out
195 }
196
197 pub fn fixed_or_locked_packages(&self) -> Vec<usize> {
199 let mut out = self.fixed_packages.clone();
200 out.extend(self.locked_packages.iter().copied());
201 out
202 }
203}
204
205pub enum Repository {
207 Root(Vec<usize>),
209 Platform(Vec<usize>),
211 Composer(Box<ComposerRepository>),
212 Path(crate::path_repo::PathRepository),
214 Locked(Vec<usize>),
216}
217
218pub struct RepositorySet {
220 pub root_aliases: BTreeMap<String, BTreeMap<String, (String, String)>>,
222 pub root_references: BTreeMap<String, String>,
223 pub acceptable_stabilities: BTreeMap<String, i32>,
224 pub stability_flags: BTreeMap<String, i32>,
225 pub root_requires: OrderedMap<Constraint>,
226 pub temporary_constraints: BTreeMap<String, Constraint>,
227 pub repositories: Vec<Repository>,
228}
229
230impl RepositorySet {
231 pub fn new(
232 minimum_stability: &str,
233 stability_flags: BTreeMap<String, i32>,
234 root_aliases: &[RootAlias],
235 root_references: BTreeMap<String, String>,
236 root_requires: OrderedMap<Constraint>,
237 temporary_constraints: BTreeMap<String, Constraint>,
238 ) -> RepositorySet {
239 let mut aliases: BTreeMap<String, BTreeMap<String, (String, String)>> = BTreeMap::new();
240 for a in root_aliases {
241 aliases.entry(a.package.clone()).or_default().insert(
242 a.version.clone(),
243 (a.alias.clone(), a.alias_normalized.clone()),
244 );
245 }
246 let min = stability_rank(minimum_stability);
247 let mut acceptable = BTreeMap::new();
248 for s in ["stable", "RC", "beta", "alpha", "dev"] {
249 let rank = stability_rank(s);
250 if rank <= min {
251 acceptable.insert(s.to_owned(), rank);
252 }
253 }
254 let mut requires = OrderedMap::default();
255 for (name, c) in root_requires.iter() {
256 if !is_platform_package(name) {
257 requires.insert(name, c.clone());
258 }
259 }
260 RepositorySet {
261 root_aliases: aliases,
262 root_references,
263 acceptable_stabilities: acceptable,
264 stability_flags,
265 root_requires: requires,
266 temporary_constraints,
267 repositories: Vec::new(),
268 }
269 }
270
271 pub fn add_repository(&mut self, repo: Repository) {
272 self.repositories.push(repo);
273 }
274
275 pub fn create_pool(
277 &self,
278 request: &mut Request,
279 arena: &mut Vec<Package>,
280 pre_pool: Option<&PrePoolFilter>,
281 ) -> Result<Pool, PoolError> {
282 let mut builder = PoolBuilder::new(self);
283 builder.build_pool(&self.repositories, request, arena, pre_pool)
284 }
285}
286
287#[derive(Debug, Clone)]
292pub struct PrePoolFilter {
293 pub symfony_require: String,
295 pub symfony: Constraint,
296 pub root_constraints: BTreeMap<String, Constraint>,
298 pub versions: crate::flex_filter::FlexVersions,
299}
300
301pub type FilterListRemoved = BTreeMap<String, Vec<(String, Vec<crate::repository::FilterEntry>)>>;
304
305#[derive(Debug, Clone, Default)]
306pub struct Pool {
307 pub identity: u64,
310 pub packages: Vec<usize>,
312 id_of: HashMap<usize, usize>,
313 package_by_name: HashMap<String, Vec<usize>>,
314 pub unacceptable_fixed_or_locked: Vec<usize>,
315 pub warnings: Vec<String>,
316 pub flex_notice: Option<String>,
319 pub filter_list_removed: FilterListRemoved,
322 pub security_removed: BTreeMap<String, Vec<(String, Vec<String>)>>,
325 pub abandoned_removed: BTreeMap<String, BTreeMap<String, String>>,
327 pub removed_versions: BTreeMap<String, BTreeMap<String, String>>,
330 pub removed_versions_by_package: BTreeMap<usize, BTreeMap<String, String>>,
333}
334
335impl Pool {
336 pub fn new(packages: Vec<usize>, unacceptable: Vec<usize>, arena: &[Package]) -> Pool {
337 static NEXT_IDENTITY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
338 let mut pool = Pool {
339 flex_notice: None,
340 identity: NEXT_IDENTITY.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
341 packages: Vec::with_capacity(packages.len()),
342 id_of: HashMap::new(),
343 package_by_name: HashMap::new(),
344 unacceptable_fixed_or_locked: unacceptable,
345 warnings: Vec::new(),
346 filter_list_removed: BTreeMap::new(),
347 security_removed: BTreeMap::new(),
348 abandoned_removed: BTreeMap::new(),
349 removed_versions: BTreeMap::new(),
350 removed_versions_by_package: BTreeMap::new(),
351 };
352 for idx in packages {
353 pool.packages.push(idx);
354 let id = pool.packages.len();
355 pool.id_of.insert(idx, id);
356 for name in arena[idx].names(true) {
357 pool.package_by_name.entry(name).or_default().push(id);
358 }
359 }
360 pool
361 }
362
363 pub fn len(&self) -> usize {
364 self.packages.len()
365 }
366
367 pub fn is_empty(&self) -> bool {
368 self.packages.is_empty()
369 }
370
371 pub fn package_by_id(&self, id: usize) -> usize {
373 self.packages[id - 1]
374 }
375
376 pub fn id_of(&self, arena_idx: usize) -> Option<usize> {
377 self.id_of.get(&arena_idx).copied()
378 }
379
380 pub fn literal_to_package(&self, literal: i64) -> usize {
381 self.package_by_id(literal.unsigned_abs() as usize)
382 }
383
384 pub fn what_provides(
386 &self,
387 arena: &[Package],
388 name: &str,
389 constraint: Option<&Constraint>,
390 ) -> Vec<usize> {
391 let Some(candidates) = self.package_by_name.get(name) else {
392 return Vec::new();
393 };
394 candidates
395 .iter()
396 .copied()
397 .filter(|id| Self::matches(&arena[self.packages[id - 1]], name, constraint))
398 .collect()
399 }
400
401 pub fn matches(candidate: &Package, name: &str, constraint: Option<&Constraint>) -> bool {
403 if candidate.name == name {
404 return match constraint {
405 None => true,
406 Some(c) => c.matches_version(&candidate.version),
407 };
408 }
409 let provides = &candidate.provides;
410 let replaces = &candidate.replaces;
411 if replaces.has_numeric_keys() || provides.has_numeric_keys() {
416 for link in provides.iter().chain(replaces.iter()) {
417 if link.target == name && constraint.is_none_or(|c| c.matches(&link.constraint)) {
418 return true;
419 }
420 }
421 return false;
422 }
423 if let Some(link) = provides.get(name) {
424 if constraint.is_none_or(|c| c.matches(&link.constraint)) {
425 return true;
426 }
427 }
428 if let Some(link) = replaces.get(name) {
429 if constraint.is_none_or(|c| c.matches(&link.constraint)) {
430 return true;
431 }
432 }
433 false
434 }
435
436 pub fn is_filter_list_removed(&self, name: &str, version: &str) -> bool {
438 self.filter_list_removed
439 .get(name)
440 .is_some_and(|versions| versions.iter().any(|(v, _)| v == version))
441 }
442
443 pub fn with_packages(&self, kept: Vec<usize>, arena: &[Package]) -> Pool {
446 let mut pool = Pool::new(kept, self.unacceptable_fixed_or_locked.clone(), arena);
447 pool.warnings = self.warnings.clone();
448 pool.filter_list_removed = self.filter_list_removed.clone();
449 pool.security_removed = self.security_removed.clone();
450 pool.abandoned_removed = self.abandoned_removed.clone();
451 pool.removed_versions = self.removed_versions.clone();
452 pool.removed_versions_by_package = self.removed_versions_by_package.clone();
453 pool
454 }
455
456 pub fn is_security_removed(&self, name: &str, constraint: &Constraint) -> bool {
458 !self.security_advisory_ids(name, constraint).is_empty()
459 }
460
461 pub fn security_advisory_ids(&self, name: &str, constraint: &Constraint) -> Vec<String> {
464 self.security_removed
465 .get(name)
466 .and_then(|m| {
467 m.iter()
468 .find(|(v, _)| constraint.matches(&Constraint::new(Op::Eq, v.clone())))
469 .map(|(_, ids)| ids.clone())
470 })
471 .unwrap_or_default()
472 }
473
474 pub fn is_abandoned_removed(&self, name: &str, constraint: &Constraint) -> bool {
476 self.abandoned_removed.get(name).is_some_and(|m| {
477 m.keys()
478 .any(|v| constraint.matches(&Constraint::new(Op::Eq, v.clone())))
479 })
480 }
481
482 pub fn is_filter_list_removed_version(&self, name: &str, constraint: &Constraint) -> bool {
484 self.filter_list_removed.get(name).is_some_and(|versions| {
485 versions
486 .iter()
487 .any(|(v, _)| constraint.matches(&Constraint::new(Op::Eq, v.clone())))
488 })
489 }
490
491 pub fn filter_list_entries_text(
494 &self,
495 name: &str,
496 constraint: &Constraint,
497 ) -> Vec<(String, String)> {
498 let mut lists: Vec<(String, Vec<String>)> = Vec::new();
499 let mut seen: Vec<&crate::repository::FilterEntry> = Vec::new();
502 if let Some(versions) = self.filter_list_removed.get(name) {
503 for (v, entries) in versions {
504 if !constraint.matches(&Constraint::new(Op::Eq, v.clone())) {
505 continue;
506 }
507 for e in entries {
508 if seen.iter().any(|s| s.same_entry(e)) {
509 continue;
510 }
511 seen.push(e);
512 let source = e
513 .source
514 .as_deref()
515 .filter(|s| !s.is_empty() && *s != "0")
516 .map(|s| format!(" reported by {s}"))
517 .unwrap_or_default();
518 let url = e
519 .url
520 .as_deref()
521 .filter(|s| !s.is_empty() && *s != "0")
522 .map(|s| format!(" (see {s})"))
523 .unwrap_or_default();
524 let reason = e
525 .reason
526 .as_deref()
527 .filter(|s| !s.is_empty() && *s != "0")
528 .map(|s| format!(" reason: {s}"))
529 .unwrap_or_default();
530 let text = format!("{source}{url}{reason}");
531 match lists.iter_mut().find(|(l, _)| *l == e.list_name) {
532 Some((_, texts)) => texts.push(text),
533 None => lists.push((e.list_name.clone(), vec![text])),
534 }
535 }
536 }
537 }
538 lists
539 .into_iter()
540 .map(|(list, texts)| {
541 let action = if list == "malware" {
542 "flagged as "
543 } else {
544 "filtered by "
545 };
546 (list.clone(), format!("{action}{list}{}", texts.join(", ")))
547 })
548 .collect()
549 }
550
551 pub fn is_unacceptable_fixed_or_locked(&self, arena_idx: usize) -> bool {
552 self.unacceptable_fixed_or_locked.contains(&arena_idx)
553 }
554
555 pub fn removed_versions(&self, name: &str, constraint: &Constraint) -> Vec<(String, String)> {
558 let Some(versions) = self.removed_versions.get(name) else {
559 return Vec::new();
560 };
561 versions
562 .iter()
563 .filter(|(v, _)| constraint.matches(&Constraint::new(Op::Eq, (*v).clone())))
564 .map(|(v, p)| (v.clone(), p.clone()))
565 .collect()
566 }
567
568 pub fn removed_versions_by_package(&self, arena_idx: usize) -> Vec<(String, String)> {
570 self.removed_versions_by_package
571 .get(&arena_idx)
572 .map(|m| m.iter().map(|(v, p)| (v.clone(), p.clone())).collect())
573 .unwrap_or_default()
574 }
575}
576
577const LOAD_BATCH_SIZE: usize = 50;
578
579pub fn package_name_regexp(pattern: &str) -> Regex {
581 let quoted = crate::version::preg_quote(pattern).replace("\\*", ".*");
582 pcre2::bytes::RegexBuilder::new()
583 .caseless(true)
584 .build(&format!("^{quoted}$"))
585 .unwrap_or_else(|e| panic!("pattern {pattern}: {e}"))
586}
587
588struct PoolBuilder<'a> {
589 set: &'a RepositorySet,
590 alias_map: HashMap<usize, Vec<(usize, usize)>>,
592 packages_to_load: OrderedMap<Constraint>,
593 loaded_packages: BTreeMap<String, Constraint>,
594 loaded_per_repo: BTreeMap<usize, BTreeMap<String, BTreeSet<String>>>,
595 packages: Vec<Option<usize>>,
597 unacceptable: Vec<usize>,
598 update_allow_list: Vec<String>,
599 update_allow_patterns: Vec<Regex>,
600 skipped_load: BTreeMap<String, Vec<usize>>,
601 ignored_types: Vec<String>,
602 allowed_types: Option<Vec<String>>,
603 restricted: Option<BTreeSet<String>>,
604 path_repo_unlocked: BTreeSet<String>,
605 max_extended_reqs: BTreeSet<String>,
606 update_allow_warned: BTreeSet<String>,
607 warnings: Vec<String>,
608}
609
610impl<'a> PoolBuilder<'a> {
611 fn new(set: &'a RepositorySet) -> PoolBuilder<'a> {
612 PoolBuilder {
613 set,
614 alias_map: HashMap::new(),
615 packages_to_load: OrderedMap::default(),
616 loaded_packages: BTreeMap::new(),
617 loaded_per_repo: BTreeMap::new(),
618 packages: Vec::new(),
619 unacceptable: Vec::new(),
620 update_allow_list: Vec::new(),
621 update_allow_patterns: Vec::new(),
622 skipped_load: BTreeMap::new(),
623 ignored_types: Vec::new(),
624 allowed_types: None,
625 restricted: None,
626 path_repo_unlocked: BTreeSet::new(),
627 max_extended_reqs: BTreeSet::new(),
628 update_allow_warned: BTreeSet::new(),
629 warnings: Vec::new(),
630 }
631 }
632
633 fn loaded_packages_in_pool(&self) -> Vec<usize> {
634 self.packages.iter().flatten().copied().collect()
635 }
636
637 fn build_pool(
638 &mut self,
639 repositories: &[Repository],
640 request: &mut Request,
641 arena: &mut Vec<Package>,
642 pre_pool: Option<&PrePoolFilter>,
643 ) -> Result<Pool, PoolError> {
644 self.restricted = request
645 .restricted_packages
646 .as_ref()
647 .map(|l| l.iter().cloned().collect());
648
649 if !request.update_allow_list.is_empty() {
650 self.update_allow_list = request.update_allow_list.clone();
651 self.update_allow_patterns = self
652 .update_allow_list
653 .iter()
654 .map(|p| package_name_regexp(p))
655 .collect();
656 self.warn_about_non_matching_update_allow_list(request, arena)?;
657 let Some(locked) = request.locked_repository.clone() else {
658 return Err(PoolError(
659 "No lock repo present and yet a partial update was requested.".into(),
660 ));
661 };
662 for locked_idx in locked {
663 if !self.is_update_allowed(&arena[locked_idx]) {
664 let p = &arena[locked_idx];
665 self.skipped_load
666 .entry(p.name.clone())
667 .or_default()
668 .push(locked_idx);
669 for link in p.replaces.iter() {
670 self.skipped_load
671 .entry(link.target.clone())
672 .or_default()
673 .push(locked_idx);
674 }
675 if p.dist.as_ref().is_some_and(|d| d.kind == "path") {
676 let symlink = p
677 .raw
678 .get("transport-options")
679 .and_then(|t| t.get("symlink"))
680 .cloned();
681 if symlink != Some(serde_json::Value::Bool(false)) {
682 self.path_repo_unlocked.insert(p.name.clone());
683 continue;
684 }
685 }
686 request.lock_package(locked_idx);
687 }
688 }
689 }
690
691 for idx in request.fixed_or_locked_packages() {
692 let (name, replaces, names, stability, origin) = {
693 let p = &arena[idx];
694 (
695 p.name.clone(),
696 p.replaces
697 .iter()
698 .map(|l| l.target.clone())
699 .collect::<Vec<_>>(),
700 p.names(true),
701 p.stability,
702 p.origin,
703 )
704 };
705 self.loaded_packages.insert(name, Constraint::MatchAll);
706 for target in replaces {
707 self.loaded_packages.insert(target, Constraint::MatchAll);
708 }
709 if matches!(origin, Origin::Root | Origin::Platform)
710 || is_package_acceptable(
711 &self.set.acceptable_stabilities,
712 &self.set.stability_flags,
713 &names,
714 stability,
715 )
716 {
717 self.load_package(request, idx, false, arena);
718 } else {
719 self.unacceptable.push(idx);
720 }
721 }
722
723 let requires: Vec<(String, Constraint)> = request.requires.0.clone();
724 for (name, constraint) in &requires {
725 if self.loaded_packages.contains_key(name) {
726 continue;
727 }
728 self.packages_to_load.insert(name, constraint.clone());
729 self.max_extended_reqs.insert(name.clone());
730 }
731 let already: Vec<String> = self
732 .packages_to_load
733 .keys()
734 .filter(|n| self.loaded_packages.contains_key(*n))
735 .cloned()
736 .collect();
737 for name in already {
738 self.packages_to_load.remove(&name);
739 }
740
741 while !self.packages_to_load.is_empty() {
742 self.load_packages_marked_for_loading(request, repositories, arena)?;
743 }
744
745 if !self.set.temporary_constraints.is_empty() {
746 let entries: Vec<(usize, usize)> = self
747 .packages
748 .iter()
749 .enumerate()
750 .filter_map(|(i, p)| p.map(|idx| (i, idx)))
751 .collect();
752 for (i, idx) in entries {
753 if arena[idx].is_alias() {
754 continue;
755 }
756 for name in arena[idx].names(true) {
757 let Some(constraint) = self.set.temporary_constraints.get(&name) else {
758 continue;
759 };
760 let mut package_and_aliases: Vec<(usize, usize)> = vec![(i, idx)];
761 if let Some(aliases) = self.alias_map.get(&idx) {
762 package_and_aliases.extend(aliases.iter().copied());
763 }
764 let found = package_and_aliases
765 .iter()
766 .any(|(_, p)| constraint.matches_version(&arena[*p].version));
767 if !found {
768 for (pool_index, _) in package_and_aliases {
769 self.packages[pool_index] = None;
770 }
771 }
772 }
773 }
774 }
775
776 let mut packages = self.loaded_packages_in_pool();
777 let mut flex_notice = None;
780 if let Some(f) = pre_pool {
781 let mut locked: BTreeMap<String, Vec<String>> = BTreeMap::new();
782 for idx in request.fixed_or_locked_packages() {
783 let p = &arena[idx];
784 let entry = locked.entry(p.name.clone()).or_default();
785 entry.push(p.version.clone());
786 if let Some(base) = p.alias_of {
787 entry.push(arena[base].version.clone());
788 }
789 }
790 let (kept, restricting) = crate::flex_filter::remove_legacy_packages(
791 &packages,
792 arena,
793 &f.root_constraints,
794 &locked,
795 &f.symfony,
796 &f.versions,
797 );
798 if std::env::var_os("VIVACITY_TRACE").is_some() {
799 eprintln!(
800 "trace: flex filter {} → {} package versions (require {}, {} splits)",
801 packages.len(),
802 kept.len(),
803 f.symfony_require,
804 f.versions.splits.len()
805 );
806 }
807 packages = kept;
808 if restricting {
809 flex_notice = Some(format!(
810 "Restricting packages listed in \"symfony/symfony\" to \"{}\"",
811 f.symfony_require
812 ));
813 }
814 }
815 let mut pool = Pool::new(packages, std::mem::take(&mut self.unacceptable), arena);
816 pool.warnings = std::mem::take(&mut self.warnings);
817 pool.flex_notice = flex_notice;
818 Ok(pool)
819 }
820
821 fn mark_package_name_for_loading(
823 &mut self,
824 request: &Request,
825 name: &str,
826 constraint: &Constraint,
827 ) {
828 if is_platform_package(name) {
829 return;
830 }
831 if self.max_extended_reqs.contains(name) {
832 return;
833 }
834 let mut constraint = constraint.clone();
835 if let Some(root) = request.requires.get(name) {
836 if !intervals::is_subset_of(&constraint, root) {
837 constraint = root.clone();
838 }
839 }
840 if !self.loaded_packages.contains_key(name) {
841 if let Some(pending) = self.packages_to_load.get(name) {
842 if intervals::is_subset_of(&constraint, pending) {
843 return;
844 }
845 constraint = intervals::compact_constraint(&Constraint::create(
846 vec![pending.clone(), constraint],
847 false,
848 ));
849 }
850 self.packages_to_load.insert(name, constraint);
851 return;
852 }
853 let loaded = &self.loaded_packages[name];
854 if intervals::is_subset_of(&constraint, loaded) {
855 return;
856 }
857 let merged = intervals::compact_constraint(&Constraint::create(
858 vec![loaded.clone(), constraint],
859 false,
860 ));
861 self.packages_to_load.insert(name, merged);
862 self.loaded_packages.remove(name);
863 }
864
865 fn load_packages_marked_for_loading(
867 &mut self,
868 request: &mut Request,
869 repositories: &[Repository],
870 arena: &mut Vec<Package>,
871 ) -> Result<(), PoolError> {
872 let names: Vec<String> = self.packages_to_load.keys().cloned().collect();
873 for name in names {
874 if let Some(restricted) = &self.restricted {
875 if !restricted.contains(&name) {
876 self.packages_to_load.remove(&name);
877 continue;
878 }
879 }
880 let c = self
881 .packages_to_load
882 .get(&name)
883 .cloned()
884 .unwrap_or(Constraint::MatchAll);
885 self.loaded_packages.insert(name, c);
886 }
887 let mut remaining: Vec<(String, Constraint)> = std::mem::take(&mut self.packages_to_load).0;
888 for (repo_index, repository) in repositories.iter().enumerate() {
889 if matches!(repository, Repository::Platform(_) | Repository::Locked(_)) {
890 continue;
891 }
892 if remaining.is_empty() {
893 break;
894 }
895 let batches: Vec<Vec<(String, Constraint)>> = remaining
896 .chunks(LOAD_BATCH_SIZE)
897 .map(|c| c.to_vec())
898 .collect();
899 let mut kept: Vec<Vec<(String, Constraint)>> = Vec::new();
900 for batch in batches {
901 let empty = BTreeMap::new();
902 let already = self.loaded_per_repo.get(&repo_index).unwrap_or(&empty);
903 let (names_found, ids) = match repository {
904 Repository::Composer(repo) => repo.load_packages(
905 &batch,
906 &self.set.acceptable_stabilities,
907 &self.set.stability_flags,
908 already,
909 Origin::Repository(repo_index),
910 arena,
911 )?,
912 Repository::Root(members) => array_repository_load_packages(
913 members,
914 &batch,
915 &self.set.acceptable_stabilities,
916 &self.set.stability_flags,
917 already,
918 arena,
919 ),
920 Repository::Path(repo) => array_repository_load_packages(
921 &repo.members,
922 &batch,
923 &self.set.acceptable_stabilities,
924 &self.set.stability_flags,
925 already,
926 arena,
927 ),
928 Repository::Platform(_) | Repository::Locked(_) => unreachable!(),
929 };
930 let mut batch = batch;
931 batch.retain(|(n, _)| !names_found.contains(n));
932 kept.push(batch);
933 for idx in ids {
934 let (name, version, package_type) = {
935 let p = &arena[idx];
936 (p.name.clone(), p.version.clone(), p.package_type.clone())
937 };
938 self.loaded_per_repo
939 .entry(repo_index)
940 .or_default()
941 .entry(name.clone())
942 .or_default()
943 .insert(version);
944 if self.ignored_types.contains(&package_type)
945 || self
946 .allowed_types
947 .as_ref()
948 .is_some_and(|a| !a.contains(&package_type))
949 {
950 continue;
951 }
952 let propagate = !self.path_repo_unlocked.contains(&name);
953 self.load_package(request, idx, propagate, arena);
954 }
955 }
956 remaining = kept.into_iter().flatten().collect();
957 }
958 Ok(())
959 }
960
961 fn load_package(
963 &mut self,
964 request: &mut Request,
965 idx: usize,
966 propagate_update: bool,
967 arena: &mut Vec<Package>,
968 ) {
969 let index = self.packages.len();
970 self.packages.push(Some(idx));
971 if let Some(base) = arena[idx].alias_of {
972 self.alias_map.entry(base).or_default().push((index, idx));
973 }
974 let name = arena[idx].name.clone();
975 if let Some(reference) = self.set.root_references.get(&name) {
976 if !request.is_locked_package(idx) && !request.is_fixed_package(idx) {
977 set_source_dist_references(arena, idx, reference);
978 }
979 }
980 if propagate_update || self.path_repo_unlocked.contains(&name) {
981 let version = arena[idx].version.clone();
982 if let Some((alias, alias_normalized)) = self
983 .set
984 .root_aliases
985 .get(&name)
986 .and_then(|m| m.get(&version))
987 {
988 let base = arena[idx].alias_of.unwrap_or(idx);
989 let mut alias_package = arena[base].alias(base, alias_normalized, alias);
990 alias_package.root_package_alias = true;
991 alias_package.origin = Origin::Detached;
992 arena.push(alias_package);
993 let alias_idx = arena.len() - 1;
994 let new_index = self.packages.len();
995 self.packages.push(Some(alias_idx));
996 self.alias_map
997 .entry(base)
998 .or_default()
999 .push((new_index, alias_idx));
1000 }
1001 }
1002 let requires: Vec<(String, Constraint)> = arena[idx]
1003 .requires
1004 .iter()
1005 .map(|l| (l.target.clone(), l.constraint.clone()))
1006 .collect();
1007 for (require, link_constraint) in requires {
1008 if self.skipped_load.contains_key(&require) {
1009 if propagate_update && request.update_allow_transitive_dependencies() {
1010 let skipped_root_requires =
1011 self.skipped_root_requires(request, &require, arena);
1012 if request.update_allow_transitive_root_dependencies()
1013 || skipped_root_requires.is_empty()
1014 {
1015 self.unlock_package(request, &require, arena);
1016 self.mark_package_name_for_loading(request, &require, &link_constraint);
1017 } else {
1018 self.warn_root_requires(&skipped_root_requires);
1019 }
1020 } else if self.path_repo_unlocked.contains(&require)
1021 && !self.loaded_packages.contains_key(&require)
1022 {
1023 self.mark_package_name_for_loading(request, &require, &link_constraint);
1024 }
1025 } else {
1026 self.mark_package_name_for_loading(request, &require, &link_constraint);
1027 }
1028 }
1029 if propagate_update && request.update_allow_transitive_dependencies() {
1030 let replaces: Vec<String> = arena[idx]
1031 .replaces
1032 .iter()
1033 .map(|l| l.target.clone())
1034 .collect();
1035 for replace in replaces {
1036 if self.loaded_packages.contains_key(&replace)
1037 && self.skipped_load.contains_key(&replace)
1038 {
1039 let skipped_root_requires =
1040 self.skipped_root_requires(request, &replace, arena);
1041 if request.update_allow_transitive_root_dependencies()
1042 || skipped_root_requires.is_empty()
1043 {
1044 self.unlock_package(request, &replace, arena);
1045 self.mark_package_name_for_loading_if_required(request, &replace, arena);
1046 } else {
1047 self.warn_root_requires(&skipped_root_requires);
1048 }
1049 }
1050 }
1051 }
1052 }
1053
1054 fn warn_root_requires(&mut self, root_requires: &[String]) {
1055 for root_require in root_requires {
1056 if self.update_allow_warned.insert(root_require.clone()) {
1057 self.warnings.push(format!(
1058 "Dependency {root_require} is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies (-W) to include root dependencies."
1059 ));
1060 }
1061 }
1062 }
1063
1064 fn is_root_require(request: &Request, name: &str) -> bool {
1065 request.requires.contains(name)
1066 }
1067
1068 fn skipped_root_requires(
1070 &self,
1071 request: &Request,
1072 name: &str,
1073 arena: &[Package],
1074 ) -> Vec<String> {
1075 let Some(skipped) = self.skipped_load.get(name) else {
1076 return Vec::new();
1077 };
1078 if request.requires.contains(name) {
1079 return skipped
1080 .iter()
1081 .map(|idx| {
1082 let p = &arena[*idx];
1083 if p.name != name {
1084 format!("{} (via replace of {name})", p.name)
1085 } else {
1086 p.name.clone()
1087 }
1088 })
1089 .collect();
1090 }
1091 let mut matches = Vec::new();
1092 for idx in skipped {
1093 let p = &arena[*idx];
1094 if request.requires.contains(&p.name) {
1095 matches.push(p.name.clone());
1096 }
1097 for link in p.replaces.iter() {
1098 if request.requires.contains(&link.target) {
1099 if p.name != name {
1100 matches.push(format!("{} (via replace of {name})", p.name));
1101 } else {
1102 matches.push(p.name.clone());
1103 }
1104 break;
1105 }
1106 }
1107 }
1108 matches
1109 }
1110
1111 fn is_update_allowed(&self, package: &Package) -> bool {
1113 self.update_allow_patterns
1114 .iter()
1115 .any(|re| re.is_match(package.name.as_bytes()).unwrap_or(false))
1116 }
1117
1118 fn warn_about_non_matching_update_allow_list(
1120 &mut self,
1121 request: &Request,
1122 arena: &[Package],
1123 ) -> Result<(), PoolError> {
1124 let Some(locked) = &request.locked_repository else {
1125 return Err(PoolError(
1126 "No lock repo present and yet a partial update was requested.".into(),
1127 ));
1128 };
1129 'patterns: for pattern in &self.update_allow_list.clone() {
1130 let mut matched_platform = false;
1131 let re = package_name_regexp(pattern);
1132 for idx in locked {
1133 if re.is_match(arena[*idx].name.as_bytes()).unwrap_or(false) {
1134 continue 'patterns;
1135 }
1136 }
1137 for name in request.requires.keys() {
1138 if re.is_match(name.as_bytes()).unwrap_or(false) {
1139 if is_platform_package(name) {
1140 matched_platform = true;
1141 continue;
1142 }
1143 continue 'patterns;
1144 }
1145 }
1146 if matched_platform {
1147 self.warnings.push(format!(
1148 "Pattern \"{pattern}\" listed for update matches platform packages, but these cannot be updated by Composer."
1149 ));
1150 } else if pattern.contains('*') {
1151 self.warnings.push(format!(
1152 "Pattern \"{pattern}\" listed for update does not match any locked packages."
1153 ));
1154 } else {
1155 self.warnings.push(format!(
1156 "Package \"{pattern}\" listed for update is not locked."
1157 ));
1158 }
1159 }
1160 Ok(())
1161 }
1162
1163 fn unlock_package(&mut self, request: &mut Request, name: &str, arena: &mut Vec<Package>) {
1165 let skipped: Vec<usize> = self.skipped_load.get(name).cloned().unwrap_or_default();
1166 for idx in skipped {
1167 let replacer_name = arena[idx].name.clone();
1168 if replacer_name != name
1169 && self.skipped_load.contains_key(&replacer_name)
1170 && (request.update_allow_transitive_root_dependencies()
1171 || (!Self::is_root_require(request, name)
1172 && !Self::is_root_require(request, &replacer_name)))
1173 {
1174 self.unlock_package(request, &replacer_name, arena);
1175 if Self::is_root_require(request, &replacer_name) {
1176 self.mark_package_name_for_loading(
1177 request,
1178 &replacer_name,
1179 &Constraint::MatchAll,
1180 );
1181 } else {
1182 for loaded in self.loaded_packages_in_pool() {
1183 let c = arena[loaded]
1184 .requires
1185 .get(&replacer_name)
1186 .map(|l| l.constraint.clone());
1187 if let Some(c) = c {
1188 self.mark_package_name_for_loading(request, &replacer_name, &c);
1189 }
1190 }
1191 }
1192 }
1193 }
1194 if self.path_repo_unlocked.contains(name) {
1195 let entries: Vec<(usize, usize)> = self
1196 .packages
1197 .iter()
1198 .enumerate()
1199 .filter_map(|(i, p)| p.map(|idx| (i, idx)))
1200 .filter(|(_, idx)| arena[*idx].name == name)
1201 .collect();
1202 for (index, idx) in entries {
1203 self.remove_loaded_package(idx, index, arena);
1204 }
1205 }
1206 self.skipped_load.remove(name);
1207 self.loaded_packages.remove(name);
1208 self.max_extended_reqs.remove(name);
1209 self.path_repo_unlocked.remove(name);
1210 for locked_idx in request.locked_packages_all() {
1211 if !arena[locked_idx].is_alias() && arena[locked_idx].name == name {
1212 if let Some(index) = self.packages.iter().position(|p| *p == Some(locked_idx)) {
1213 request.unlock_package(locked_idx);
1214 self.remove_loaded_package(locked_idx, index, arena);
1215 for fixed_or_locked in request.fixed_or_locked_packages() {
1216 if fixed_or_locked == locked_idx {
1217 continue;
1218 }
1219 if self.skipped_load.contains_key(&arena[fixed_or_locked].name) {
1220 let locked_name = arena[locked_idx].name.clone();
1221 let requires = arena[fixed_or_locked].requires.clone();
1222 if let Some(link) = requires.get(&locked_name) {
1223 let c = link.constraint.clone();
1224 self.mark_package_name_for_loading(request, &locked_name, &c);
1225 }
1226 let replaces = arena[locked_idx].replaces.clone();
1227 for replace in replaces.iter() {
1228 if requires.get(&replace.target).is_some()
1229 && self.skipped_load.contains_key(&replace.target)
1230 {
1231 self.unlock_package(request, &replace.target, arena);
1232 self.mark_package_name_for_loading(
1233 request,
1234 &replace.target,
1235 &replace.constraint,
1236 );
1237 }
1238 }
1239 }
1240 }
1241 }
1242 }
1243 }
1244 }
1245
1246 fn mark_package_name_for_loading_if_required(
1248 &mut self,
1249 request: &Request,
1250 name: &str,
1251 arena: &[Package],
1252 ) {
1253 if let Some(c) = request.requires.get(name).cloned() {
1254 self.mark_package_name_for_loading(request, name, &c);
1255 }
1256 for loaded in self.loaded_packages_in_pool() {
1257 let links: Vec<Constraint> = arena[loaded]
1258 .requires
1259 .iter()
1260 .filter(|l| l.target == name)
1261 .map(|l| l.constraint.clone())
1262 .collect();
1263 for c in links {
1264 self.mark_package_name_for_loading(request, name, &c);
1265 }
1266 }
1267 }
1268
1269 fn remove_loaded_package(&mut self, idx: usize, index: usize, arena: &[Package]) {
1271 let repo_index = match arena[idx].origin {
1272 Origin::Repository(i) => Some(i),
1273 _ => None,
1274 };
1275 let mut forget = |p: &Package| {
1276 if let Some(r) = repo_index {
1277 if let Some(by_name) = self
1278 .loaded_per_repo
1279 .get_mut(&r)
1280 .and_then(|m| m.get_mut(&p.name))
1281 {
1282 by_name.remove(&p.version);
1283 }
1284 }
1285 };
1286 forget(&arena[idx]);
1287 self.packages[index] = None;
1288 if let Some(aliases) = self.alias_map.remove(&idx) {
1289 for (alias_index, alias_idx) in aliases {
1290 forget(&arena[alias_idx]);
1291 self.packages[alias_index] = None;
1292 }
1293 }
1294 }
1295}
1296
1297pub fn array_repository_load_packages(
1299 members: &[usize],
1300 package_name_map: &[(String, Constraint)],
1301 acceptable: &BTreeMap<String, i32>,
1302 flags: &BTreeMap<String, i32>,
1303 already_loaded: &BTreeMap<String, BTreeSet<String>>,
1304 arena: &[Package],
1305) -> (Vec<String>, Vec<usize>) {
1306 let mut result: Vec<usize> = Vec::new();
1307 let mut names_found: Vec<String> = Vec::new();
1308 for idx in members {
1309 let p = &arena[*idx];
1310 let Some((_, constraint)) = package_name_map.iter().find(|(n, _)| *n == p.name) else {
1311 continue;
1312 };
1313 let matches =
1314 matches!(constraint, Constraint::MatchAll) || constraint.matches_version(&p.version);
1315 if matches
1316 && is_package_acceptable(acceptable, flags, &p.names(true), p.stability)
1317 && !already_loaded
1318 .get(&p.name)
1319 .is_some_and(|s| s.contains(&p.version))
1320 {
1321 push_unique(&mut result, *idx);
1322 if let Some(base) = p.alias_of {
1323 push_unique(&mut result, base);
1324 }
1325 }
1326 if !names_found.contains(&p.name) {
1327 names_found.push(p.name.clone());
1328 }
1329 }
1330 for idx in members {
1331 if let Some(base) = arena[*idx].alias_of {
1332 if result.contains(&base) {
1333 push_unique(&mut result, *idx);
1334 }
1335 }
1336 }
1337 (names_found, result)
1338}
1339
1340pub fn set_source_dist_references(arena: &mut [Package], idx: usize, reference: &str) {
1344 static HOSTS: OnceLock<Regex> = OnceLock::new();
1345 static SHA: OnceLock<Regex> = OnceLock::new();
1346 let base = arena[idx].alias_of.unwrap_or(idx);
1347 let targets: Vec<usize> = (0..arena.len())
1348 .filter(|i| *i == base || arena[*i].alias_of == Some(base))
1349 .collect();
1350 for t in targets {
1351 let p = &mut arena[t];
1352 if let Some(s) = &mut p.source {
1353 s.reference = Some(reference.to_owned());
1354 }
1355 let dist_url = p.dist.as_ref().map(|d| d.url.clone());
1356 let hosts = regex(
1357 &HOSTS,
1358 r"^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/",
1359 true,
1360 );
1361 match dist_url {
1362 Some(url) if hosts.is_match(url.as_bytes()).unwrap_or(false) => {
1363 let sha = regex(&SHA, r"(?<=/|sha=)[a-f0-9]{40}(?=/|$)", true);
1364 let replaced = replace_all(sha, &url, reference);
1365 if let Some(d) = &mut p.dist {
1366 d.reference = Some(reference.to_owned());
1367 d.url = replaced;
1368 }
1369 }
1370 _ => {
1371 if let Some(d) = &mut p.dist {
1372 if d.reference
1373 .as_deref()
1374 .is_some_and(|r| !r.is_empty() && r != "0")
1375 {
1376 d.reference = Some(reference.to_owned());
1377 }
1378 }
1379 }
1380 }
1381 }
1382}
1383
1384fn replace_all(re: &Regex, subject: &str, replacement: &str) -> String {
1385 let mut out = String::new();
1386 let mut last = 0;
1387 for m in re.find_iter(subject.as_bytes()).flatten() {
1388 out.push_str(&subject[last..m.start()]);
1389 out.push_str(replacement);
1390 last = m.end();
1391 }
1392 out.push_str(&subject[last..]);
1393 out
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398 use super::*;
1399 use crate::package::{Link, LinkType};
1400 use serde_json::json;
1401
1402 #[test]
1403 fn match_looks_up_links_by_php_key() {
1404 let mut lib = Package::new("lib-libxslt", "1.1.35.0", "1.1.35", Origin::Platform);
1407 lib.replaces.insert(Link {
1408 key: Some("xsl".into()),
1409 source: "lib-libxslt".into(),
1410 target: "lib-xsl".into(),
1411 constraint: Constraint::new(crate::constraint::Op::Eq, "1.1.35.0"),
1412 pretty_constraint: "1.1.35".into(),
1413 kind: LinkType::Replace,
1414 });
1415 assert!(!Pool::matches(&lib, "lib-xsl", None));
1416 assert!(Pool::matches(&lib, "xsl", None));
1419 let pool = Pool::new(vec![0], Vec::new(), std::slice::from_ref(&lib));
1420 assert_eq!(
1421 pool.what_provides(std::slice::from_ref(&lib), "xsl", None),
1422 Vec::<usize>::new()
1423 );
1424 assert_eq!(
1425 pool.what_provides(std::slice::from_ref(&lib), "lib-xsl", None),
1426 Vec::<usize>::new()
1427 );
1428 let cfg = json!({"name": "acme/lib", "version": "dev-main", "default-branch": true,
1430 "replace": {"acme/old": "self.version"}});
1431 let mut arena = Vec::new();
1432 let ids =
1433 crate::loader::load_packages(&[cfg], Origin::Repository(0), &mut arena, false).unwrap();
1434 let alias = &arena[ids[0]];
1435 assert!(alias.replaces.has_numeric_keys());
1436 assert!(Pool::matches(
1437 alias,
1438 "acme/old",
1439 Some(&Constraint::new(crate::constraint::Op::Eq, "9999999-dev"))
1440 ));
1441 assert!(Pool::matches(
1442 alias,
1443 "acme/old",
1444 Some(&Constraint::new(crate::constraint::Op::Eq, "dev-main"))
1445 ));
1446 let base = &arena[ids[1]];
1447 assert!(!base.replaces.has_numeric_keys());
1448 assert!(!Pool::matches(
1449 base,
1450 "acme/old",
1451 Some(&Constraint::new(crate::constraint::Op::Eq, "9999999-dev"))
1452 ));
1453 let pool = Pool::new(vec![ids[1], ids[0]], Vec::new(), &arena);
1454 assert_eq!(pool.what_provides(&arena, "acme/old", None), vec![1, 2]);
1455 assert_eq!(
1456 pool.what_provides(&arena, "lib-xsl", None),
1457 Vec::<usize>::new()
1458 );
1459 }
1460
1461 #[test]
1462 fn transitive_dependencies_allowed_without_allow_list() {
1463 let r = Request::new(None);
1464 assert!(r.update_allow_transitive_dependencies());
1465 assert!(!r.update_allow_transitive_root_dependencies());
1466 }
1467}