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