Skip to main content

vivacity_resolver/
pool.rs

1//! Port of `Composer\DependencyResolver\{Request, PoolBuilder, Pool}` and
2//! `Composer\Repository\RepositorySet` (docs/reference/resolver/). The pool
3//! is the ordered list of packages the solver will see: the order is
4//! Composer's, index by index, because the literal ids depend on it.
5
6use 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/// String-keyed PHP array: insertion order, in-place rewrite.
28#[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/// `Request::UPDATE_*`.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum UpdateMode {
72    OnlyListed,
73    ListedWithTransitiveDepsNoRootRequire,
74    ListedWithTransitiveDeps,
75}
76
77/// `Composer\DependencyResolver\Request`. Packages are arena indices; the
78/// PHP arrays keyed by `spl_object_id` become ordered lists without
79/// duplicates.
80#[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        // `$this->updateAllowTransitiveDependencies !== self::UPDATE_ONLY_LISTED`:
146        // also true when no list was set (`false !== 0`).
147        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    /// `array_merge($lockedPackages, $fixedLockedPackages)`.
163    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    /// `array_merge($fixedPackages, $lockedPackages)`.
170    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
177/// A repository of the `RepositorySet`, in insertion order.
178pub enum Repository {
179    /// `RootPackageRepository`: [root alias?, root].
180    Root(Vec<usize>),
181    /// `PlatformRepository`.
182    Platform(Vec<usize>),
183    Composer(Box<ComposerRepository>),
184    /// `LockArrayRepository`.
185    Locked(Vec<usize>),
186}
187
188/// `Composer\Repository\RepositorySet`.
189pub struct RepositorySet {
190    /// name -> version -> (alias, alias_normalized) (`getRootAliasesPerPackage`).
191    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    /// `createPool` without optimizer or filters.
246    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
256/// `Composer\DependencyResolver\Pool`: 1-based ids in construction order.
257/// Versions removed by a filter list: name -> (version, entries).
258pub type FilterListRemoved = BTreeMap<String, Vec<(String, Vec<crate::repository::FilterEntry>)>>;
259
260#[derive(Debug, Clone, Default)]
261pub struct Pool {
262    /// Pool identity (`spl_object_id($pool)` in Composer): the policy caches
263    /// are keyed by pool.
264    pub identity: u64,
265    /// id - 1 -> arena index.
266    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    /// `filterListRemovedVersions`: what a filter list removed (the rule
272    /// generator and the solver consult it).
273    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    /// `packageById` -> arena index.
308    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    /// `whatProvides` -> pool ids.
321    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    /// `Pool::match`.
338    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        // `isset($replaces[0]) || isset($provides[0])`: numeric keys
348        // (self.version links of an alias) -> scan by target; otherwise
349        // lookup by key (`isset($provides[$name])`), which is not always the
350        // target (platform lib-*).
351        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    /// `isFilterListRemovedPackageVersion($name, Constraint('==', $version))`.
373    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    /// The same pool reduced to `kept` (renumbered ids), removed versions
380    /// and warnings preserved.
381    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
395/// `BasePackage::packageNameToRegexp`.
396pub 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    /// base arena idx -> [(pool index, alias arena idx)].
407    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    /// Pool index -> arena idx (`unset` = None).
412    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    /// `markPackageNameForLoading`.
598    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    /// `loadPackagesMarkedForLoading`.
642    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    /// `loadPackage`.
730    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    /// `getSkippedRootRequires`.
837    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    /// `isUpdateAllowed`.
880    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    /// `warnAboutNonMatchingUpdateAllowList`.
887    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    /// `unlockPackage`.
932    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    /// `markPackageNameForLoadingIfRequired`.
1015    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    /// `removeLoadedPackage`.
1038    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
1065/// `ArrayRepository::loadPackages` (root repository).
1066pub 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
1108/// `Package::setSourceDistReferences`, applied to the base package and its
1109/// aliases (an `AliasPackage` delegates its references to the aliased
1110/// package).
1111pub 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        // lib-libxslt replace: key `xsl`, target `lib-xsl` -> not found by
1173        // `lib-xsl` (isset($replaces['lib-xsl']) is false in Composer).
1174        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        // By key, `match` says yes, but `packageByName` only knows the
1185        // targets, so `whatProvides('xsl')` stays empty.
1186        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        // With numeric keys (self.version alias), scan by target.
1197        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}