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