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