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