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