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