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