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, TemporaryConstraint>,
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, TemporaryConstraint>,
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
301#[derive(Debug, Clone)]
305pub struct TemporaryConstraint {
306 pub pretty: String,
307 pub constraint: Constraint,
308}
309
310impl std::fmt::Display for TemporaryConstraint {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.write_str(&self.pretty)
313 }
314}
315
316pub type FilterListRemoved = BTreeMap<String, Vec<(String, Vec<crate::repository::FilterEntry>)>>;
319
320#[derive(Debug, Clone, Default)]
321pub struct Pool {
322 pub identity: u64,
325 pub packages: Vec<usize>,
327 id_of: HashMap<usize, usize>,
328 package_by_name: HashMap<String, Vec<usize>>,
329 pub unacceptable_fixed_or_locked: Vec<usize>,
330 pub warnings: Vec<String>,
331 pub flex_notice: Option<String>,
334 pub filter_list_removed: FilterListRemoved,
337 pub security_removed: BTreeMap<String, Vec<(String, Vec<String>)>>,
340 pub abandoned_removed: BTreeMap<String, BTreeMap<String, String>>,
342 pub removed_versions: BTreeMap<String, BTreeMap<String, String>>,
345 pub removed_versions_by_package: BTreeMap<usize, BTreeMap<String, String>>,
348}
349
350impl Pool {
351 pub fn new(packages: Vec<usize>, unacceptable: Vec<usize>, arena: &[Package]) -> Pool {
352 static NEXT_IDENTITY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
353 let mut pool = Pool {
354 flex_notice: None,
355 identity: NEXT_IDENTITY.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
356 packages: Vec::with_capacity(packages.len()),
357 id_of: HashMap::new(),
358 package_by_name: HashMap::new(),
359 unacceptable_fixed_or_locked: unacceptable,
360 warnings: Vec::new(),
361 filter_list_removed: BTreeMap::new(),
362 security_removed: BTreeMap::new(),
363 abandoned_removed: BTreeMap::new(),
364 removed_versions: BTreeMap::new(),
365 removed_versions_by_package: BTreeMap::new(),
366 };
367 for idx in packages {
368 pool.packages.push(idx);
369 let id = pool.packages.len();
370 pool.id_of.insert(idx, id);
371 for name in arena[idx].names(true) {
372 pool.package_by_name.entry(name).or_default().push(id);
373 }
374 }
375 pool
376 }
377
378 pub fn len(&self) -> usize {
379 self.packages.len()
380 }
381
382 pub fn is_empty(&self) -> bool {
383 self.packages.is_empty()
384 }
385
386 pub fn package_by_id(&self, id: usize) -> usize {
388 self.packages[id - 1]
389 }
390
391 pub fn id_of(&self, arena_idx: usize) -> Option<usize> {
392 self.id_of.get(&arena_idx).copied()
393 }
394
395 pub fn literal_to_package(&self, literal: i64) -> usize {
396 self.package_by_id(literal.unsigned_abs() as usize)
397 }
398
399 pub fn what_provides(
401 &self,
402 arena: &[Package],
403 name: &str,
404 constraint: Option<&Constraint>,
405 ) -> Vec<usize> {
406 let Some(candidates) = self.package_by_name.get(name) else {
407 return Vec::new();
408 };
409 candidates
410 .iter()
411 .copied()
412 .filter(|id| Self::matches(&arena[self.packages[id - 1]], name, constraint))
413 .collect()
414 }
415
416 pub fn matches(candidate: &Package, name: &str, constraint: Option<&Constraint>) -> bool {
418 if candidate.name == name {
419 return match constraint {
420 None => true,
421 Some(c) => c.matches_version(&candidate.version),
422 };
423 }
424 let provides = &candidate.provides;
425 let replaces = &candidate.replaces;
426 if replaces.has_numeric_keys() || provides.has_numeric_keys() {
431 for link in provides.iter().chain(replaces.iter()) {
432 if link.target == name && constraint.is_none_or(|c| c.matches(&link.constraint)) {
433 return true;
434 }
435 }
436 return false;
437 }
438 if let Some(link) = provides.get(name) {
439 if constraint.is_none_or(|c| c.matches(&link.constraint)) {
440 return true;
441 }
442 }
443 if let Some(link) = replaces.get(name) {
444 if constraint.is_none_or(|c| c.matches(&link.constraint)) {
445 return true;
446 }
447 }
448 false
449 }
450
451 pub fn is_filter_list_removed(&self, name: &str, version: &str) -> bool {
453 self.filter_list_removed
454 .get(name)
455 .is_some_and(|versions| versions.iter().any(|(v, _)| v == version))
456 }
457
458 pub fn with_packages(&self, kept: Vec<usize>, arena: &[Package]) -> Pool {
461 let mut pool = Pool::new(kept, self.unacceptable_fixed_or_locked.clone(), arena);
462 pool.warnings = self.warnings.clone();
463 pool.filter_list_removed = self.filter_list_removed.clone();
464 pool.security_removed = self.security_removed.clone();
465 pool.abandoned_removed = self.abandoned_removed.clone();
466 pool.removed_versions = self.removed_versions.clone();
467 pool.removed_versions_by_package = self.removed_versions_by_package.clone();
468 pool
469 }
470
471 pub fn is_security_removed(&self, name: &str, constraint: &Constraint) -> bool {
473 !self.security_advisory_ids(name, constraint).is_empty()
474 }
475
476 pub fn security_advisory_ids(&self, name: &str, constraint: &Constraint) -> Vec<String> {
479 self.security_removed
480 .get(name)
481 .and_then(|m| {
482 m.iter()
483 .find(|(v, _)| constraint.matches(&Constraint::new(Op::Eq, v.clone())))
484 .map(|(_, ids)| ids.clone())
485 })
486 .unwrap_or_default()
487 }
488
489 pub fn is_abandoned_removed(&self, name: &str, constraint: &Constraint) -> bool {
491 self.abandoned_removed.get(name).is_some_and(|m| {
492 m.keys()
493 .any(|v| constraint.matches(&Constraint::new(Op::Eq, v.clone())))
494 })
495 }
496
497 pub fn is_filter_list_removed_version(&self, name: &str, constraint: &Constraint) -> bool {
499 self.filter_list_removed.get(name).is_some_and(|versions| {
500 versions
501 .iter()
502 .any(|(v, _)| constraint.matches(&Constraint::new(Op::Eq, v.clone())))
503 })
504 }
505
506 pub fn filter_list_entries_text(
509 &self,
510 name: &str,
511 constraint: &Constraint,
512 ) -> Vec<(String, String)> {
513 let mut lists: Vec<(String, Vec<String>)> = Vec::new();
514 let mut seen: Vec<&crate::repository::FilterEntry> = Vec::new();
517 if let Some(versions) = self.filter_list_removed.get(name) {
518 for (v, entries) in versions {
519 if !constraint.matches(&Constraint::new(Op::Eq, v.clone())) {
520 continue;
521 }
522 for e in entries {
523 if seen.iter().any(|s| s.same_entry(e)) {
524 continue;
525 }
526 seen.push(e);
527 let source = e
528 .source
529 .as_deref()
530 .filter(|s| !s.is_empty() && *s != "0")
531 .map(|s| format!(" reported by {s}"))
532 .unwrap_or_default();
533 let url = e
534 .url
535 .as_deref()
536 .filter(|s| !s.is_empty() && *s != "0")
537 .map(|s| format!(" (see {s})"))
538 .unwrap_or_default();
539 let reason = e
540 .reason
541 .as_deref()
542 .filter(|s| !s.is_empty() && *s != "0")
543 .map(|s| format!(" reason: {s}"))
544 .unwrap_or_default();
545 let text = format!("{source}{url}{reason}");
546 match lists.iter_mut().find(|(l, _)| *l == e.list_name) {
547 Some((_, texts)) => texts.push(text),
548 None => lists.push((e.list_name.clone(), vec![text])),
549 }
550 }
551 }
552 }
553 lists
554 .into_iter()
555 .map(|(list, texts)| {
556 let action = if list == "malware" {
557 "flagged as "
558 } else {
559 "filtered by "
560 };
561 (list.clone(), format!("{action}{list}{}", texts.join(", ")))
562 })
563 .collect()
564 }
565
566 pub fn is_unacceptable_fixed_or_locked(&self, arena_idx: usize) -> bool {
567 self.unacceptable_fixed_or_locked.contains(&arena_idx)
568 }
569
570 pub fn removed_versions(&self, name: &str, constraint: &Constraint) -> Vec<(String, String)> {
573 let Some(versions) = self.removed_versions.get(name) else {
574 return Vec::new();
575 };
576 versions
577 .iter()
578 .filter(|(v, _)| constraint.matches(&Constraint::new(Op::Eq, (*v).clone())))
579 .map(|(v, p)| (v.clone(), p.clone()))
580 .collect()
581 }
582
583 pub fn removed_versions_by_package(&self, arena_idx: usize) -> Vec<(String, String)> {
585 self.removed_versions_by_package
586 .get(&arena_idx)
587 .map(|m| m.iter().map(|(v, p)| (v.clone(), p.clone())).collect())
588 .unwrap_or_default()
589 }
590}
591
592const LOAD_BATCH_SIZE: usize = 50;
593
594pub fn package_name_regexp(pattern: &str) -> Regex {
596 let quoted = crate::version::preg_quote(pattern).replace("\\*", ".*");
597 pcre2::bytes::RegexBuilder::new()
598 .caseless(true)
599 .build(&format!("^{quoted}$"))
600 .unwrap_or_else(|e| panic!("pattern {pattern}: {e}"))
601}
602
603struct PoolBuilder<'a> {
604 set: &'a RepositorySet,
605 alias_map: HashMap<usize, Vec<(usize, usize)>>,
607 packages_to_load: OrderedMap<Constraint>,
608 loaded_packages: BTreeMap<String, Constraint>,
609 loaded_per_repo: BTreeMap<usize, BTreeMap<String, BTreeSet<String>>>,
610 packages: Vec<Option<usize>>,
612 unacceptable: Vec<usize>,
613 update_allow_list: Vec<String>,
614 update_allow_patterns: Vec<Regex>,
615 skipped_load: BTreeMap<String, Vec<usize>>,
616 ignored_types: Vec<String>,
617 allowed_types: Option<Vec<String>>,
618 restricted: Option<BTreeSet<String>>,
619 path_repo_unlocked: BTreeSet<String>,
620 max_extended_reqs: BTreeSet<String>,
621 update_allow_warned: BTreeSet<String>,
622 warnings: Vec<String>,
623}
624
625impl<'a> PoolBuilder<'a> {
626 fn new(set: &'a RepositorySet) -> PoolBuilder<'a> {
627 PoolBuilder {
628 set,
629 alias_map: HashMap::new(),
630 packages_to_load: OrderedMap::default(),
631 loaded_packages: BTreeMap::new(),
632 loaded_per_repo: BTreeMap::new(),
633 packages: Vec::new(),
634 unacceptable: Vec::new(),
635 update_allow_list: Vec::new(),
636 update_allow_patterns: Vec::new(),
637 skipped_load: BTreeMap::new(),
638 ignored_types: Vec::new(),
639 allowed_types: None,
640 restricted: None,
641 path_repo_unlocked: BTreeSet::new(),
642 max_extended_reqs: BTreeSet::new(),
643 update_allow_warned: BTreeSet::new(),
644 warnings: Vec::new(),
645 }
646 }
647
648 fn loaded_packages_in_pool(&self) -> Vec<usize> {
649 self.packages.iter().flatten().copied().collect()
650 }
651
652 fn build_pool(
653 &mut self,
654 repositories: &[Repository],
655 request: &mut Request,
656 arena: &mut Vec<Package>,
657 pre_pool: Option<&PrePoolFilter>,
658 ) -> Result<Pool, PoolError> {
659 self.restricted = request
660 .restricted_packages
661 .as_ref()
662 .map(|l| l.iter().cloned().collect());
663
664 if !request.update_allow_list.is_empty() {
665 self.update_allow_list = request.update_allow_list.clone();
666 self.update_allow_patterns = self
667 .update_allow_list
668 .iter()
669 .map(|p| package_name_regexp(p))
670 .collect();
671 self.warn_about_non_matching_update_allow_list(request, arena)?;
672 let Some(locked) = request.locked_repository.clone() else {
673 return Err(PoolError(
674 "No lock repo present and yet a partial update was requested.".into(),
675 ));
676 };
677 for locked_idx in locked {
678 if !self.is_update_allowed(&arena[locked_idx]) {
679 let p = &arena[locked_idx];
680 self.skipped_load
681 .entry(p.name.clone())
682 .or_default()
683 .push(locked_idx);
684 for link in p.replaces.iter() {
685 self.skipped_load
686 .entry(link.target.clone())
687 .or_default()
688 .push(locked_idx);
689 }
690 if p.dist.as_ref().is_some_and(|d| d.kind == "path") {
691 let symlink = p
692 .raw
693 .get("transport-options")
694 .and_then(|t| t.get("symlink"))
695 .cloned();
696 if symlink != Some(serde_json::Value::Bool(false)) {
697 self.path_repo_unlocked.insert(p.name.clone());
698 continue;
699 }
700 }
701 request.lock_package(locked_idx);
702 }
703 }
704 }
705
706 for idx in request.fixed_or_locked_packages() {
707 let (name, replaces, names, stability, origin) = {
708 let p = &arena[idx];
709 (
710 p.name.clone(),
711 p.replaces
712 .iter()
713 .map(|l| l.target.clone())
714 .collect::<Vec<_>>(),
715 p.names(true),
716 p.stability,
717 p.origin,
718 )
719 };
720 self.loaded_packages.insert(name, Constraint::MatchAll);
721 for target in replaces {
722 self.loaded_packages.insert(target, Constraint::MatchAll);
723 }
724 if matches!(origin, Origin::Root | Origin::Platform)
725 || is_package_acceptable(
726 &self.set.acceptable_stabilities,
727 &self.set.stability_flags,
728 &names,
729 stability,
730 )
731 {
732 self.load_package(request, idx, false, arena);
733 } else {
734 self.unacceptable.push(idx);
735 }
736 }
737
738 let requires: Vec<(String, Constraint)> = request.requires.0.clone();
739 for (name, constraint) in &requires {
740 if self.loaded_packages.contains_key(name) {
741 continue;
742 }
743 self.packages_to_load.insert(name, constraint.clone());
744 self.max_extended_reqs.insert(name.clone());
745 }
746 let already: Vec<String> = self
747 .packages_to_load
748 .keys()
749 .filter(|n| self.loaded_packages.contains_key(*n))
750 .cloned()
751 .collect();
752 for name in already {
753 self.packages_to_load.remove(&name);
754 }
755
756 while !self.packages_to_load.is_empty() {
757 self.load_packages_marked_for_loading(request, repositories, arena)?;
758 }
759
760 if !self.set.temporary_constraints.is_empty() {
761 let entries: Vec<(usize, usize)> = self
762 .packages
763 .iter()
764 .enumerate()
765 .filter_map(|(i, p)| p.map(|idx| (i, idx)))
766 .collect();
767 for (i, idx) in entries {
768 if arena[idx].is_alias() {
769 continue;
770 }
771 for name in arena[idx].names(true) {
772 let Some(constraint) = self.set.temporary_constraints.get(&name) else {
773 continue;
774 };
775 let mut package_and_aliases: Vec<(usize, usize)> = vec![(i, idx)];
776 if let Some(aliases) = self.alias_map.get(&idx) {
777 package_and_aliases.extend(aliases.iter().copied());
778 }
779 let found = package_and_aliases
780 .iter()
781 .any(|(_, p)| constraint.constraint.matches_version(&arena[*p].version));
782 if !found {
783 for (pool_index, _) in package_and_aliases {
784 self.packages[pool_index] = None;
785 }
786 }
787 }
788 }
789 }
790
791 let mut packages = self.loaded_packages_in_pool();
792 let mut flex_notice = None;
795 if let Some(f) = pre_pool {
796 let mut locked: BTreeMap<String, Vec<String>> = BTreeMap::new();
797 for idx in request.fixed_or_locked_packages() {
798 let p = &arena[idx];
799 let entry = locked.entry(p.name.clone()).or_default();
800 entry.push(p.version.clone());
801 if let Some(base) = p.alias_of {
802 entry.push(arena[base].version.clone());
803 }
804 }
805 let (kept, restricting) = crate::flex_filter::remove_legacy_packages(
806 &packages,
807 arena,
808 &f.root_constraints,
809 &locked,
810 &f.symfony,
811 &f.versions,
812 );
813 if std::env::var_os("VIVACITY_TRACE").is_some() {
814 eprintln!(
815 "trace: flex filter {} → {} package versions (require {}, {} splits)",
816 packages.len(),
817 kept.len(),
818 f.symfony_require,
819 f.versions.splits.len()
820 );
821 }
822 packages = kept;
823 if restricting {
824 flex_notice = Some(format!(
825 "Restricting packages listed in \"symfony/symfony\" to \"{}\"",
826 f.symfony_require
827 ));
828 }
829 }
830 let mut pool = Pool::new(packages, std::mem::take(&mut self.unacceptable), arena);
831 pool.warnings = std::mem::take(&mut self.warnings);
832 pool.flex_notice = flex_notice;
833 Ok(pool)
834 }
835
836 fn mark_package_name_for_loading(
838 &mut self,
839 request: &Request,
840 name: &str,
841 constraint: &Constraint,
842 ) {
843 if is_platform_package(name) {
844 return;
845 }
846 if self.max_extended_reqs.contains(name) {
847 return;
848 }
849 let mut constraint = constraint.clone();
850 if let Some(root) = request.requires.get(name) {
851 if !intervals::is_subset_of(&constraint, root) {
852 constraint = root.clone();
853 }
854 }
855 if !self.loaded_packages.contains_key(name) {
856 if let Some(pending) = self.packages_to_load.get(name) {
857 if intervals::is_subset_of(&constraint, pending) {
858 return;
859 }
860 constraint = intervals::compact_constraint(&Constraint::create(
861 vec![pending.clone(), constraint],
862 false,
863 ));
864 }
865 self.packages_to_load.insert(name, constraint);
866 return;
867 }
868 let loaded = &self.loaded_packages[name];
869 if intervals::is_subset_of(&constraint, loaded) {
870 return;
871 }
872 let merged = intervals::compact_constraint(&Constraint::create(
873 vec![loaded.clone(), constraint],
874 false,
875 ));
876 self.packages_to_load.insert(name, merged);
877 self.loaded_packages.remove(name);
878 }
879
880 fn load_packages_marked_for_loading(
882 &mut self,
883 request: &mut Request,
884 repositories: &[Repository],
885 arena: &mut Vec<Package>,
886 ) -> Result<(), PoolError> {
887 let names: Vec<String> = self.packages_to_load.keys().cloned().collect();
888 for name in names {
889 if let Some(restricted) = &self.restricted {
890 if !restricted.contains(&name) {
891 self.packages_to_load.remove(&name);
892 continue;
893 }
894 }
895 let c = self
896 .packages_to_load
897 .get(&name)
898 .cloned()
899 .unwrap_or(Constraint::MatchAll);
900 self.loaded_packages.insert(name, c);
901 }
902 let mut remaining: Vec<(String, Constraint)> = std::mem::take(&mut self.packages_to_load).0;
903 for (repo_index, repository) in repositories.iter().enumerate() {
904 if matches!(repository, Repository::Platform(_) | Repository::Locked(_)) {
905 continue;
906 }
907 if remaining.is_empty() {
908 break;
909 }
910 let batches: Vec<Vec<(String, Constraint)>> = remaining
911 .chunks(LOAD_BATCH_SIZE)
912 .map(|c| c.to_vec())
913 .collect();
914 let mut kept: Vec<Vec<(String, Constraint)>> = Vec::new();
915 for batch in batches {
916 let empty = BTreeMap::new();
917 let already = self.loaded_per_repo.get(&repo_index).unwrap_or(&empty);
918 let (names_found, ids) = match repository {
919 Repository::Composer(repo) => repo.load_packages(
920 &batch,
921 &self.set.acceptable_stabilities,
922 &self.set.stability_flags,
923 already,
924 Origin::Repository(repo_index),
925 arena,
926 )?,
927 Repository::Root(members) => array_repository_load_packages(
928 members,
929 &batch,
930 &self.set.acceptable_stabilities,
931 &self.set.stability_flags,
932 already,
933 arena,
934 ),
935 Repository::Path(repo) => array_repository_load_packages(
936 &repo.members,
937 &batch,
938 &self.set.acceptable_stabilities,
939 &self.set.stability_flags,
940 already,
941 arena,
942 ),
943 Repository::Platform(_) | Repository::Locked(_) => unreachable!(),
944 };
945 let mut batch = batch;
946 batch.retain(|(n, _)| !names_found.contains(n));
947 kept.push(batch);
948 for idx in ids {
949 let (name, version, package_type) = {
950 let p = &arena[idx];
951 (p.name.clone(), p.version.clone(), p.package_type.clone())
952 };
953 self.loaded_per_repo
954 .entry(repo_index)
955 .or_default()
956 .entry(name.clone())
957 .or_default()
958 .insert(version);
959 if self.ignored_types.contains(&package_type)
960 || self
961 .allowed_types
962 .as_ref()
963 .is_some_and(|a| !a.contains(&package_type))
964 {
965 continue;
966 }
967 let propagate = !self.path_repo_unlocked.contains(&name);
968 self.load_package(request, idx, propagate, arena);
969 }
970 }
971 remaining = kept.into_iter().flatten().collect();
972 }
973 Ok(())
974 }
975
976 fn load_package(
978 &mut self,
979 request: &mut Request,
980 idx: usize,
981 propagate_update: bool,
982 arena: &mut Vec<Package>,
983 ) {
984 let index = self.packages.len();
985 self.packages.push(Some(idx));
986 if let Some(base) = arena[idx].alias_of {
987 self.alias_map.entry(base).or_default().push((index, idx));
988 }
989 let name = arena[idx].name.clone();
990 if let Some(reference) = self.set.root_references.get(&name) {
991 if !request.is_locked_package(idx) && !request.is_fixed_package(idx) {
992 set_source_dist_references(arena, idx, reference);
993 }
994 }
995 if propagate_update || self.path_repo_unlocked.contains(&name) {
996 let version = arena[idx].version.clone();
997 if let Some((alias, alias_normalized)) = self
998 .set
999 .root_aliases
1000 .get(&name)
1001 .and_then(|m| m.get(&version))
1002 {
1003 let base = arena[idx].alias_of.unwrap_or(idx);
1004 let mut alias_package = arena[base].alias(base, alias_normalized, alias);
1005 alias_package.root_package_alias = true;
1006 alias_package.origin = Origin::Detached;
1007 arena.push(alias_package);
1008 let alias_idx = arena.len() - 1;
1009 let new_index = self.packages.len();
1010 self.packages.push(Some(alias_idx));
1011 self.alias_map
1012 .entry(base)
1013 .or_default()
1014 .push((new_index, alias_idx));
1015 }
1016 }
1017 let requires: Vec<(String, Constraint)> = arena[idx]
1018 .requires
1019 .iter()
1020 .map(|l| (l.target.clone(), l.constraint.clone()))
1021 .collect();
1022 for (require, link_constraint) in requires {
1023 if self.skipped_load.contains_key(&require) {
1024 if propagate_update && request.update_allow_transitive_dependencies() {
1025 let skipped_root_requires =
1026 self.skipped_root_requires(request, &require, arena);
1027 if request.update_allow_transitive_root_dependencies()
1028 || skipped_root_requires.is_empty()
1029 {
1030 self.unlock_package(request, &require, arena);
1031 self.mark_package_name_for_loading(request, &require, &link_constraint);
1032 } else {
1033 self.warn_root_requires(&skipped_root_requires);
1034 }
1035 } else if self.path_repo_unlocked.contains(&require)
1036 && !self.loaded_packages.contains_key(&require)
1037 {
1038 self.mark_package_name_for_loading(request, &require, &link_constraint);
1039 }
1040 } else {
1041 self.mark_package_name_for_loading(request, &require, &link_constraint);
1042 }
1043 }
1044 if propagate_update && request.update_allow_transitive_dependencies() {
1045 let replaces: Vec<String> = arena[idx]
1046 .replaces
1047 .iter()
1048 .map(|l| l.target.clone())
1049 .collect();
1050 for replace in replaces {
1051 if self.loaded_packages.contains_key(&replace)
1052 && self.skipped_load.contains_key(&replace)
1053 {
1054 let skipped_root_requires =
1055 self.skipped_root_requires(request, &replace, arena);
1056 if request.update_allow_transitive_root_dependencies()
1057 || skipped_root_requires.is_empty()
1058 {
1059 self.unlock_package(request, &replace, arena);
1060 self.mark_package_name_for_loading_if_required(request, &replace, arena);
1061 } else {
1062 self.warn_root_requires(&skipped_root_requires);
1063 }
1064 }
1065 }
1066 }
1067 }
1068
1069 fn warn_root_requires(&mut self, root_requires: &[String]) {
1070 for root_require in root_requires {
1071 if self.update_allow_warned.insert(root_require.clone()) {
1072 self.warnings.push(format!(
1073 "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."
1074 ));
1075 }
1076 }
1077 }
1078
1079 fn is_root_require(request: &Request, name: &str) -> bool {
1080 request.requires.contains(name)
1081 }
1082
1083 fn skipped_root_requires(
1085 &self,
1086 request: &Request,
1087 name: &str,
1088 arena: &[Package],
1089 ) -> Vec<String> {
1090 let Some(skipped) = self.skipped_load.get(name) else {
1091 return Vec::new();
1092 };
1093 if request.requires.contains(name) {
1094 return skipped
1095 .iter()
1096 .map(|idx| {
1097 let p = &arena[*idx];
1098 if p.name != name {
1099 format!("{} (via replace of {name})", p.name)
1100 } else {
1101 p.name.clone()
1102 }
1103 })
1104 .collect();
1105 }
1106 let mut matches = Vec::new();
1107 for idx in skipped {
1108 let p = &arena[*idx];
1109 if request.requires.contains(&p.name) {
1110 matches.push(p.name.clone());
1111 }
1112 for link in p.replaces.iter() {
1113 if request.requires.contains(&link.target) {
1114 if p.name != name {
1115 matches.push(format!("{} (via replace of {name})", p.name));
1116 } else {
1117 matches.push(p.name.clone());
1118 }
1119 break;
1120 }
1121 }
1122 }
1123 matches
1124 }
1125
1126 fn is_update_allowed(&self, package: &Package) -> bool {
1128 self.update_allow_patterns
1129 .iter()
1130 .any(|re| re.is_match(package.name.as_bytes()).unwrap_or(false))
1131 }
1132
1133 fn warn_about_non_matching_update_allow_list(
1135 &mut self,
1136 request: &Request,
1137 arena: &[Package],
1138 ) -> Result<(), PoolError> {
1139 let Some(locked) = &request.locked_repository else {
1140 return Err(PoolError(
1141 "No lock repo present and yet a partial update was requested.".into(),
1142 ));
1143 };
1144 'patterns: for pattern in &self.update_allow_list.clone() {
1145 let mut matched_platform = false;
1146 let re = package_name_regexp(pattern);
1147 for idx in locked {
1148 if re.is_match(arena[*idx].name.as_bytes()).unwrap_or(false) {
1149 continue 'patterns;
1150 }
1151 }
1152 for name in request.requires.keys() {
1153 if re.is_match(name.as_bytes()).unwrap_or(false) {
1154 if is_platform_package(name) {
1155 matched_platform = true;
1156 continue;
1157 }
1158 continue 'patterns;
1159 }
1160 }
1161 if matched_platform {
1162 self.warnings.push(format!(
1163 "Pattern \"{pattern}\" listed for update matches platform packages, but these cannot be updated by Composer."
1164 ));
1165 } else if pattern.contains('*') {
1166 self.warnings.push(format!(
1167 "Pattern \"{pattern}\" listed for update does not match any locked packages."
1168 ));
1169 } else {
1170 self.warnings.push(format!(
1171 "Package \"{pattern}\" listed for update is not locked."
1172 ));
1173 }
1174 }
1175 Ok(())
1176 }
1177
1178 fn unlock_package(&mut self, request: &mut Request, name: &str, arena: &mut Vec<Package>) {
1180 let skipped: Vec<usize> = self.skipped_load.get(name).cloned().unwrap_or_default();
1181 for idx in skipped {
1182 let replacer_name = arena[idx].name.clone();
1183 if replacer_name != name
1184 && self.skipped_load.contains_key(&replacer_name)
1185 && (request.update_allow_transitive_root_dependencies()
1186 || (!Self::is_root_require(request, name)
1187 && !Self::is_root_require(request, &replacer_name)))
1188 {
1189 self.unlock_package(request, &replacer_name, arena);
1190 if Self::is_root_require(request, &replacer_name) {
1191 self.mark_package_name_for_loading(
1192 request,
1193 &replacer_name,
1194 &Constraint::MatchAll,
1195 );
1196 } else {
1197 for loaded in self.loaded_packages_in_pool() {
1198 let c = arena[loaded]
1199 .requires
1200 .get(&replacer_name)
1201 .map(|l| l.constraint.clone());
1202 if let Some(c) = c {
1203 self.mark_package_name_for_loading(request, &replacer_name, &c);
1204 }
1205 }
1206 }
1207 }
1208 }
1209 if self.path_repo_unlocked.contains(name) {
1210 let entries: Vec<(usize, usize)> = self
1211 .packages
1212 .iter()
1213 .enumerate()
1214 .filter_map(|(i, p)| p.map(|idx| (i, idx)))
1215 .filter(|(_, idx)| arena[*idx].name == name)
1216 .collect();
1217 for (index, idx) in entries {
1218 self.remove_loaded_package(idx, index, arena);
1219 }
1220 }
1221 self.skipped_load.remove(name);
1222 self.loaded_packages.remove(name);
1223 self.max_extended_reqs.remove(name);
1224 self.path_repo_unlocked.remove(name);
1225 for locked_idx in request.locked_packages_all() {
1226 if !arena[locked_idx].is_alias() && arena[locked_idx].name == name {
1227 if let Some(index) = self.packages.iter().position(|p| *p == Some(locked_idx)) {
1228 request.unlock_package(locked_idx);
1229 self.remove_loaded_package(locked_idx, index, arena);
1230 for fixed_or_locked in request.fixed_or_locked_packages() {
1231 if fixed_or_locked == locked_idx {
1232 continue;
1233 }
1234 if self.skipped_load.contains_key(&arena[fixed_or_locked].name) {
1235 let locked_name = arena[locked_idx].name.clone();
1236 let requires = arena[fixed_or_locked].requires.clone();
1237 if let Some(link) = requires.get(&locked_name) {
1238 let c = link.constraint.clone();
1239 self.mark_package_name_for_loading(request, &locked_name, &c);
1240 }
1241 let replaces = arena[locked_idx].replaces.clone();
1242 for replace in replaces.iter() {
1243 if requires.get(&replace.target).is_some()
1244 && self.skipped_load.contains_key(&replace.target)
1245 {
1246 self.unlock_package(request, &replace.target, arena);
1247 self.mark_package_name_for_loading(
1248 request,
1249 &replace.target,
1250 &replace.constraint,
1251 );
1252 }
1253 }
1254 }
1255 }
1256 }
1257 }
1258 }
1259 }
1260
1261 fn mark_package_name_for_loading_if_required(
1263 &mut self,
1264 request: &Request,
1265 name: &str,
1266 arena: &[Package],
1267 ) {
1268 if let Some(c) = request.requires.get(name).cloned() {
1269 self.mark_package_name_for_loading(request, name, &c);
1270 }
1271 for loaded in self.loaded_packages_in_pool() {
1272 let links: Vec<Constraint> = arena[loaded]
1273 .requires
1274 .iter()
1275 .filter(|l| l.target == name)
1276 .map(|l| l.constraint.clone())
1277 .collect();
1278 for c in links {
1279 self.mark_package_name_for_loading(request, name, &c);
1280 }
1281 }
1282 }
1283
1284 fn remove_loaded_package(&mut self, idx: usize, index: usize, arena: &[Package]) {
1286 let repo_index = match arena[idx].origin {
1287 Origin::Repository(i) => Some(i),
1288 _ => None,
1289 };
1290 let mut forget = |p: &Package| {
1291 if let Some(r) = repo_index {
1292 if let Some(by_name) = self
1293 .loaded_per_repo
1294 .get_mut(&r)
1295 .and_then(|m| m.get_mut(&p.name))
1296 {
1297 by_name.remove(&p.version);
1298 }
1299 }
1300 };
1301 forget(&arena[idx]);
1302 self.packages[index] = None;
1303 if let Some(aliases) = self.alias_map.remove(&idx) {
1304 for (alias_index, alias_idx) in aliases {
1305 forget(&arena[alias_idx]);
1306 self.packages[alias_index] = None;
1307 }
1308 }
1309 }
1310}
1311
1312pub fn array_repository_load_packages(
1314 members: &[usize],
1315 package_name_map: &[(String, Constraint)],
1316 acceptable: &BTreeMap<String, i32>,
1317 flags: &BTreeMap<String, i32>,
1318 already_loaded: &BTreeMap<String, BTreeSet<String>>,
1319 arena: &[Package],
1320) -> (Vec<String>, Vec<usize>) {
1321 let mut result: Vec<usize> = Vec::new();
1322 let mut names_found: Vec<String> = Vec::new();
1323 for idx in members {
1324 let p = &arena[*idx];
1325 let Some((_, constraint)) = package_name_map.iter().find(|(n, _)| *n == p.name) else {
1326 continue;
1327 };
1328 let matches =
1329 matches!(constraint, Constraint::MatchAll) || constraint.matches_version(&p.version);
1330 if matches
1331 && is_package_acceptable(acceptable, flags, &p.names(true), p.stability)
1332 && !already_loaded
1333 .get(&p.name)
1334 .is_some_and(|s| s.contains(&p.version))
1335 {
1336 push_unique(&mut result, *idx);
1337 if let Some(base) = p.alias_of {
1338 push_unique(&mut result, base);
1339 }
1340 }
1341 if !names_found.contains(&p.name) {
1342 names_found.push(p.name.clone());
1343 }
1344 }
1345 for idx in members {
1346 if let Some(base) = arena[*idx].alias_of {
1347 if result.contains(&base) {
1348 push_unique(&mut result, *idx);
1349 }
1350 }
1351 }
1352 (names_found, result)
1353}
1354
1355pub fn set_source_dist_references(arena: &mut [Package], idx: usize, reference: &str) {
1359 static HOSTS: OnceLock<Regex> = OnceLock::new();
1360 static SHA: OnceLock<Regex> = OnceLock::new();
1361 let base = arena[idx].alias_of.unwrap_or(idx);
1362 let targets: Vec<usize> = (0..arena.len())
1363 .filter(|i| *i == base || arena[*i].alias_of == Some(base))
1364 .collect();
1365 for t in targets {
1366 let p = &mut arena[t];
1367 if let Some(s) = &mut p.source {
1368 s.reference = Some(reference.to_owned());
1369 }
1370 let dist_url = p.dist.as_ref().map(|d| d.url.clone());
1371 let hosts = regex(
1372 &HOSTS,
1373 r"^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/",
1374 true,
1375 );
1376 match dist_url {
1377 Some(url) if hosts.is_match(url.as_bytes()).unwrap_or(false) => {
1378 let sha = regex(&SHA, r"(?<=/|sha=)[a-f0-9]{40}(?=/|$)", true);
1379 let replaced = replace_all(sha, &url, reference);
1380 if let Some(d) = &mut p.dist {
1381 d.reference = Some(reference.to_owned());
1382 d.url = replaced;
1383 }
1384 }
1385 _ => {
1386 if let Some(d) = &mut p.dist {
1387 if d.reference
1388 .as_deref()
1389 .is_some_and(|r| !r.is_empty() && r != "0")
1390 {
1391 d.reference = Some(reference.to_owned());
1392 }
1393 }
1394 }
1395 }
1396 }
1397}
1398
1399fn replace_all(re: &Regex, subject: &str, replacement: &str) -> String {
1400 let mut out = String::new();
1401 let mut last = 0;
1402 for m in re.find_iter(subject.as_bytes()).flatten() {
1403 out.push_str(&subject[last..m.start()]);
1404 out.push_str(replacement);
1405 last = m.end();
1406 }
1407 out.push_str(&subject[last..]);
1408 out
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use super::*;
1414 use crate::package::{Link, LinkType};
1415 use serde_json::json;
1416
1417 #[test]
1418 fn match_looks_up_links_by_php_key() {
1419 let mut lib = Package::new("lib-libxslt", "1.1.35.0", "1.1.35", Origin::Platform);
1422 lib.replaces.insert(Link {
1423 key: Some("xsl".into()),
1424 source: "lib-libxslt".into(),
1425 target: "lib-xsl".into(),
1426 constraint: Constraint::new(crate::constraint::Op::Eq, "1.1.35.0"),
1427 pretty_constraint: "1.1.35".into(),
1428 kind: LinkType::Replace,
1429 });
1430 assert!(!Pool::matches(&lib, "lib-xsl", None));
1431 assert!(Pool::matches(&lib, "xsl", None));
1434 let pool = Pool::new(vec![0], Vec::new(), std::slice::from_ref(&lib));
1435 assert_eq!(
1436 pool.what_provides(std::slice::from_ref(&lib), "xsl", None),
1437 Vec::<usize>::new()
1438 );
1439 assert_eq!(
1440 pool.what_provides(std::slice::from_ref(&lib), "lib-xsl", None),
1441 Vec::<usize>::new()
1442 );
1443 let cfg = json!({"name": "acme/lib", "version": "dev-main", "default-branch": true,
1445 "replace": {"acme/old": "self.version"}});
1446 let mut arena = Vec::new();
1447 let ids =
1448 crate::loader::load_packages(&[cfg], Origin::Repository(0), &mut arena, false).unwrap();
1449 let alias = &arena[ids[0]];
1450 assert!(alias.replaces.has_numeric_keys());
1451 assert!(Pool::matches(
1452 alias,
1453 "acme/old",
1454 Some(&Constraint::new(crate::constraint::Op::Eq, "9999999-dev"))
1455 ));
1456 assert!(Pool::matches(
1457 alias,
1458 "acme/old",
1459 Some(&Constraint::new(crate::constraint::Op::Eq, "dev-main"))
1460 ));
1461 let base = &arena[ids[1]];
1462 assert!(!base.replaces.has_numeric_keys());
1463 assert!(!Pool::matches(
1464 base,
1465 "acme/old",
1466 Some(&Constraint::new(crate::constraint::Op::Eq, "9999999-dev"))
1467 ));
1468 let pool = Pool::new(vec![ids[1], ids[0]], Vec::new(), &arena);
1469 assert_eq!(pool.what_provides(&arena, "acme/old", None), vec![1, 2]);
1470 assert_eq!(
1471 pool.what_provides(&arena, "lib-xsl", None),
1472 Vec::<usize>::new()
1473 );
1474 }
1475
1476 #[test]
1477 fn transitive_dependencies_allowed_without_allow_list() {
1478 let r = Request::new(None);
1479 assert!(r.update_allow_transitive_dependencies());
1480 assert!(!r.update_allow_transitive_root_dependencies());
1481 }
1482}