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