Skip to main content

vivacity_resolver/
pool.rs

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