1use crate::constraint::Constraint;
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 fixed_packages: Vec<usize>,
85 pub locked_packages: Vec<usize>,
86 pub fixed_locked_packages: Vec<usize>,
87 pub update_allow_list: Vec<String>,
88 pub update_mode: Option<UpdateMode>,
89 pub restricted_packages: Option<Vec<String>>,
90}
91
92fn push_unique(list: &mut Vec<usize>, idx: usize) {
93 if !list.contains(&idx) {
94 list.push(idx);
95 }
96}
97
98impl Request {
99 pub fn new(locked_repository: Option<Vec<usize>>) -> Request {
100 Request {
101 locked_repository,
102 ..Request::default()
103 }
104 }
105
106 pub fn require_name(
107 &mut self,
108 name: &str,
109 constraint: Option<Constraint>,
110 ) -> Result<(), PoolError> {
111 let name = name.to_lowercase();
112 let constraint = constraint.unwrap_or(Constraint::MatchAll);
113 if let Some(existing) = self.requires.get(&name) {
114 return Err(PoolError(format!(
115 "Overwriting requires seems like a bug ({name} {existing} => {constraint}, check why it is happening, might be a root alias"
116 )));
117 }
118 self.requires.insert(&name, constraint);
119 Ok(())
120 }
121
122 pub fn fix_package(&mut self, idx: usize) {
123 push_unique(&mut self.fixed_packages, idx);
124 }
125
126 pub fn lock_package(&mut self, idx: usize) {
127 push_unique(&mut self.locked_packages, idx);
128 }
129
130 pub fn fix_locked_package(&mut self, idx: usize) {
131 push_unique(&mut self.fixed_packages, idx);
132 push_unique(&mut self.fixed_locked_packages, idx);
133 }
134
135 pub fn unlock_package(&mut self, idx: usize) {
136 self.locked_packages.retain(|i| *i != idx);
137 }
138
139 pub fn set_update_allow_list(&mut self, list: Vec<String>, mode: UpdateMode) {
140 self.update_allow_list = list;
141 self.update_mode = Some(mode);
142 }
143
144 pub fn update_allow_transitive_dependencies(&self) -> bool {
145 self.update_mode != Some(UpdateMode::OnlyListed)
148 }
149
150 pub fn update_allow_transitive_root_dependencies(&self) -> bool {
151 self.update_mode == Some(UpdateMode::ListedWithTransitiveDeps)
152 }
153
154 pub fn is_fixed_package(&self, idx: usize) -> bool {
155 self.fixed_packages.contains(&idx)
156 }
157
158 pub fn is_locked_package(&self, idx: usize) -> bool {
159 self.locked_packages.contains(&idx) || self.fixed_locked_packages.contains(&idx)
160 }
161
162 pub fn locked_packages_all(&self) -> Vec<usize> {
164 let mut out = self.locked_packages.clone();
165 out.extend(self.fixed_locked_packages.iter().copied());
166 out
167 }
168
169 pub fn fixed_or_locked_packages(&self) -> Vec<usize> {
171 let mut out = self.fixed_packages.clone();
172 out.extend(self.locked_packages.iter().copied());
173 out
174 }
175}
176
177pub enum Repository {
179 Root(Vec<usize>),
181 Platform(Vec<usize>),
183 Composer(Box<ComposerRepository>),
184 Locked(Vec<usize>),
186}
187
188pub struct RepositorySet {
190 pub root_aliases: BTreeMap<String, BTreeMap<String, (String, String)>>,
192 pub root_references: BTreeMap<String, String>,
193 pub acceptable_stabilities: BTreeMap<String, i32>,
194 pub stability_flags: BTreeMap<String, i32>,
195 pub root_requires: OrderedMap<Constraint>,
196 pub temporary_constraints: BTreeMap<String, Constraint>,
197 pub repositories: Vec<Repository>,
198}
199
200impl RepositorySet {
201 pub fn new(
202 minimum_stability: &str,
203 stability_flags: BTreeMap<String, i32>,
204 root_aliases: &[RootAlias],
205 root_references: BTreeMap<String, String>,
206 root_requires: OrderedMap<Constraint>,
207 temporary_constraints: BTreeMap<String, Constraint>,
208 ) -> RepositorySet {
209 let mut aliases: BTreeMap<String, BTreeMap<String, (String, String)>> = BTreeMap::new();
210 for a in root_aliases {
211 aliases.entry(a.package.clone()).or_default().insert(
212 a.version.clone(),
213 (a.alias.clone(), a.alias_normalized.clone()),
214 );
215 }
216 let min = stability_rank(minimum_stability);
217 let mut acceptable = BTreeMap::new();
218 for s in ["stable", "RC", "beta", "alpha", "dev"] {
219 let rank = stability_rank(s);
220 if rank <= min {
221 acceptable.insert(s.to_owned(), rank);
222 }
223 }
224 let mut requires = OrderedMap::default();
225 for (name, c) in root_requires.iter() {
226 if !is_platform_package(name) {
227 requires.insert(name, c.clone());
228 }
229 }
230 RepositorySet {
231 root_aliases: aliases,
232 root_references,
233 acceptable_stabilities: acceptable,
234 stability_flags,
235 root_requires: requires,
236 temporary_constraints,
237 repositories: Vec::new(),
238 }
239 }
240
241 pub fn add_repository(&mut self, repo: Repository) {
242 self.repositories.push(repo);
243 }
244
245 pub fn create_pool(
247 &self,
248 request: &mut Request,
249 arena: &mut Vec<Package>,
250 ) -> Result<Pool, PoolError> {
251 let mut builder = PoolBuilder::new(self);
252 builder.build_pool(&self.repositories, request, arena)
253 }
254}
255
256pub type FilterListRemoved = BTreeMap<String, Vec<(String, Vec<crate::repository::FilterEntry>)>>;
259
260#[derive(Debug, Clone, Default)]
261pub struct Pool {
262 pub identity: u64,
265 pub packages: Vec<usize>,
267 id_of: HashMap<usize, usize>,
268 package_by_name: HashMap<String, Vec<usize>>,
269 pub unacceptable_fixed_or_locked: Vec<usize>,
270 pub warnings: Vec<String>,
271 pub filter_list_removed: FilterListRemoved,
274}
275
276impl Pool {
277 pub fn new(packages: Vec<usize>, unacceptable: Vec<usize>, arena: &[Package]) -> Pool {
278 static NEXT_IDENTITY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
279 let mut pool = Pool {
280 identity: NEXT_IDENTITY.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
281 packages: Vec::with_capacity(packages.len()),
282 id_of: HashMap::new(),
283 package_by_name: HashMap::new(),
284 unacceptable_fixed_or_locked: unacceptable,
285 warnings: Vec::new(),
286 filter_list_removed: BTreeMap::new(),
287 };
288 for idx in packages {
289 pool.packages.push(idx);
290 let id = pool.packages.len();
291 pool.id_of.insert(idx, id);
292 for name in arena[idx].names(true) {
293 pool.package_by_name.entry(name).or_default().push(id);
294 }
295 }
296 pool
297 }
298
299 pub fn len(&self) -> usize {
300 self.packages.len()
301 }
302
303 pub fn is_empty(&self) -> bool {
304 self.packages.is_empty()
305 }
306
307 pub fn package_by_id(&self, id: usize) -> usize {
309 self.packages[id - 1]
310 }
311
312 pub fn id_of(&self, arena_idx: usize) -> Option<usize> {
313 self.id_of.get(&arena_idx).copied()
314 }
315
316 pub fn literal_to_package(&self, literal: i64) -> usize {
317 self.package_by_id(literal.unsigned_abs() as usize)
318 }
319
320 pub fn what_provides(
322 &self,
323 arena: &[Package],
324 name: &str,
325 constraint: Option<&Constraint>,
326 ) -> Vec<usize> {
327 let Some(candidates) = self.package_by_name.get(name) else {
328 return Vec::new();
329 };
330 candidates
331 .iter()
332 .copied()
333 .filter(|id| Self::matches(&arena[self.packages[id - 1]], name, constraint))
334 .collect()
335 }
336
337 pub fn matches(candidate: &Package, name: &str, constraint: Option<&Constraint>) -> bool {
339 if candidate.name == name {
340 return match constraint {
341 None => true,
342 Some(c) => c.matches_version(&candidate.version),
343 };
344 }
345 let provides = &candidate.provides;
346 let replaces = &candidate.replaces;
347 if replaces.has_numeric_keys() || provides.has_numeric_keys() {
352 for link in provides.iter().chain(replaces.iter()) {
353 if link.target == name && constraint.is_none_or(|c| c.matches(&link.constraint)) {
354 return true;
355 }
356 }
357 return false;
358 }
359 if let Some(link) = provides.get(name) {
360 if constraint.is_none_or(|c| c.matches(&link.constraint)) {
361 return true;
362 }
363 }
364 if let Some(link) = replaces.get(name) {
365 if constraint.is_none_or(|c| c.matches(&link.constraint)) {
366 return true;
367 }
368 }
369 false
370 }
371
372 pub fn is_filter_list_removed(&self, name: &str, version: &str) -> bool {
374 self.filter_list_removed
375 .get(name)
376 .is_some_and(|versions| versions.iter().any(|(v, _)| v == version))
377 }
378
379 pub fn with_packages(&self, kept: Vec<usize>, arena: &[Package]) -> Pool {
382 let mut pool = Pool::new(kept, self.unacceptable_fixed_or_locked.clone(), arena);
383 pool.warnings = self.warnings.clone();
384 pool.filter_list_removed = self.filter_list_removed.clone();
385 pool
386 }
387
388 pub fn is_unacceptable_fixed_or_locked(&self, arena_idx: usize) -> bool {
389 self.unacceptable_fixed_or_locked.contains(&arena_idx)
390 }
391}
392
393const LOAD_BATCH_SIZE: usize = 50;
394
395pub fn package_name_regexp(pattern: &str) -> Regex {
397 let quoted = crate::version::preg_quote(pattern).replace("\\*", ".*");
398 pcre2::bytes::RegexBuilder::new()
399 .caseless(true)
400 .build(&format!("^{quoted}$"))
401 .unwrap_or_else(|e| panic!("pattern {pattern}: {e}"))
402}
403
404struct PoolBuilder<'a> {
405 set: &'a RepositorySet,
406 alias_map: HashMap<usize, Vec<(usize, usize)>>,
408 packages_to_load: OrderedMap<Constraint>,
409 loaded_packages: BTreeMap<String, Constraint>,
410 loaded_per_repo: BTreeMap<usize, BTreeMap<String, BTreeSet<String>>>,
411 packages: Vec<Option<usize>>,
413 unacceptable: Vec<usize>,
414 update_allow_list: Vec<String>,
415 update_allow_patterns: Vec<Regex>,
416 skipped_load: BTreeMap<String, Vec<usize>>,
417 ignored_types: Vec<String>,
418 allowed_types: Option<Vec<String>>,
419 restricted: Option<BTreeSet<String>>,
420 path_repo_unlocked: BTreeSet<String>,
421 max_extended_reqs: BTreeSet<String>,
422 update_allow_warned: BTreeSet<String>,
423 warnings: Vec<String>,
424}
425
426impl<'a> PoolBuilder<'a> {
427 fn new(set: &'a RepositorySet) -> PoolBuilder<'a> {
428 PoolBuilder {
429 set,
430 alias_map: HashMap::new(),
431 packages_to_load: OrderedMap::default(),
432 loaded_packages: BTreeMap::new(),
433 loaded_per_repo: BTreeMap::new(),
434 packages: Vec::new(),
435 unacceptable: Vec::new(),
436 update_allow_list: Vec::new(),
437 update_allow_patterns: Vec::new(),
438 skipped_load: BTreeMap::new(),
439 ignored_types: Vec::new(),
440 allowed_types: None,
441 restricted: None,
442 path_repo_unlocked: BTreeSet::new(),
443 max_extended_reqs: BTreeSet::new(),
444 update_allow_warned: BTreeSet::new(),
445 warnings: Vec::new(),
446 }
447 }
448
449 fn loaded_packages_in_pool(&self) -> Vec<usize> {
450 self.packages.iter().flatten().copied().collect()
451 }
452
453 fn build_pool(
454 &mut self,
455 repositories: &[Repository],
456 request: &mut Request,
457 arena: &mut Vec<Package>,
458 ) -> Result<Pool, PoolError> {
459 self.restricted = request
460 .restricted_packages
461 .as_ref()
462 .map(|l| l.iter().cloned().collect());
463
464 if !request.update_allow_list.is_empty() {
465 self.update_allow_list = request.update_allow_list.clone();
466 self.update_allow_patterns = self
467 .update_allow_list
468 .iter()
469 .map(|p| package_name_regexp(p))
470 .collect();
471 self.warn_about_non_matching_update_allow_list(request, arena)?;
472 let Some(locked) = request.locked_repository.clone() else {
473 return Err(PoolError(
474 "No lock repo present and yet a partial update was requested.".into(),
475 ));
476 };
477 for locked_idx in locked {
478 if !self.is_update_allowed(&arena[locked_idx]) {
479 let p = &arena[locked_idx];
480 self.skipped_load
481 .entry(p.name.clone())
482 .or_default()
483 .push(locked_idx);
484 for link in p.replaces.iter() {
485 self.skipped_load
486 .entry(link.target.clone())
487 .or_default()
488 .push(locked_idx);
489 }
490 if p.dist.as_ref().is_some_and(|d| d.kind == "path") {
491 let symlink = p
492 .raw
493 .get("transport-options")
494 .and_then(|t| t.get("symlink"))
495 .cloned();
496 if symlink != Some(serde_json::Value::Bool(false)) {
497 self.path_repo_unlocked.insert(p.name.clone());
498 continue;
499 }
500 }
501 request.lock_package(locked_idx);
502 }
503 }
504 }
505
506 for idx in request.fixed_or_locked_packages() {
507 let (name, replaces, names, stability, origin) = {
508 let p = &arena[idx];
509 (
510 p.name.clone(),
511 p.replaces
512 .iter()
513 .map(|l| l.target.clone())
514 .collect::<Vec<_>>(),
515 p.names(true),
516 p.stability,
517 p.origin,
518 )
519 };
520 self.loaded_packages.insert(name, Constraint::MatchAll);
521 for target in replaces {
522 self.loaded_packages.insert(target, Constraint::MatchAll);
523 }
524 if matches!(origin, Origin::Root | Origin::Platform)
525 || is_package_acceptable(
526 &self.set.acceptable_stabilities,
527 &self.set.stability_flags,
528 &names,
529 stability,
530 )
531 {
532 self.load_package(request, idx, false, arena);
533 } else {
534 self.unacceptable.push(idx);
535 }
536 }
537
538 let requires: Vec<(String, Constraint)> = request.requires.0.clone();
539 for (name, constraint) in &requires {
540 if self.loaded_packages.contains_key(name) {
541 continue;
542 }
543 self.packages_to_load.insert(name, constraint.clone());
544 self.max_extended_reqs.insert(name.clone());
545 }
546 let already: Vec<String> = self
547 .packages_to_load
548 .keys()
549 .filter(|n| self.loaded_packages.contains_key(*n))
550 .cloned()
551 .collect();
552 for name in already {
553 self.packages_to_load.remove(&name);
554 }
555
556 while !self.packages_to_load.is_empty() {
557 self.load_packages_marked_for_loading(request, repositories, arena)?;
558 }
559
560 if !self.set.temporary_constraints.is_empty() {
561 let entries: Vec<(usize, usize)> = self
562 .packages
563 .iter()
564 .enumerate()
565 .filter_map(|(i, p)| p.map(|idx| (i, idx)))
566 .collect();
567 for (i, idx) in entries {
568 if arena[idx].is_alias() {
569 continue;
570 }
571 for name in arena[idx].names(true) {
572 let Some(constraint) = self.set.temporary_constraints.get(&name) else {
573 continue;
574 };
575 let mut package_and_aliases: Vec<(usize, usize)> = vec![(i, idx)];
576 if let Some(aliases) = self.alias_map.get(&idx) {
577 package_and_aliases.extend(aliases.iter().copied());
578 }
579 let found = package_and_aliases
580 .iter()
581 .any(|(_, p)| constraint.matches_version(&arena[*p].version));
582 if !found {
583 for (pool_index, _) in package_and_aliases {
584 self.packages[pool_index] = None;
585 }
586 }
587 }
588 }
589 }
590
591 let packages = self.loaded_packages_in_pool();
592 let mut pool = Pool::new(packages, std::mem::take(&mut self.unacceptable), arena);
593 pool.warnings = std::mem::take(&mut self.warnings);
594 Ok(pool)
595 }
596
597 fn mark_package_name_for_loading(
599 &mut self,
600 request: &Request,
601 name: &str,
602 constraint: &Constraint,
603 ) {
604 if is_platform_package(name) {
605 return;
606 }
607 if self.max_extended_reqs.contains(name) {
608 return;
609 }
610 let mut constraint = constraint.clone();
611 if let Some(root) = request.requires.get(name) {
612 if !intervals::is_subset_of(&constraint, root) {
613 constraint = root.clone();
614 }
615 }
616 if !self.loaded_packages.contains_key(name) {
617 if let Some(pending) = self.packages_to_load.get(name) {
618 if intervals::is_subset_of(&constraint, pending) {
619 return;
620 }
621 constraint = intervals::compact_constraint(&Constraint::create(
622 vec![pending.clone(), constraint],
623 false,
624 ));
625 }
626 self.packages_to_load.insert(name, constraint);
627 return;
628 }
629 let loaded = &self.loaded_packages[name];
630 if intervals::is_subset_of(&constraint, loaded) {
631 return;
632 }
633 let merged = intervals::compact_constraint(&Constraint::create(
634 vec![loaded.clone(), constraint],
635 false,
636 ));
637 self.packages_to_load.insert(name, merged);
638 self.loaded_packages.remove(name);
639 }
640
641 fn load_packages_marked_for_loading(
643 &mut self,
644 request: &mut Request,
645 repositories: &[Repository],
646 arena: &mut Vec<Package>,
647 ) -> Result<(), PoolError> {
648 let names: Vec<String> = self.packages_to_load.keys().cloned().collect();
649 for name in names {
650 if let Some(restricted) = &self.restricted {
651 if !restricted.contains(&name) {
652 self.packages_to_load.remove(&name);
653 continue;
654 }
655 }
656 let c = self
657 .packages_to_load
658 .get(&name)
659 .cloned()
660 .unwrap_or(Constraint::MatchAll);
661 self.loaded_packages.insert(name, c);
662 }
663 let mut remaining: Vec<(String, Constraint)> = std::mem::take(&mut self.packages_to_load).0;
664 for (repo_index, repository) in repositories.iter().enumerate() {
665 if matches!(repository, Repository::Platform(_) | Repository::Locked(_)) {
666 continue;
667 }
668 if remaining.is_empty() {
669 break;
670 }
671 let batches: Vec<Vec<(String, Constraint)>> = remaining
672 .chunks(LOAD_BATCH_SIZE)
673 .map(|c| c.to_vec())
674 .collect();
675 let mut kept: Vec<Vec<(String, Constraint)>> = Vec::new();
676 for batch in batches {
677 let empty = BTreeMap::new();
678 let already = self.loaded_per_repo.get(&repo_index).unwrap_or(&empty);
679 let (names_found, ids) = match repository {
680 Repository::Composer(repo) => repo.load_packages(
681 &batch,
682 &self.set.acceptable_stabilities,
683 &self.set.stability_flags,
684 already,
685 Origin::Repository(repo_index),
686 arena,
687 )?,
688 Repository::Root(members) => array_repository_load_packages(
689 members,
690 &batch,
691 &self.set.acceptable_stabilities,
692 &self.set.stability_flags,
693 already,
694 arena,
695 ),
696 Repository::Platform(_) | Repository::Locked(_) => unreachable!(),
697 };
698 let mut batch = batch;
699 batch.retain(|(n, _)| !names_found.contains(n));
700 kept.push(batch);
701 for idx in ids {
702 let (name, version, package_type) = {
703 let p = &arena[idx];
704 (p.name.clone(), p.version.clone(), p.package_type.clone())
705 };
706 self.loaded_per_repo
707 .entry(repo_index)
708 .or_default()
709 .entry(name.clone())
710 .or_default()
711 .insert(version);
712 if self.ignored_types.contains(&package_type)
713 || self
714 .allowed_types
715 .as_ref()
716 .is_some_and(|a| !a.contains(&package_type))
717 {
718 continue;
719 }
720 let propagate = !self.path_repo_unlocked.contains(&name);
721 self.load_package(request, idx, propagate, arena);
722 }
723 }
724 remaining = kept.into_iter().flatten().collect();
725 }
726 Ok(())
727 }
728
729 fn load_package(
731 &mut self,
732 request: &mut Request,
733 idx: usize,
734 propagate_update: bool,
735 arena: &mut Vec<Package>,
736 ) {
737 let index = self.packages.len();
738 self.packages.push(Some(idx));
739 if let Some(base) = arena[idx].alias_of {
740 self.alias_map.entry(base).or_default().push((index, idx));
741 }
742 let name = arena[idx].name.clone();
743 if let Some(reference) = self.set.root_references.get(&name) {
744 if !request.is_locked_package(idx) && !request.is_fixed_package(idx) {
745 set_source_dist_references(arena, idx, reference);
746 }
747 }
748 if propagate_update || self.path_repo_unlocked.contains(&name) {
749 let version = arena[idx].version.clone();
750 if let Some((alias, alias_normalized)) = self
751 .set
752 .root_aliases
753 .get(&name)
754 .and_then(|m| m.get(&version))
755 {
756 let base = arena[idx].alias_of.unwrap_or(idx);
757 let mut alias_package = arena[base].alias(base, alias_normalized, alias);
758 alias_package.root_package_alias = true;
759 alias_package.origin = Origin::Detached;
760 arena.push(alias_package);
761 let alias_idx = arena.len() - 1;
762 let new_index = self.packages.len();
763 self.packages.push(Some(alias_idx));
764 self.alias_map
765 .entry(base)
766 .or_default()
767 .push((new_index, alias_idx));
768 }
769 }
770 let requires: Vec<(String, Constraint)> = arena[idx]
771 .requires
772 .iter()
773 .map(|l| (l.target.clone(), l.constraint.clone()))
774 .collect();
775 for (require, link_constraint) in requires {
776 if self.skipped_load.contains_key(&require) {
777 if propagate_update && request.update_allow_transitive_dependencies() {
778 let skipped_root_requires =
779 self.skipped_root_requires(request, &require, arena);
780 if request.update_allow_transitive_root_dependencies()
781 || skipped_root_requires.is_empty()
782 {
783 self.unlock_package(request, &require, arena);
784 self.mark_package_name_for_loading(request, &require, &link_constraint);
785 } else {
786 self.warn_root_requires(&skipped_root_requires);
787 }
788 } else if self.path_repo_unlocked.contains(&require)
789 && !self.loaded_packages.contains_key(&require)
790 {
791 self.mark_package_name_for_loading(request, &require, &link_constraint);
792 }
793 } else {
794 self.mark_package_name_for_loading(request, &require, &link_constraint);
795 }
796 }
797 if propagate_update && request.update_allow_transitive_dependencies() {
798 let replaces: Vec<String> = arena[idx]
799 .replaces
800 .iter()
801 .map(|l| l.target.clone())
802 .collect();
803 for replace in replaces {
804 if self.loaded_packages.contains_key(&replace)
805 && self.skipped_load.contains_key(&replace)
806 {
807 let skipped_root_requires =
808 self.skipped_root_requires(request, &replace, arena);
809 if request.update_allow_transitive_root_dependencies()
810 || skipped_root_requires.is_empty()
811 {
812 self.unlock_package(request, &replace, arena);
813 self.mark_package_name_for_loading_if_required(request, &replace, arena);
814 } else {
815 self.warn_root_requires(&skipped_root_requires);
816 }
817 }
818 }
819 }
820 }
821
822 fn warn_root_requires(&mut self, root_requires: &[String]) {
823 for root_require in root_requires {
824 if self.update_allow_warned.insert(root_require.clone()) {
825 self.warnings.push(format!(
826 "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."
827 ));
828 }
829 }
830 }
831
832 fn is_root_require(request: &Request, name: &str) -> bool {
833 request.requires.contains(name)
834 }
835
836 fn skipped_root_requires(
838 &self,
839 request: &Request,
840 name: &str,
841 arena: &[Package],
842 ) -> Vec<String> {
843 let Some(skipped) = self.skipped_load.get(name) else {
844 return Vec::new();
845 };
846 if request.requires.contains(name) {
847 return skipped
848 .iter()
849 .map(|idx| {
850 let p = &arena[*idx];
851 if p.name != name {
852 format!("{} (via replace of {name})", p.name)
853 } else {
854 p.name.clone()
855 }
856 })
857 .collect();
858 }
859 let mut matches = Vec::new();
860 for idx in skipped {
861 let p = &arena[*idx];
862 if request.requires.contains(&p.name) {
863 matches.push(p.name.clone());
864 }
865 for link in p.replaces.iter() {
866 if request.requires.contains(&link.target) {
867 if p.name != name {
868 matches.push(format!("{} (via replace of {name})", p.name));
869 } else {
870 matches.push(p.name.clone());
871 }
872 break;
873 }
874 }
875 }
876 matches
877 }
878
879 fn is_update_allowed(&self, package: &Package) -> bool {
881 self.update_allow_patterns
882 .iter()
883 .any(|re| re.is_match(package.name.as_bytes()).unwrap_or(false))
884 }
885
886 fn warn_about_non_matching_update_allow_list(
888 &mut self,
889 request: &Request,
890 arena: &[Package],
891 ) -> Result<(), PoolError> {
892 let Some(locked) = &request.locked_repository else {
893 return Err(PoolError(
894 "No lock repo present and yet a partial update was requested.".into(),
895 ));
896 };
897 'patterns: for pattern in &self.update_allow_list.clone() {
898 let mut matched_platform = false;
899 let re = package_name_regexp(pattern);
900 for idx in locked {
901 if re.is_match(arena[*idx].name.as_bytes()).unwrap_or(false) {
902 continue 'patterns;
903 }
904 }
905 for name in request.requires.keys() {
906 if re.is_match(name.as_bytes()).unwrap_or(false) {
907 if is_platform_package(name) {
908 matched_platform = true;
909 continue;
910 }
911 continue 'patterns;
912 }
913 }
914 if matched_platform {
915 self.warnings.push(format!(
916 "Pattern \"{pattern}\" listed for update matches platform packages, but these cannot be updated by Composer."
917 ));
918 } else if pattern.contains('*') {
919 self.warnings.push(format!(
920 "Pattern \"{pattern}\" listed for update does not match any locked packages."
921 ));
922 } else {
923 self.warnings.push(format!(
924 "Package \"{pattern}\" listed for update is not locked."
925 ));
926 }
927 }
928 Ok(())
929 }
930
931 fn unlock_package(&mut self, request: &mut Request, name: &str, arena: &mut Vec<Package>) {
933 let skipped: Vec<usize> = self.skipped_load.get(name).cloned().unwrap_or_default();
934 for idx in skipped {
935 let replacer_name = arena[idx].name.clone();
936 if replacer_name != name
937 && self.skipped_load.contains_key(&replacer_name)
938 && (request.update_allow_transitive_root_dependencies()
939 || (!Self::is_root_require(request, name)
940 && !Self::is_root_require(request, &replacer_name)))
941 {
942 self.unlock_package(request, &replacer_name, arena);
943 if Self::is_root_require(request, &replacer_name) {
944 self.mark_package_name_for_loading(
945 request,
946 &replacer_name,
947 &Constraint::MatchAll,
948 );
949 } else {
950 for loaded in self.loaded_packages_in_pool() {
951 let c = arena[loaded]
952 .requires
953 .get(&replacer_name)
954 .map(|l| l.constraint.clone());
955 if let Some(c) = c {
956 self.mark_package_name_for_loading(request, &replacer_name, &c);
957 }
958 }
959 }
960 }
961 }
962 if self.path_repo_unlocked.contains(name) {
963 let entries: Vec<(usize, usize)> = self
964 .packages
965 .iter()
966 .enumerate()
967 .filter_map(|(i, p)| p.map(|idx| (i, idx)))
968 .filter(|(_, idx)| arena[*idx].name == name)
969 .collect();
970 for (index, idx) in entries {
971 self.remove_loaded_package(idx, index, arena);
972 }
973 }
974 self.skipped_load.remove(name);
975 self.loaded_packages.remove(name);
976 self.max_extended_reqs.remove(name);
977 self.path_repo_unlocked.remove(name);
978 for locked_idx in request.locked_packages_all() {
979 if !arena[locked_idx].is_alias() && arena[locked_idx].name == name {
980 if let Some(index) = self.packages.iter().position(|p| *p == Some(locked_idx)) {
981 request.unlock_package(locked_idx);
982 self.remove_loaded_package(locked_idx, index, arena);
983 for fixed_or_locked in request.fixed_or_locked_packages() {
984 if fixed_or_locked == locked_idx {
985 continue;
986 }
987 if self.skipped_load.contains_key(&arena[fixed_or_locked].name) {
988 let locked_name = arena[locked_idx].name.clone();
989 let requires = arena[fixed_or_locked].requires.clone();
990 if let Some(link) = requires.get(&locked_name) {
991 let c = link.constraint.clone();
992 self.mark_package_name_for_loading(request, &locked_name, &c);
993 }
994 let replaces = arena[locked_idx].replaces.clone();
995 for replace in replaces.iter() {
996 if requires.get(&replace.target).is_some()
997 && self.skipped_load.contains_key(&replace.target)
998 {
999 self.unlock_package(request, &replace.target, arena);
1000 self.mark_package_name_for_loading(
1001 request,
1002 &replace.target,
1003 &replace.constraint,
1004 );
1005 }
1006 }
1007 }
1008 }
1009 }
1010 }
1011 }
1012 }
1013
1014 fn mark_package_name_for_loading_if_required(
1016 &mut self,
1017 request: &Request,
1018 name: &str,
1019 arena: &[Package],
1020 ) {
1021 if let Some(c) = request.requires.get(name).cloned() {
1022 self.mark_package_name_for_loading(request, name, &c);
1023 }
1024 for loaded in self.loaded_packages_in_pool() {
1025 let links: Vec<Constraint> = arena[loaded]
1026 .requires
1027 .iter()
1028 .filter(|l| l.target == name)
1029 .map(|l| l.constraint.clone())
1030 .collect();
1031 for c in links {
1032 self.mark_package_name_for_loading(request, name, &c);
1033 }
1034 }
1035 }
1036
1037 fn remove_loaded_package(&mut self, idx: usize, index: usize, arena: &[Package]) {
1039 let repo_index = match arena[idx].origin {
1040 Origin::Repository(i) => Some(i),
1041 _ => None,
1042 };
1043 let mut forget = |p: &Package| {
1044 if let Some(r) = repo_index {
1045 if let Some(by_name) = self
1046 .loaded_per_repo
1047 .get_mut(&r)
1048 .and_then(|m| m.get_mut(&p.name))
1049 {
1050 by_name.remove(&p.version);
1051 }
1052 }
1053 };
1054 forget(&arena[idx]);
1055 self.packages[index] = None;
1056 if let Some(aliases) = self.alias_map.remove(&idx) {
1057 for (alias_index, alias_idx) in aliases {
1058 forget(&arena[alias_idx]);
1059 self.packages[alias_index] = None;
1060 }
1061 }
1062 }
1063}
1064
1065pub fn array_repository_load_packages(
1067 members: &[usize],
1068 package_name_map: &[(String, Constraint)],
1069 acceptable: &BTreeMap<String, i32>,
1070 flags: &BTreeMap<String, i32>,
1071 already_loaded: &BTreeMap<String, BTreeSet<String>>,
1072 arena: &[Package],
1073) -> (Vec<String>, Vec<usize>) {
1074 let mut result: Vec<usize> = Vec::new();
1075 let mut names_found: Vec<String> = Vec::new();
1076 for idx in members {
1077 let p = &arena[*idx];
1078 let Some((_, constraint)) = package_name_map.iter().find(|(n, _)| *n == p.name) else {
1079 continue;
1080 };
1081 let matches =
1082 matches!(constraint, Constraint::MatchAll) || constraint.matches_version(&p.version);
1083 if matches
1084 && is_package_acceptable(acceptable, flags, &p.names(true), p.stability)
1085 && !already_loaded
1086 .get(&p.name)
1087 .is_some_and(|s| s.contains(&p.version))
1088 {
1089 push_unique(&mut result, *idx);
1090 if let Some(base) = p.alias_of {
1091 push_unique(&mut result, base);
1092 }
1093 }
1094 if !names_found.contains(&p.name) {
1095 names_found.push(p.name.clone());
1096 }
1097 }
1098 for idx in members {
1099 if let Some(base) = arena[*idx].alias_of {
1100 if result.contains(&base) {
1101 push_unique(&mut result, *idx);
1102 }
1103 }
1104 }
1105 (names_found, result)
1106}
1107
1108pub fn set_source_dist_references(arena: &mut [Package], idx: usize, reference: &str) {
1112 static HOSTS: OnceLock<Regex> = OnceLock::new();
1113 static SHA: OnceLock<Regex> = OnceLock::new();
1114 let base = arena[idx].alias_of.unwrap_or(idx);
1115 let targets: Vec<usize> = (0..arena.len())
1116 .filter(|i| *i == base || arena[*i].alias_of == Some(base))
1117 .collect();
1118 for t in targets {
1119 let p = &mut arena[t];
1120 if let Some(s) = &mut p.source {
1121 s.reference = Some(reference.to_owned());
1122 }
1123 let dist_url = p.dist.as_ref().map(|d| d.url.clone());
1124 let hosts = regex(
1125 &HOSTS,
1126 r"^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/",
1127 true,
1128 );
1129 match dist_url {
1130 Some(url) if hosts.is_match(url.as_bytes()).unwrap_or(false) => {
1131 let sha = regex(&SHA, r"(?<=/|sha=)[a-f0-9]{40}(?=/|$)", true);
1132 let replaced = replace_all(sha, &url, reference);
1133 if let Some(d) = &mut p.dist {
1134 d.reference = Some(reference.to_owned());
1135 d.url = replaced;
1136 }
1137 }
1138 _ => {
1139 if let Some(d) = &mut p.dist {
1140 if d.reference
1141 .as_deref()
1142 .is_some_and(|r| !r.is_empty() && r != "0")
1143 {
1144 d.reference = Some(reference.to_owned());
1145 }
1146 }
1147 }
1148 }
1149 }
1150}
1151
1152fn replace_all(re: &Regex, subject: &str, replacement: &str) -> String {
1153 let mut out = String::new();
1154 let mut last = 0;
1155 for m in re.find_iter(subject.as_bytes()).flatten() {
1156 out.push_str(&subject[last..m.start()]);
1157 out.push_str(replacement);
1158 last = m.end();
1159 }
1160 out.push_str(&subject[last..]);
1161 out
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use super::*;
1167 use crate::package::{Link, LinkType};
1168 use serde_json::json;
1169
1170 #[test]
1171 fn match_looks_up_links_by_php_key() {
1172 let mut lib = Package::new("lib-libxslt", "1.1.35.0", "1.1.35", Origin::Platform);
1175 lib.replaces.insert(Link {
1176 key: Some("xsl".into()),
1177 source: "lib-libxslt".into(),
1178 target: "lib-xsl".into(),
1179 constraint: Constraint::new(crate::constraint::Op::Eq, "1.1.35.0"),
1180 pretty_constraint: "1.1.35".into(),
1181 kind: LinkType::Replace,
1182 });
1183 assert!(!Pool::matches(&lib, "lib-xsl", None));
1184 assert!(Pool::matches(&lib, "xsl", None));
1187 let pool = Pool::new(vec![0], Vec::new(), std::slice::from_ref(&lib));
1188 assert_eq!(
1189 pool.what_provides(std::slice::from_ref(&lib), "xsl", None),
1190 Vec::<usize>::new()
1191 );
1192 assert_eq!(
1193 pool.what_provides(std::slice::from_ref(&lib), "lib-xsl", None),
1194 Vec::<usize>::new()
1195 );
1196 let cfg = json!({"name": "acme/lib", "version": "dev-main", "default-branch": true,
1198 "replace": {"acme/old": "self.version"}});
1199 let mut arena = Vec::new();
1200 let ids =
1201 crate::loader::load_packages(&[cfg], Origin::Repository(0), &mut arena, false).unwrap();
1202 let alias = &arena[ids[0]];
1203 assert!(alias.replaces.has_numeric_keys());
1204 assert!(Pool::matches(
1205 alias,
1206 "acme/old",
1207 Some(&Constraint::new(crate::constraint::Op::Eq, "9999999-dev"))
1208 ));
1209 assert!(Pool::matches(
1210 alias,
1211 "acme/old",
1212 Some(&Constraint::new(crate::constraint::Op::Eq, "dev-main"))
1213 ));
1214 let base = &arena[ids[1]];
1215 assert!(!base.replaces.has_numeric_keys());
1216 assert!(!Pool::matches(
1217 base,
1218 "acme/old",
1219 Some(&Constraint::new(crate::constraint::Op::Eq, "9999999-dev"))
1220 ));
1221 let pool = Pool::new(vec![ids[1], ids[0]], Vec::new(), &arena);
1222 assert_eq!(pool.what_provides(&arena, "acme/old", None), vec![1, 2]);
1223 assert_eq!(
1224 pool.what_provides(&arena, "lib-xsl", None),
1225 Vec::<usize>::new()
1226 );
1227 }
1228
1229 #[test]
1230 fn transitive_dependencies_allowed_without_allow_list() {
1231 let r = Request::new(None);
1232 assert!(r.update_allow_transitive_dependencies());
1233 assert!(!r.update_allow_transitive_root_dependencies());
1234 }
1235}