diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-16 19:46:18 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-16 22:17:14 +0900 |
| commit | 09189ce989c595d28d1a288cad70c4711b870d26 (patch) | |
| tree | 4db022d5ff326d2d871c2280a8e313d9d89d6ea1 /crates/shirabe | |
| parent | 88bd20e885499ee3f3fff46b755527bad435c7df (diff) | |
| download | php-shirabe-09189ce989c595d28d1a288cad70c4711b870d26.tar.gz php-shirabe-09189ce989c595d28d1a288cad70c4711b870d26.tar.zst php-shirabe-09189ce989c595d28d1a288cad70c4711b870d26.zip | |
feat(port): port ComposerRepository.php
Diffstat (limited to 'crates/shirabe')
| -rw-r--r-- | crates/shirabe/src/repository/composer_repository.rs | 3533 |
1 files changed, 3533 insertions, 0 deletions
diff --git a/crates/shirabe/src/repository/composer_repository.rs b/crates/shirabe/src/repository/composer_repository.rs index 179d674..9d74e5b 100644 --- a/crates/shirabe/src/repository/composer_repository.rs +++ b/crates/shirabe/src/repository/composer_repository.rs @@ -1 +1,3534 @@ //! ref: composer/src/Composer/Repository/ComposerRepository.php + +use indexmap::IndexMap; +use shirabe_external_packages::composer::metadata_minifier::metadata_minifier::MetadataMinifier; +use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_external_packages::react::promise::promise_interface::PromiseInterface; +use shirabe_php_shim::{ + PhpMixed, InvalidArgumentException, LogicException, RuntimeException, + UnexpectedValueException, PHP_EOL, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE, + extension_loaded, hash, http_build_query, in_array, + json_decode, parse_url_all, realpath, strtolower, strtr, + spl_object_hash, urlencode, var_export, +}; + +use shirabe_semver::compiling_matcher::CompilingMatcher; +use shirabe_semver::constraint::constraint::Constraint; +use shirabe_semver::constraint::constraint_interface::ConstraintInterface; +use shirabe_semver::constraint::match_all_constraint::MatchAllConstraint; + +use crate::advisory::partial_security_advisory::PartialSecurityAdvisory; +use crate::cache::Cache; +use crate::config::Config; +use crate::downloader::transport_exception::TransportException; +use crate::event_dispatcher::event_dispatcher::EventDispatcher; +use crate::io::io_interface::IOInterface; +use crate::json::json_file::JsonFile; +use crate::package::base_package::BasePackage; +use crate::package::loader::array_loader::ArrayLoader; +use crate::package::package_interface::PackageInterface; +use crate::package::version::stability_filter::StabilityFilter; +use crate::package::version::version_parser::VersionParser; +use crate::plugin::plugin_events::PluginEvents; +use crate::plugin::post_file_download_event::PostFileDownloadEvent; +use crate::plugin::pre_file_download_event::PreFileDownloadEvent; +use crate::repository::advisory_provider_interface::{ + PartialOrSecurityAdvisory, SecurityAdvisoryResult, +}; +use crate::repository::array_repository::ArrayRepository; +use crate::repository::configurable_repository_interface::ConfigurableRepositoryInterface; +use crate::repository::platform_repository::PlatformRepository; +use crate::repository::repository_security_exception::RepositorySecurityException; +use crate::util::http::response::Response; +use crate::util::http_downloader::HttpDownloader; +use crate::util::r#loop::Loop; +use crate::util::url::Url; + +#[derive(Debug)] +pub enum RootData { + Data(IndexMap<String, PhpMixed>), + True, +} + +#[derive(Debug)] +pub struct SecurityAdvisoryConfig { + pub metadata: bool, + pub api_url: Option<String>, +} + +#[derive(Debug)] +pub struct SourceMirror { + pub url: String, + pub preferred: bool, +} + +#[derive(Debug)] +pub struct DistMirror { + pub url: String, + pub preferred: bool, +} + +#[derive(Debug)] +pub struct ProviderListingEntry { + pub sha256: String, +} + +#[derive(Debug)] +pub struct ComposerRepository { + inner: ArrayRepository, + /// @phpstan-var array{url: string, options?: mixed[], type?: 'composer', allow_ssl_downgrade?: bool} + repo_config: IndexMap<String, PhpMixed>, + options: IndexMap<String, PhpMixed>, + /// non-empty-string + url: String, + /// non-empty-string + base_url: String, + io: Box<dyn IOInterface>, + http_downloader: HttpDownloader, + r#loop: Loop, + pub(crate) cache: Cache, + pub(crate) notify_url: Option<String>, + pub(crate) search_url: Option<String>, + pub(crate) providers_api_url: Option<String>, + pub(crate) has_providers: bool, + pub(crate) providers_url: Option<String>, + pub(crate) list_url: Option<String>, + pub(crate) has_available_package_list: bool, + pub(crate) available_packages: Option<IndexMap<String, String>>, + pub(crate) available_package_patterns: Option<Vec<String>>, + pub(crate) lazy_providers_url: Option<String>, + pub(crate) provider_listing: Option<IndexMap<String, ProviderListingEntry>>, + pub(crate) loader: ArrayLoader, + allow_ssl_downgrade: bool, + event_dispatcher: Option<EventDispatcher>, + source_mirrors: Option<IndexMap<String, Vec<SourceMirror>>>, + dist_mirrors: Option<Vec<DistMirror>>, + degraded_mode: bool, + root_data: Option<RootData>, + has_partial_packages: bool, + partial_packages_by_name: Option<IndexMap<String, Vec<IndexMap<String, PhpMixed>>>>, + displayed_warning_about_non_matching_package_index: bool, + security_advisory_config: Option<SecurityAdvisoryConfig>, + /// list of package names which are fresh and can be loaded from the cache directly in case loadPackage is called several times + /// useful for v2 metadata repositories with lazy providers + freshMetadataUrls: IndexMap<String, bool>, + /// list of package names which returned a 404 and should not be re-fetched in case loadPackage is called several times + /// useful for v2 metadata repositories with lazy providers + packagesNotFoundCache: IndexMap<String, bool>, + version_parser: VersionParser, +} + +#[derive(Debug)] +pub enum FindPackageReturn { + Package(Box<BasePackage>), + Packages(Vec<Box<BasePackage>>), + None, +} + +#[derive(Debug)] +pub struct LoadPackagesResult { + pub names_found: Vec<String>, + pub packages: IndexMap<String, Box<BasePackage>>, +} + +#[derive(Debug)] +pub struct LoadAsyncPackagesResult { + pub names_found: IndexMap<String, bool>, + pub packages: IndexMap<String, Box<BasePackage>>, +} + +impl ConfigurableRepositoryInterface for ComposerRepository { + fn get_repo_config(&self) -> IndexMap<String, PhpMixed> { + self.repo_config.clone() + } +} + +impl ComposerRepository { + pub fn new( + mut repo_config: IndexMap<String, PhpMixed>, + io: Box<dyn IOInterface>, + config: &Config, + http_downloader: HttpDownloader, + event_dispatcher: Option<EventDispatcher>, + ) -> anyhow::Result<Self> { + // parent::__construct(); + let inner = ArrayRepository::new(); + + let url_str = repo_config + .get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + if !Preg::is_match(r"{^[\w.]+\??://}", &url_str)? { + if let Some(local_file_path) = realpath(&url_str) { + // it is a local path, add file scheme + repo_config.insert( + "url".to_string(), + PhpMixed::String(format!("file://{}", local_file_path)), + ); + } else { + // otherwise, assume http as the default protocol + repo_config.insert( + "url".to_string(), + PhpMixed::String(format!("http://{}", url_str)), + ); + } + } + let url_after = repo_config + .get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .trim_end_matches('/') + .to_string(); + repo_config.insert("url".to_string(), PhpMixed::String(url_after.clone())); + if url_after.is_empty() { + return Err(InvalidArgumentException { + message: "The repository url must not be an empty string".to_string(), + code: 0, + } + .into()); + } + + if url_after.starts_with("https?") { + let scheme = if extension_loaded("openssl") { "https" } else { "http" }; + let rest = &url_after[6..]; + repo_config.insert( + "url".to_string(), + PhpMixed::String(format!("{}{}", scheme, rest)), + ); + } + + let current_url = repo_config + .get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let url_bits = parse_url_all(&strtr(¤t_url, "\\", "/")); + let url_bits_arr = url_bits.as_array(); + let scheme_present = url_bits_arr + .and_then(|a| a.get("scheme")) + .and_then(|v| v.as_string()) + .map_or(false, |s| !s.is_empty()); + if url_bits_arr.is_none() || !scheme_present { + return Err(UnexpectedValueException { + message: format!( + "Invalid url given for Composer repository: {}", + current_url + ), + code: 0, + } + .into()); + } + + if !repo_config.contains_key("options") { + repo_config.insert("options".to_string(), PhpMixed::Array(IndexMap::new())); + } + let mut allow_ssl_downgrade = false; + if let Some(v) = repo_config.get("allow_ssl_downgrade") { + if v.as_bool() == Some(true) { + allow_ssl_downgrade = true; + } + } + + let options = repo_config + .get("options") + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_default() + .into_iter() + .map(|(k, v)| (k, *v)) + .collect::<IndexMap<String, PhpMixed>>(); + let mut url = repo_config + .get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + + // force url for packagist.org to repo.packagist.org + let mut match_packagist: Vec<String> = Vec::new(); + if Preg::is_match_with_matches( + r"{^(?P<proto>https?)://packagist\.org/?$}i", + &url, + &mut match_packagist, + )? { + let proto = match_packagist.get(1).cloned().unwrap_or_default(); + url = format!("{}://repo.packagist.org", proto); + } + + let base_url_trimmed = + Preg::replace(r"{(?:/[^/\\]+\.json)?(?:[?#].*)?$}", "", &url)?; + let base_url = base_url_trimmed.trim_end_matches('/').to_string(); + assert!(!base_url.is_empty()); + + let cache = Cache::new( + &*io, + format!( + "{}/{}", + config.get("cache-repo-dir").as_string().unwrap_or(""), + Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(url.clone()))?, + ), + ); + let version_parser = VersionParser::new(); + let loader = ArrayLoader::new_with_parser(version_parser.clone()); + + let r#loop = Loop::new(http_downloader.clone(), None); + + let mut this = Self { + inner, + repo_config, + options, + url, + base_url, + io, + http_downloader, + r#loop, + cache, + notify_url: None, + search_url: None, + providers_api_url: None, + has_providers: false, + providers_url: None, + list_url: None, + has_available_package_list: false, + available_packages: None, + available_package_patterns: None, + lazy_providers_url: None, + provider_listing: None, + loader, + allow_ssl_downgrade, + event_dispatcher, + source_mirrors: None, + dist_mirrors: None, + degraded_mode: false, + root_data: None, + has_partial_packages: false, + partial_packages_by_name: None, + displayed_warning_about_non_matching_package_index: false, + security_advisory_config: None, + freshMetadataUrls: IndexMap::new(), + packagesNotFoundCache: IndexMap::new(), + version_parser, + }; + this.cache + .set_read_only(config.get("cache-read-only").as_bool().unwrap_or(false)); + Ok(this) + } + + pub fn get_repo_name(&self) -> String { + format!("composer repo ({})", Url::sanitize(self.url.clone())) + } + + pub fn get_repo_config_pub(&self) -> IndexMap<String, PhpMixed> { + self.repo_config.clone() + } + + /// @inheritDoc + pub fn find_package( + &mut self, + name: String, + constraint: PhpMixed, + ) -> anyhow::Result<Option<Box<BasePackage>>> { + // this call initializes loadRootServerFile which is needed for the rest below to work + let has_providers = self.has_providers()?; + + let name = strtolower(&name); + let constraint: Box<dyn ConstraintInterface> = match constraint { + PhpMixed::String(s) => self.version_parser.parse_constraints(&s)?, + _ => { + // already a ConstraintInterface object passed as opaque PhpMixed + self.version_parser.parse_constraints("")? + } + }; + + if self.lazy_providers_url.is_some() { + if self.has_partial_packages()? + && self + .partial_packages_by_name + .as_ref() + .map_or(false, |m| m.contains_key(&name)) + { + let packages = self.what_provides(&name, None, None, IndexMap::new())?; + let packages_vec: Vec<Box<BasePackage>> = packages.into_values().collect(); + return Ok( + match self.filter_packages(packages_vec, Some(&*constraint), true) { + FindPackageReturn::Package(p) => Some(p), + _ => None, + }, + ); + } + + if self.has_available_package_list && !self.lazy_providers_repo_contains(&name)? { + return Ok(None); + } + + let mut map: IndexMap<String, Option<Box<dyn ConstraintInterface>>> = IndexMap::new(); + map.insert(name.clone(), Some(constraint)); + let packages = self.load_async_packages(map, None, None, IndexMap::new())?; + + if !packages.packages.is_empty() { + return Ok(packages.packages.into_iter().next().map(|(_, v)| v)); + } + + return Ok(None); + } + + if has_providers { + for provider_name in self.get_provider_names()? { + if name == provider_name { + let packages = + self.what_provides(&provider_name, None, None, IndexMap::new())?; + let packages_vec: Vec<Box<BasePackage>> = packages.into_values().collect(); + return Ok( + match self.filter_packages(packages_vec, Some(&*constraint), true) { + FindPackageReturn::Package(p) => Some(p), + _ => None, + }, + ); + } + } + + return Ok(None); + } + + Ok(self.inner.find_package(name, Some(constraint))) + } + + /// @inheritDoc + pub fn find_packages( + &mut self, + name: String, + constraint: Option<PhpMixed>, + ) -> anyhow::Result<Vec<Box<BasePackage>>> { + // this call initializes loadRootServerFile which is needed for the rest below to work + let has_providers = self.has_providers()?; + + let name = strtolower(&name); + let constraint: Option<Box<dyn ConstraintInterface>> = match constraint { + None => None, + Some(PhpMixed::String(s)) => Some(self.version_parser.parse_constraints(&s)?), + Some(_) => None, + }; + + if self.lazy_providers_url.is_some() { + if self.has_partial_packages()? + && self + .partial_packages_by_name + .as_ref() + .map_or(false, |m| m.contains_key(&name)) + { + let packages = self.what_provides(&name, None, None, IndexMap::new())?; + let packages_vec: Vec<Box<BasePackage>> = packages.into_values().collect(); + return Ok( + match self.filter_packages(packages_vec, constraint.as_deref(), false) { + FindPackageReturn::Packages(v) => v, + _ => vec![], + }, + ); + } + + if self.has_available_package_list && !self.lazy_providers_repo_contains(&name)? { + return Ok(vec![]); + } + + let mut map: IndexMap<String, Option<Box<dyn ConstraintInterface>>> = IndexMap::new(); + map.insert(name.clone(), constraint); + let result = self.load_async_packages(map, None, None, IndexMap::new())?; + + return Ok(result.packages.into_values().collect()); + } + + if has_providers { + for provider_name in self.get_provider_names()? { + if name == provider_name { + let packages = + self.what_provides(&provider_name, None, None, IndexMap::new())?; + let packages_vec: Vec<Box<BasePackage>> = packages.into_values().collect(); + return Ok( + match self.filter_packages(packages_vec, constraint.as_deref(), false) { + FindPackageReturn::Packages(v) => v, + _ => vec![], + }, + ); + } + } + + return Ok(vec![]); + } + + Ok(self.inner.find_packages(name, constraint)) + } + + fn filter_packages( + &self, + packages: Vec<Box<BasePackage>>, + constraint: Option<&dyn ConstraintInterface>, + return_first_match: bool, + ) -> FindPackageReturn { + if constraint.is_none() { + if return_first_match { + return match packages.into_iter().next() { + Some(p) => FindPackageReturn::Package(p), + None => FindPackageReturn::None, + }; + } + + return FindPackageReturn::Packages(packages); + } + let constraint = constraint.unwrap(); + + let mut filtered_packages: Vec<Box<BasePackage>> = Vec::new(); + + for package in packages.into_iter() { + let pkg_constraint = Constraint::new("==", package.get_version().to_string()); + + if constraint.matches(&pkg_constraint) { + if return_first_match { + return FindPackageReturn::Package(package); + } + + filtered_packages.push(package); + } + } + + if return_first_match { + return FindPackageReturn::None; + } + + FindPackageReturn::Packages(filtered_packages) + } + + pub fn get_packages(&mut self) -> anyhow::Result<Vec<Box<BasePackage>>> { + let has_providers = self.has_providers()?; + + if self.lazy_providers_url.is_some() { + if let Some(ref available_packages) = self.available_packages.clone() { + if self.available_package_patterns.is_none() { + let mut package_map: IndexMap< + String, + Option<Box<dyn ConstraintInterface>>, + > = IndexMap::new(); + for name in available_packages.values() { + package_map.insert( + name.clone(), + Some(Box::new(MatchAllConstraint::new()) + as Box<dyn ConstraintInterface>), + ); + } + + let result = + self.load_async_packages(package_map, None, None, IndexMap::new())?; + + return Ok(result.packages.into_values().collect()); + } + } + + if self.has_partial_packages()? { + if self.partial_packages_by_name.is_none() { + return Err(LogicException { + message: + "hasPartialPackages failed to initialize $this->partialPackagesByName" + .to_string(), + code: 0, + } + .into()); + } + + let partial = self.partial_packages_by_name.clone().unwrap(); + let flat: Vec<IndexMap<String, PhpMixed>> = + partial.into_values().flatten().collect(); + return self.create_packages_flat( + flat, + Some("packages.json inline packages".to_string()), + ); + } + + return Err(LogicException { + message: "Composer repositories that have lazy providers and no available-packages list can not load the complete list of packages, use getPackageNames instead.".to_string(), + code: 0, + }.into()); + } + + if has_providers { + return Err(LogicException { + message: "Composer repositories that have providers can not load the complete list of packages, use getPackageNames instead.".to_string(), + code: 0, + }.into()); + } + + Ok(self.inner.get_packages()) + } + + /// @param packageFilter Package pattern filter which can include "*" as a wildcard + pub fn get_package_names( + &mut self, + package_filter: Option<&str>, + ) -> anyhow::Result<Vec<String>> { + let has_providers = self.has_providers()?; + + let package_filter_regex: Option<String> = match package_filter { + Some(p) if !p.is_empty() => Some(BasePackage::package_name_to_regexp(p)), + _ => None, + }; + let filter_results = |results: Vec<String>| -> anyhow::Result<Vec<String>> { + match &package_filter_regex { + Some(regex) => Ok(Preg::grep(regex, &results)?), + None => Ok(results), + } + }; + + if self.lazy_providers_url.is_some() { + if let Some(ref available_packages) = self.available_packages { + let keys: Vec<String> = available_packages.keys().cloned().collect(); + return filter_results(keys); + } + + if self.list_url.is_some() { + // no need to call $filterResults here as the $packageFilter is applied in the function itself + return self.load_package_list(package_filter); + } + + if self.has_partial_packages()? && self.partial_packages_by_name.is_some() { + let keys: Vec<String> = self + .partial_packages_by_name + .as_ref() + .unwrap() + .keys() + .cloned() + .collect(); + return filter_results(keys); + } + + return Ok(vec![]); + } + + if has_providers { + return filter_results(self.get_provider_names()?); + } + + let mut names: Vec<String> = Vec::new(); + for package in self.get_packages()? { + names.push(package.get_pretty_name().to_string()); + } + + filter_results(names) + } + + fn get_vendor_names(&mut self) -> anyhow::Result<Vec<String>> { + let cache_key = "vendor-list.txt"; + let cache_age = self.cache.get_age(cache_key); + if let Some(age) = cache_age { + if age < 600 { + if let Some(cached_data) = self.cache.read(cache_key) { + let cached_data: Vec<String> = + cached_data.split('\n').map(|s| s.to_string()).collect(); + return Ok(cached_data); + } + } + } + + let names = self.get_package_names(None)?; + + let mut uniques: IndexMap<String, bool> = IndexMap::new(); + for name in &names { + let vendor = name.splitn(2, '/').next().unwrap_or("").to_string(); + uniques.insert(vendor, true); + } + + let vendors: Vec<String> = uniques.keys().cloned().collect(); + + if !self.cache.is_read_only() { + self.cache.write(cache_key, &vendors.join("\n")); + } + + Ok(vendors) + } + + fn load_package_list( + &mut self, + package_filter: Option<&str>, + ) -> anyhow::Result<Vec<String>> { + if self.list_url.is_none() { + return Err(LogicException { + message: "Make sure to call loadRootServerFile before loadPackageList".to_string(), + code: 0, + } + .into()); + } + + let mut url = self.list_url.clone().unwrap(); + if let Some(filter) = package_filter { + if !filter.is_empty() { + url.push_str(&format!("?filter={}", urlencode(filter))); + let result = self + .http_downloader + .get(&url, &self.options)? + .decode_json()?; + let package_names: Vec<String> = result + .as_array() + .and_then(|a| a.get("packageNames")) + .and_then(|v| v.as_list()) + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + + return Ok(package_names); + } + } + + let cache_key = "package-list.txt"; + let cache_age = self.cache.get_age(cache_key); + if let Some(age) = cache_age { + if age < 600 { + if let Some(cached_data) = self.cache.read(cache_key) { + let cached_data: Vec<String> = + cached_data.split('\n').map(|s| s.to_string()).collect(); + return Ok(cached_data); + } + } + } + + let result = self + .http_downloader + .get(&url, &self.options)? + .decode_json()?; + let package_names: Vec<String> = result + .as_array() + .and_then(|a| a.get("packageNames")) + .and_then(|v| v.as_list()) + .map(|l| { + l.iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + if !self.cache.is_read_only() { + self.cache.write(cache_key, &package_names.join("\n")); + } + + Ok(package_names) + } + + pub fn load_packages( + &mut self, + mut package_name_map: IndexMap<String, Option<Box<dyn ConstraintInterface>>>, + acceptable_stabilities: IndexMap<String, i64>, + stability_flags: IndexMap<String, i64>, + already_loaded: IndexMap<String, IndexMap<String, Box<dyn PackageInterface>>>, + ) -> anyhow::Result<LoadPackagesResult> { + // this call initializes loadRootServerFile which is needed for the rest below to work + let has_providers = self.has_providers()?; + + if !has_providers && !self.has_partial_packages()? && self.lazy_providers_url.is_none() { + return self.inner.load_packages( + package_name_map, + acceptable_stabilities, + stability_flags, + already_loaded, + ); + } + + let mut packages: IndexMap<String, Box<BasePackage>> = IndexMap::new(); + let mut names_found: IndexMap<String, bool> = IndexMap::new(); + + if has_providers || self.has_partial_packages()? { + let names: Vec<String> = package_name_map.keys().cloned().collect(); + for name in names { + let mut matches: IndexMap<String, Box<BasePackage>> = IndexMap::new(); + + // if a repo has no providers but only partial packages and the partial packages are missing + // then we don't want to call whatProvides as it would try to load from the providers and fail + if !has_providers + && !self + .partial_packages_by_name + .as_ref() + .map_or(false, |m| m.contains_key(&name)) + { + continue; + } + + let candidates = self.what_provides( + &name, + Some(&acceptable_stabilities), + Some(&stability_flags), + already_loaded.clone(), + )?; + let constraint = package_name_map.get(&name).cloned().flatten(); + for (_uid, candidate) in candidates.iter() { + if candidate.get_name() != name { + return Err(LogicException { + message: "whatProvides should never return a package with a different name than the requested one".to_string(), + code: 0, + }.into()); + } + names_found.insert(name.clone(), true); + + let matches_constraint = match &constraint { + None => true, + Some(c) => { + let pkg_c = + Constraint::new("==", candidate.get_version().to_string()); + c.matches(&pkg_c) + } + }; + if matches_constraint { + let hash_c = spl_object_hash(&**candidate); + matches.insert(hash_c, dyn_clone_box(&**candidate)); + if let Some(alias) = candidate.as_alias_package() { + let aliased = alias.get_alias_of(); + let aliased_hash = spl_object_hash(aliased); + if !matches.contains_key(&aliased_hash) { + matches.insert(aliased_hash, dyn_clone_box(aliased)); + } + } + } + } + + // add aliases of matched packages even if they did not match the constraint + for (_uid, candidate) in candidates.iter() { + if let Some(alias) = candidate.as_alias_package() { + let aliased = alias.get_alias_of(); + let aliased_hash = spl_object_hash(aliased); + if matches.contains_key(&aliased_hash) { + let hash_c = spl_object_hash(&**candidate); + matches.insert(hash_c, dyn_clone_box(&**candidate)); + } + } + } + for (k, v) in matches.into_iter() { + packages.insert(k, v); + } + + package_name_map.shift_remove(&name); + } + } + + if self.lazy_providers_url.is_some() && !package_name_map.is_empty() { + if self.has_available_package_list { + let names: Vec<String> = package_name_map.keys().cloned().collect(); + for name in names { + if !self.lazy_providers_repo_contains(&strtolower(&name))? { + package_name_map.shift_remove(&name); + } + } + } + + let result = self.load_async_packages( + package_name_map, + Some(&acceptable_stabilities), + Some(&stability_flags), + already_loaded, + )?; + for (k, v) in result.packages.into_iter() { + packages.insert(k, v); + } + for (k, v) in result.names_found.into_iter() { + names_found.insert(k, v); + } + } + + Ok(LoadPackagesResult { + names_found: names_found.keys().cloned().collect(), + packages, + }) + } + + /// @inheritDoc + pub fn search( + &mut self, + query: String, + mode: i64, + r#type: Option<String>, + ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> { + self.load_root_server_file(Some(600))?; + + if let Some(search_url) = self.search_url.clone() { + if mode == SEARCH_FULLTEXT { + let url = search_url + .replace("%query%", &urlencode(&query)) + .replace("%type%", r#type.as_deref().unwrap_or("")); + + let search = self + .http_downloader + .get(&url, &self.options)? + .decode_json()?; + + let results_arr = search + .as_array() + .and_then(|a| a.get("results")) + .and_then(|v| v.as_list()) + .cloned() + .unwrap_or_default(); + if results_arr.is_empty() { + return Ok(vec![]); + } + + let mut results: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + for result in results_arr.iter() { + let arr = match result.as_array() { + Some(a) => a, + None => continue, + }; + // do not show virtual packages in results as they are not directly useful from a composer perspective + if let Some(v) = arr.get("virtual") { + // PHP's `empty()` is false when the value is truthy + let is_empty = match v { + PhpMixed::Null => true, + PhpMixed::Bool(false) => true, + PhpMixed::Int(0) => true, + PhpMixed::Float(f) if *f == 0.0 => true, + PhpMixed::String(s) if s.is_empty() || s == "0" => true, + PhpMixed::List(l) if l.is_empty() => true, + PhpMixed::Array(a) if a.is_empty() => true, + _ => false, + }; + if !is_empty { + continue; + } + } + + results.push( + arr.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(), + ); + } + + return Ok(results); + } + } + + if mode == SEARCH_VENDOR { + let mut results: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + let parts = Preg::split(r"{\s+}", &query)?; + let regex = format!("{{(?:{})}}i", parts.join("|")); + + let vendor_names = self.get_vendor_names()?; + for name in Preg::grep(®ex, &vendor_names)? { + let mut entry = IndexMap::new(); + entry.insert("name".to_string(), PhpMixed::String(name)); + entry.insert( + "description".to_string(), + PhpMixed::String(String::new()), + ); + results.push(entry); + } + + return Ok(results); + } + + if self.has_providers()? || self.lazy_providers_url.is_some() { + // optimize search for "^foo/bar" where at least "^foo/" is present by loading this directly from the listUrl if present + let mut match_groups: Vec<String> = Vec::new(); + if Preg::is_match_strict_groups( + r"{^\^(?P<query>(?P<vendor>[a-z0-9_.-]+)/[a-z0-9_.-]*)\*?$}i", + &query, + &mut match_groups, + )? + && self.list_url.is_some() + { + let q = match_groups.get(1).cloned().unwrap_or_default(); + let vendor = match_groups.get(2).cloned().unwrap_or_default(); + let url = format!( + "{}?vendor={}&filter={}", + self.list_url.as_ref().unwrap(), + urlencode(&vendor), + urlencode(&format!("{}*", q)), + ); + let result = self + .http_downloader + .get(&url, &self.options)? + .decode_json()?; + + let mut results: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + if let Some(list) = result + .as_array() + .and_then(|a| a.get("packageNames")) + .and_then(|v| v.as_list()) + { + for name_mixed in list.iter() { + if let Some(name) = name_mixed.as_string() { + let mut entry = IndexMap::new(); + entry.insert( + "name".to_string(), + PhpMixed::String(name.to_string()), + ); + entry.insert( + "description".to_string(), + PhpMixed::String(String::new()), + ); + results.push(entry); + } + } + } + + return Ok(results); + } + + let mut results: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + let parts = Preg::split(r"{\s+}", &query)?; + let regex = format!("{{(?:{})}}i", parts.join("|")); + + let package_names = self.get_package_names(None)?; + for name in Preg::grep(®ex, &package_names)? { + let mut entry = IndexMap::new(); + entry.insert("name".to_string(), PhpMixed::String(name)); + entry.insert( + "description".to_string(), + PhpMixed::String(String::new()), + ); + results.push(entry); + } + + return Ok(results); + } + + Ok(self.inner.search(query, mode, None)) + } + + pub fn has_security_advisories(&mut self) -> anyhow::Result<bool> { + self.load_root_server_file(Some(600))?; + + Ok(self.security_advisory_config.as_ref().map_or(false, |c| { + c.metadata || c.api_url.is_some() + })) + } + + /// @inheritDoc + pub fn get_security_advisories( + &mut self, + mut package_constraint_map: IndexMap<String, Box<dyn ConstraintInterface>>, + allow_partial_advisories: bool, + ) -> anyhow::Result<SecurityAdvisoryResult> { + self.load_root_server_file(Some(600))?; + if self.security_advisory_config.is_none() { + return Ok(SecurityAdvisoryResult { + names_found: vec![], + advisories: IndexMap::new(), + }); + } + + let mut advisories: IndexMap<String, Vec<PartialOrSecurityAdvisory>> = IndexMap::new(); + let mut names_found: IndexMap<String, bool> = IndexMap::new(); + + let api_url = self + .security_advisory_config + .as_ref() + .and_then(|c| c.api_url.clone()); + + // respect available-package-patterns / available-packages directives from the repo + if self.has_available_package_list { + let names: Vec<String> = package_constraint_map.keys().cloned().collect(); + for name in names { + if !self.lazy_providers_repo_contains(&strtolower(&name))? { + package_constraint_map.shift_remove(&name); + } + } + } + + let parser = VersionParser::new(); + let repo_name = self.get_repo_name(); + let create = |data: &IndexMap<String, PhpMixed>, + name: &str, + package_constraint_map: &IndexMap< + String, + Box<dyn ConstraintInterface>, + >| + -> anyhow::Result<Option<PartialOrSecurityAdvisory>> { + let advisory = + PartialSecurityAdvisory::create(name.to_string(), data.clone(), &parser)?; + let is_full = matches!(advisory, PartialOrSecurityAdvisory::Full(_)); + if !allow_partial_advisories && !is_full { + let data_mixed = PhpMixed::Array( + data.iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone()))) + .collect(), + ); + return Err(RuntimeException { + message: format!( + "Advisory for {} could not be loaded as a full advisory from {}{}{}", + name, + repo_name, + PHP_EOL, + var_export(&data_mixed, true), + ), + code: 0, + } + .into()); + } + let affected_versions = match &advisory { + PartialOrSecurityAdvisory::Partial(p) => &p.affected_versions, + PartialOrSecurityAdvisory::Full(p) => &p.inner.affected_versions, + }; + let constraint = package_constraint_map.get(name).map(|c| &**c); + if let Some(c) = constraint { + if !affected_versions.matches_constraint(c) { + return Ok(None); + } + } else { + return Ok(None); + } + + Ok(Some(advisory)) + }; + + if self + .security_advisory_config + .as_ref() + .map_or(false, |c| c.metadata) + && (allow_partial_advisories || api_url.is_none()) + { + let mut promises: Vec<Box<dyn PromiseInterface>> = Vec::new(); + let names: Vec<String> = package_constraint_map.keys().cloned().collect(); + for name in names { + let name = strtolower(&name); + + // skip platform packages, root package and composer-plugin-api + if PlatformRepository::is_platform_package(&name) || name == "__root__" { + continue; + } + + let promise = self + .start_cached_async_download(&name, Some(&name))? + .then_boxed(Box::new({ + let advisories_ptr = &mut advisories as *mut _; + let names_found_ptr = &mut names_found as *mut _; + let package_constraint_map_ptr = + &mut package_constraint_map as *mut _; + let name = name.clone(); + let create = &create; + move |spec: PhpMixed| -> anyhow::Result<()> { + // [$response] = $spec; + let response = spec + .as_list() + .and_then(|l| l.first()) + .map(|b| (**b).clone()) + .unwrap_or(PhpMixed::Null); + let response_arr = match response.as_array() { + Some(a) => a.clone(), + None => return Ok(()), + }; + let sec_advs = match response_arr.get("security-advisories") { + Some(v) => v.clone(), + None => return Ok(()), + }; + let sec_advs_arr = match sec_advs.as_array() { + Some(a) => a.clone(), + None => return Ok(()), + }; + unsafe { + (*names_found_ptr).insert(name.clone(), true); + } + if !sec_advs_arr.is_empty() { + let mut entries: Vec<PartialOrSecurityAdvisory> = Vec::new(); + for (_k, data_mixed) in sec_advs_arr.iter() { + if let Some(data) = data_mixed.as_array() { + let data_map: IndexMap<String, PhpMixed> = data + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + let pcm: &IndexMap< + String, + Box<dyn ConstraintInterface>, + > = unsafe { &*package_constraint_map_ptr }; + if let Some(adv) = create(&data_map, &name, pcm)? { + entries.push(adv); + } + } + } + unsafe { + (*advisories_ptr).insert(name.clone(), entries); + } + } + unsafe { + (*package_constraint_map_ptr).shift_remove(&name); + } + Ok(()) + } + })); + promises.push(promise); + } + + self.r#loop.wait(promises, None)?; + } + + if let Some(api_url) = api_url { + if !package_constraint_map.is_empty() { + let mut options = self.options.clone(); + let http_entry = options + .entry("http".to_string()) + .or_insert(PhpMixed::Array(IndexMap::new())); + if let PhpMixed::Array(ref mut http_map) = http_entry { + http_map.insert( + "method".to_string(), + Box::new(PhpMixed::String("POST".to_string())), + ); + if let Some(header_box) = http_map.get("header") { + // cast to array + let arr = match &**header_box { + PhpMixed::List(l) => l.clone(), + other => vec![Box::new(other.clone())], + }; + http_map.insert("header".to_string(), Box::new(PhpMixed::List(arr))); + } + let mut headers = match http_map.get("header") { + Some(b) => match &**b { + PhpMixed::List(l) => l.clone(), + _ => vec![], + }, + None => vec![], + }; + headers.push(Box::new(PhpMixed::String( + "Content-type: application/x-www-form-urlencoded".to_string(), + ))); + http_map.insert( + "header".to_string(), + Box::new(PhpMixed::List(headers)), + ); + http_map.insert("timeout".to_string(), Box::new(PhpMixed::Int(10))); + let packages_list: Vec<(String, String)> = package_constraint_map + .keys() + .map(|k| ("packages".to_string(), k.clone())) + .collect(); + let body = http_build_query( + &packages_list + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect::<Vec<_>>(), + "&", + "=", + ); + http_map.insert( + "content".to_string(), + Box::new(PhpMixed::String(body)), + ); + } + + let response = self.http_downloader.get(&api_url, &options)?; + let mut warned = false; + let decoded = response.decode_json()?; + let advisories_response = decoded + .as_array() + .and_then(|a| a.get("advisories")) + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_default(); + for (name, list_box) in advisories_response.iter() { + if !package_constraint_map.contains_key(name) { + if !warned { + self.io.write_error(&format!( + "<warning>{} returned names which were not requested in response to the security-advisories API. {} was not requested but is present in the response. Requested names were: {}</warning>", + self.get_repo_name(), + name, + package_constraint_map.keys().cloned().collect::<Vec<_>>().join(", "), + )); + warned = true; + } + continue; + } + let list = match list_box.as_list() { + Some(l) => l.clone(), + None => continue, + }; + if !list.is_empty() { + let mut entries: Vec<PartialOrSecurityAdvisory> = Vec::new(); + for data_mixed in list.iter() { + if let Some(data) = data_mixed.as_array() { + let data_map: IndexMap<String, PhpMixed> = data + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + if let Some(adv) = + create(&data_map, name, &package_constraint_map)? + { + entries.push(adv); + } + } + } + advisories.insert(name.clone(), entries); + } + names_found.insert(name.clone(), true); + } + } + } + + let advisories_filtered: IndexMap<String, Vec<PartialOrSecurityAdvisory>> = advisories + .into_iter() + .filter(|(_, adv)| !adv.is_empty()) + .collect(); + + Ok(SecurityAdvisoryResult { + names_found: names_found.keys().cloned().collect(), + advisories: advisories_filtered, + }) + } + + pub fn get_providers( + &mut self, + package_name: &str, + ) -> anyhow::Result<IndexMap<String, IndexMap<String, PhpMixed>>> { + self.load_root_server_file(None)?; + let mut result: IndexMap<String, IndexMap<String, PhpMixed>> = IndexMap::new(); + + if let Some(providers_api_url) = self.providers_api_url.clone() { + let api_result = match self.http_downloader.get( + &providers_api_url.replace("%package%", package_name), + &self.options, + ) { + Ok(resp) => resp.decode_json()?, + Err(e) => { + if let Some(te) = e.downcast_ref::<TransportException>() { + if te.get_status_code() == 404 { + return Ok(result); + } + } + return Err(e); + } + }; + + if let Some(providers) = api_result + .as_array() + .and_then(|a| a.get("providers")) + .and_then(|v| v.as_list()) + { + for provider_mixed in providers.iter() { + if let Some(provider) = provider_mixed.as_array() { + if let Some(name) = provider.get("name").and_then(|v| v.as_string()) { + let entry: IndexMap<String, PhpMixed> = provider + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + result.insert(name.to_string(), entry); + } + } + } + } + + return Ok(result); + } + + if self.has_partial_packages()? { + if self.partial_packages_by_name.is_none() { + return Err(LogicException { + message: + "hasPartialPackages failed to initialize $this->partialPackagesByName" + .to_string(), + code: 0, + } + .into()); + } + for (_name, versions) in self.partial_packages_by_name.as_ref().unwrap().iter() { + for candidate in versions.iter() { + let candidate_name = candidate + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + if result.contains_key(&candidate_name) + || !candidate + .get("provide") + .and_then(|v| v.as_array()) + .map_or(false, |a| a.contains_key(package_name)) + { + continue; + } + let mut entry: IndexMap<String, PhpMixed> = IndexMap::new(); + entry.insert( + "name".to_string(), + PhpMixed::String(candidate_name.clone()), + ); + entry.insert( + "description".to_string(), + candidate + .get("description") + .cloned() + .unwrap_or(PhpMixed::String(String::new())), + ); + entry.insert( + "type".to_string(), + candidate + .get("type") + .cloned() + .unwrap_or(PhpMixed::String(String::new())), + ); + result.insert(candidate_name, entry); + } + } + } + + if !self.inner.is_packages_empty() { + for (k, v) in self.inner.get_providers(package_name) { + result.insert(k, v); + } + } + + Ok(result) + } + + fn get_provider_names(&mut self) -> anyhow::Result<Vec<String>> { + self.load_root_server_file(None)?; + + if self.provider_listing.is_none() { + let data = self.load_root_server_file(None)?; + if let RootData::Data(arr) = &data { + let arr_clone = arr.clone(); + self.load_provider_listings(&arr_clone)?; + } + } + + if self.lazy_providers_url.is_some() { + // Can not determine list of provided packages for lazy repositories + return Ok(vec![]); + } + + if self.providers_url.is_some() && self.provider_listing.is_some() { + return Ok(self + .provider_listing + .as_ref() + .unwrap() + .keys() + .cloned() + .collect()); + } + + Ok(vec![]) + } + + fn configure_package_transport_options(&self, package: &mut dyn PackageInterface) { + for url in package.get_dist_urls() { + if url.starts_with(&self.base_url) { + package.set_transport_options(self.options.clone()); + + return; + } + } + } + + fn has_providers(&mut self) -> anyhow::Result<bool> { + self.load_root_server_file(None)?; + + Ok(self.has_providers) + } + + /// @param name package name + fn what_provides( + &mut self, + name: &str, + acceptable_stabilities: Option<&IndexMap<String, i64>>, + stability_flags: Option<&IndexMap<String, i64>>, + already_loaded: IndexMap<String, IndexMap<String, Box<dyn PackageInterface>>>, + ) -> anyhow::Result<IndexMap<String, Box<BasePackage>>> { + let mut packages_source: Option<String> = None; + let packages: IndexMap<String, PhpMixed>; + let loading_partial_package: bool; + if !self.has_partial_packages()? + || !self + .partial_packages_by_name + .as_ref() + .map_or(false, |m| m.contains_key(name)) + { + // skip platform packages, root package and composer-plugin-api + if PlatformRepository::is_platform_package(name) || name == "__root__" { + return Ok(IndexMap::new()); + } + + if self.provider_listing.is_none() { + let data = self.load_root_server_file(None)?; + if let RootData::Data(arr) = &data { + let arr_clone = arr.clone(); + self.load_provider_listings(&arr_clone)?; + } + } + + let mut use_last_modified_check = false; + let hash_opt: Option<String>; + let url: String; + let cache_key: String; + if self.lazy_providers_url.is_some() + && !self + .provider_listing + .as_ref() + .map_or(false, |m| m.contains_key(name)) + { + hash_opt = None; + url = self + .lazy_providers_url + .as_ref() + .unwrap() + .replace("%package%", name); + cache_key = format!("provider-{}.json", strtr(name, "/", "$")); + use_last_modified_check = true; + } else if let Some(providers_url) = self.providers_url.clone() { + // package does not exist in this repo + if !self + .provider_listing + .as_ref() + .map_or(false, |m| m.contains_key(name)) + { + return Ok(IndexMap::new()); + } + + let listing = self.provider_listing.as_ref().unwrap(); + let entry = listing.get(name).unwrap(); + hash_opt = Some(entry.sha256.clone()); + url = providers_url + .replace("%package%", name) + .replace("%hash%", &entry.sha256); + cache_key = format!("provider-{}.json", strtr(name, "/", "$")); + } else { + return Ok(IndexMap::new()); + } + + let mut packages_opt: Option<IndexMap<String, PhpMixed>> = None; + if !use_last_modified_check + && hash_opt.is_some() + && self.cache.sha256(&cache_key).as_deref() == hash_opt.as_deref() + { + if let Some(raw) = self.cache.read(&cache_key) { + let decoded = json_decode(&raw, true)?; + if let Some(arr) = decoded.as_array() { + let map: IndexMap<String, PhpMixed> = arr + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + packages_opt = Some(map); + packages_source = Some(format!( + "cached file ({} originating from {})", + cache_key, + Url::sanitize(url.clone()) + )); + } + } + } else if use_last_modified_check { + if let Some(contents_raw) = self.cache.read(&cache_key) { + let contents = json_decode(&contents_raw, true)?; + let contents_arr = contents.as_array().cloned(); + // we already loaded some packages from this file, so assume it is fresh and avoid fetching it again + if already_loaded.contains_key(name) { + if let Some(arr) = &contents_arr { + let map: IndexMap<String, PhpMixed> = arr + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + packages_opt = Some(map); + packages_source = Some(format!( + "cached file ({} originating from {})", + cache_key, + Url::sanitize(url.clone()) + )); + } + } else if let Some(arr) = &contents_arr { + if let Some(last_modified) = + arr.get("last-modified").and_then(|v| v.as_string()) + { + let response = self.fetch_file_if_last_modified( + &url, + &cache_key, + last_modified, + )?; + match response { + FetchFileIfLastModifiedResult::NotModified => { + let map: IndexMap<String, PhpMixed> = arr + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + packages_opt = Some(map); + packages_source = Some(format!( + "cached file ({} originating from {})", + cache_key, + Url::sanitize(url.clone()) + )); + } + FetchFileIfLastModifiedResult::Data(data) => { + packages_opt = Some(data); + packages_source = Some(format!( + "downloaded file ({})", + Url::sanitize(url.clone()) + )); + } + } + } + } + } + } + + if packages_opt.is_none() { + match self.fetch_file( + &url, + Some(&cache_key), + hash_opt.as_deref(), + use_last_modified_check, + ) { + Ok(p) => { + packages_opt = Some(p); + packages_source = Some(format!( + "downloaded file ({})", + Url::sanitize(url.clone()) + )); + } + Err(e) => { + // 404s are acceptable for lazy provider repos + if let Some(te) = e.downcast_ref::<TransportException>() { + let status_code = te.get_status_code(); + if self.lazy_providers_url.is_some() + && in_array( + PhpMixed::Int(status_code), + &PhpMixed::List(vec![ + Box::new(PhpMixed::Int(404)), + Box::new(PhpMixed::Int(499)), + ]), + true, + ) + { + let mut p: IndexMap<String, PhpMixed> = IndexMap::new(); + p.insert( + "packages".to_string(), + PhpMixed::Array(IndexMap::new()), + ); + packages_opt = Some(p); + packages_source = Some(format!( + "not-found file ({})", + Url::sanitize(url.clone()) + )); + if status_code == 499 { + self.io.error(&format!( + "<warning>{}</warning>", + te.get_message() + )); + } + } else { + return Err(e); + } + } else { + return Err(e); + } + } + } + } + + packages = packages_opt.unwrap(); + loading_partial_package = false; + } else { + let mut versions_map: IndexMap<String, PhpMixed> = IndexMap::new(); + let mut packages_inner: IndexMap<String, PhpMixed> = IndexMap::new(); + let entries = self + .partial_packages_by_name + .as_ref() + .unwrap() + .get(name) + .cloned() + .unwrap_or_default(); + let entries_mixed: Vec<Box<PhpMixed>> = entries + .into_iter() + .map(|m| { + Box::new(PhpMixed::Array( + m.into_iter().map(|(k, v)| (k, Box::new(v))).collect(), + )) + }) + .collect(); + versions_map.insert("versions".to_string(), PhpMixed::List(entries_mixed)); + packages_inner.insert( + "packages".to_string(), + PhpMixed::Array( + versions_map + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), + ), + ); + packages = packages_inner; + packages_source = Some(format!( + "root file ({})", + Url::sanitize(self.get_packages_json_url()) + )); + loading_partial_package = true; + } + + let mut result: IndexMap<String, Box<BasePackage>> = IndexMap::new(); + let mut versions_to_load: IndexMap<String, IndexMap<String, PhpMixed>> = IndexMap::new(); + let packages_inner = packages + .get("packages") + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_default(); + for (_pkg_key, versions_mixed) in packages_inner.iter() { + // $versions can be either array<string, array> or list<array> + let iter_versions: Vec<PhpMixed> = match &**versions_mixed { + PhpMixed::Array(a) => a.values().map(|v| (**v).clone()).collect(), + PhpMixed::List(l) => l.iter().map(|v| (**v).clone()).collect(), + _ => continue, + }; + for version_mixed in iter_versions.iter() { + let version_arr = match version_mixed.as_array() { + Some(a) => a, + None => continue, + }; + let mut version: IndexMap<String, PhpMixed> = version_arr + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + let normalized_name = strtolower( + version + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or(""), + ); + + // only load the actual named package, not other packages that might find themselves in the same file + if normalized_name != name { + continue; + } + + if !loading_partial_package + && self.has_partial_packages()? + && self + .partial_packages_by_name + .as_ref() + .map_or(false, |m| m.contains_key(&normalized_name)) + { + continue; + } + + let uid_key = match version.get("uid") { + Some(PhpMixed::Int(i)) => i.to_string(), + Some(PhpMixed::String(s)) => s.clone(), + Some(other) => format!("{:?}", other), + None => continue, + }; + if !versions_to_load.contains_key(&uid_key) { + let has_version_normalized = version.contains_key("version_normalized"); + if !has_version_normalized { + let v = version + .get("version") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let normalized = self.version_parser.normalize(&v, None)?; + version.insert( + "version_normalized".to_string(), + PhpMixed::String(normalized), + ); + } else if version + .get("version_normalized") + .and_then(|v| v.as_string()) + .map_or(false, |s| s == VersionParser::DEFAULT_BRANCH_ALIAS) + { + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained VersionParser::DEFAULT_BRANCH_ALIAS, we renormalize it + let v = version + .get("version") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let normalized = self.version_parser.normalize(&v, None)?; + version.insert( + "version_normalized".to_string(), + PhpMixed::String(normalized), + ); + } + + let version_normalized = version + .get("version_normalized") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + // avoid loading packages which have already been loaded + if already_loaded + .get(name) + .map_or(false, |m| m.contains_key(&version_normalized)) + { + continue; + } + + if self.is_version_acceptable( + None, + &normalized_name, + &version, + acceptable_stabilities, + stability_flags, + )? { + versions_to_load.insert(uid_key, version); + } + } + } + } + + // load acceptable packages in the providers + let versions_to_load_vec: Vec<IndexMap<String, PhpMixed>> = + versions_to_load.values().cloned().collect(); + let loaded_packages = + self.create_packages_flat(versions_to_load_vec, packages_source)?; + let uids: Vec<String> = versions_to_load.keys().cloned().collect(); + + for (index, mut package) in loaded_packages.into_iter().enumerate() { + package.set_repository_self(); + let uid = &uids[index]; + + if let Some(alias) = package.as_alias_package_mut() { + let aliased = alias.get_alias_of_mut(); + aliased.set_repository_self(); + + result.insert(uid.clone(), dyn_clone_box(aliased)); + result.insert(format!("{}-alias", uid), package); + } else { + result.insert(uid.clone(), package); + } + } + + Ok(result) + } + + /// @inheritDoc + pub fn initialize(&mut self) -> anyhow::Result<()> { + self.inner.initialize()?; + + let repo_data = self.load_data_from_server()?; + + let source = format!("root file ({})", Url::sanitize(self.get_packages_json_url())); + for package in self.create_packages_flat(repo_data, Some(source))? { + self.add_package(package); + } + Ok(()) + } + + /// Adds a new package to the repository + pub fn add_package(&mut self, mut package: Box<BasePackage>) { + // configurePackageTransportOptions(*package); + self.configure_package_transport_options(&mut *package); + self.inner.add_package(package); + } + + /// @param packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only packages matching it will be loaded + fn load_async_packages( + &mut self, + mut package_names: IndexMap<String, Option<Box<dyn ConstraintInterface>>>, + acceptable_stabilities: Option<&IndexMap<String, i64>>, + stability_flags: Option<&IndexMap<String, i64>>, + already_loaded: IndexMap<String, IndexMap<String, Box<dyn PackageInterface>>>, + ) -> anyhow::Result<LoadAsyncPackagesResult> { + self.load_root_server_file(None)?; + + let mut packages: IndexMap<String, Box<BasePackage>> = IndexMap::new(); + let mut names_found: IndexMap<String, bool> = IndexMap::new(); + let mut promises: Vec<Box<dyn PromiseInterface>> = Vec::new(); + + if self.lazy_providers_url.is_none() { + return Err(LogicException { + message: "loadAsyncPackages only supports v2 protocol composer repos with a metadata-url".to_string(), + code: 0, + }.into()); + } + + // load ~dev versions of the packages as well if needed + let names_snapshot: Vec<String> = package_names.keys().cloned().collect(); + for name in names_snapshot { + let constraint = package_names.get(&name).cloned().flatten(); + if acceptable_stabilities.is_none() + || stability_flags.is_none() + || StabilityFilter::is_package_acceptable( + acceptable_stabilities.unwrap(), + stability_flags.unwrap(), + &[name.clone()], + "dev", + ) + { + package_names.insert(format!("{}~dev", name), constraint); + } + // if only dev stability is requested, we skip loading the non dev file + if acceptable_stabilities + .map_or(false, |m| m.contains_key("dev") && m.len() == 1) + && stability_flags.map_or(false, |m| m.is_empty()) + { + package_names.shift_remove(&name); + } + } + + let names_iter: Vec<(String, Option<Box<dyn ConstraintInterface>>)> = package_names + .iter() + .map(|(k, v)| { + let cloned: Option<Box<dyn ConstraintInterface>> = + v.as_ref().map(|c| dyn_clone_constraint(&**c)); + (k.clone(), cloned) + }) + .collect(); + for (name, constraint) in names_iter { + let name = strtolower(&name); + + let real_name = Preg::replace(r"{~dev$}", "", &name)?; + // skip platform packages, root package and composer-plugin-api + if PlatformRepository::is_platform_package(&real_name) || real_name == "__root__" { + continue; + } + + let already_loaded_clone = already_loaded.clone(); + let acceptable_stabilities_clone = acceptable_stabilities.cloned(); + let stability_flags_clone = stability_flags.cloned(); + let version_parser = self.version_parser.clone(); + let promise = self + .start_cached_async_download(&name, Some(&real_name))? + .then_boxed(Box::new({ + let packages_ptr = &mut packages as *mut _; + let names_found_ptr = &mut names_found as *mut _; + let real_name = real_name.clone(); + let constraint = constraint; + move |spec: PhpMixed| -> anyhow::Result<()> { + let spec_list = spec.as_list().cloned().unwrap_or_default(); + let response = spec_list + .first() + .map(|b| (**b).clone()) + .unwrap_or(PhpMixed::Null); + let packages_source_val = spec_list + .get(1) + .map(|b| (**b).clone()) + .unwrap_or(PhpMixed::Null); + let packages_source: Option<String> = + packages_source_val.as_string().map(|s| s.to_string()); + if response.is_null() { + return Ok(()); + } + let response_arr = match response.as_array() { + Some(a) => a.clone(), + None => return Ok(()), + }; + let inner_packages = response_arr.get("packages"); + let versions_mixed = match inner_packages + .and_then(|v| v.as_array()) + .and_then(|a| a.get(&real_name)) + .cloned() + { + Some(b) => *b, + None => return Ok(()), + }; + + let mut versions: Vec<IndexMap<String, PhpMixed>> = match &versions_mixed { + PhpMixed::List(l) => l + .iter() + .filter_map(|v| { + v.as_array().map(|a| { + a.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect::<IndexMap<String, PhpMixed>>() + }) + }) + .collect(), + PhpMixed::Array(a) => a + .values() + .filter_map(|v| { + v.as_array().map(|a| { + a.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect::<IndexMap<String, PhpMixed>>() + }) + }) + .collect(), + _ => return Ok(()), + }; + + let minified = response_arr + .get("minified") + .and_then(|v| v.as_string()) + .map_or(false, |s| s == "composer/2.0"); + if minified { + versions = MetadataMinifier::expand(versions); + } + + unsafe { + (*names_found_ptr).insert(real_name.clone(), true); + } + let mut versions_to_load: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + for version in versions.into_iter() { + let mut version = version; + let has_vn = version.contains_key("version_normalized"); + if !has_vn { + let v = version + .get("version") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let normalized = version_parser.normalize(&v, None)?; + version.insert( + "version_normalized".to_string(), + PhpMixed::String(normalized), + ); + } else if version + .get("version_normalized") + .and_then(|v| v.as_string()) + .map_or(false, |s| s == VersionParser::DEFAULT_BRANCH_ALIAS) + { + // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained VersionParser::DEFAULT_BRANCH_ALIAS, we renormalize it + let v = version + .get("version") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let normalized = version_parser.normalize(&v, None)?; + version.insert( + "version_normalized".to_string(), + PhpMixed::String(normalized), + ); + } + + let version_normalized = version + .get("version_normalized") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + // avoid loading packages which have already been loaded + if already_loaded_clone + .get(&real_name) + .map_or(false, |m| m.contains_key(&version_normalized)) + { + continue; + } + + let acceptable = ComposerRepository::is_version_acceptable_static( + constraint.as_deref(), + &real_name, + &version, + acceptable_stabilities_clone.as_ref(), + stability_flags_clone.as_ref(), + )?; + if acceptable { + versions_to_load.push(version); + } + } + + let loaded_packages: Vec<Box<BasePackage>> = + ComposerRepository::create_packages_static( + versions_to_load, + packages_source, + )?; + for mut package in loaded_packages.into_iter() { + package.set_repository_self(); + let hash_c = spl_object_hash(&*package); + if let Some(alias) = package.as_alias_package_mut() { + let aliased_hash = spl_object_hash(alias.get_alias_of()); + if !unsafe { (*packages_ptr).contains_key(&aliased_hash) } { + alias.get_alias_of_mut().set_repository_self(); + let aliased_clone = dyn_clone_box(alias.get_alias_of()); + unsafe { + (*packages_ptr).insert(aliased_hash, aliased_clone); + } + } + } + unsafe { + (*packages_ptr).insert(hash_c, package); + } + } + Ok(()) + } + })); + promises.push(promise); + } + + self.r#loop.wait(promises, None)?; + + Ok(LoadAsyncPackagesResult { + names_found, + packages, + }) + } + + fn start_cached_async_download( + &mut self, + file_name: &str, + package_name: Option<&str>, + ) -> anyhow::Result<Box<dyn PromiseInterface>> { + if self.lazy_providers_url.is_none() { + return Err(LogicException { + message: "startCachedAsyncDownload only supports v2 protocol composer repos with a metadata-url".to_string(), + code: 0, + }.into()); + } + + let name = strtolower(file_name); + let package_name = package_name.map(|s| s.to_string()).unwrap_or_else(|| name.clone()); + + let url = self + .lazy_providers_url + .as_ref() + .unwrap() + .replace("%package%", &name); + let cache_key = format!("provider-{}.json", strtr(&name, "/", "~")); + + let mut last_modified: Option<String> = None; + let contents_opt: Option<IndexMap<String, PhpMixed>>; + if let Some(raw) = self.cache.read(&cache_key) { + let decoded = json_decode(&raw, true)?; + if let Some(arr) = decoded.as_array() { + let map: IndexMap<String, PhpMixed> = arr + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + last_modified = map + .get("last-modified") + .and_then(|v| v.as_string()) + .map(|s| s.to_string()); + contents_opt = Some(map); + } else { + contents_opt = None; + } + } else { + contents_opt = None; + } + + let promise = self.async_fetch_file(&url, &cache_key, last_modified.as_deref())?; + let url_owned = url.clone(); + let cache_key_owned = cache_key.clone(); + let contents = contents_opt; + Ok(promise.then_boxed(Box::new( + move |response: PhpMixed| -> anyhow::Result<PhpMixed> { + let mut packages_source = + format!("downloaded file ({})", Url::sanitize(url_owned.clone())); + + let response_data = if response.as_bool() == Some(true) { + packages_source = format!( + "cached file ({} originating from {})", + cache_key_owned, + Url::sanitize(url_owned.clone()) + ); + contents + .clone() + .map(|m| { + PhpMixed::Array( + m.into_iter().map(|(k, v)| (k, Box::new(v))).collect(), + ) + }) + .unwrap_or(PhpMixed::Null) + } else { + response + }; + + let response_arr = response_data.as_array(); + let has_pkg = response_arr + .and_then(|a| a.get("packages")) + .and_then(|v| v.as_array()) + .map_or(false, |a| a.contains_key(&package_name)); + let has_advisories = response_arr + .map_or(false, |a| a.contains_key("security-advisories")); + if !has_pkg && !has_advisories { + return Ok(PhpMixed::List(vec![ + Box::new(PhpMixed::Null), + Box::new(PhpMixed::String(packages_source)), + ])); + } + + Ok(PhpMixed::List(vec![ + Box::new(response_data), + Box::new(PhpMixed::String(packages_source)), + ])) + }, + ))) + } + + /// @param name package name (must be lowercased already) + fn is_version_acceptable( + &self, + constraint: Option<&dyn ConstraintInterface>, + name: &str, + version_data: &IndexMap<String, PhpMixed>, + acceptable_stabilities: Option<&IndexMap<String, i64>>, + stability_flags: Option<&IndexMap<String, i64>>, + ) -> anyhow::Result<bool> { + Self::is_version_acceptable_with_loader( + &self.loader, + constraint, + name, + version_data, + acceptable_stabilities, + stability_flags, + ) + } + + fn is_version_acceptable_static( + constraint: Option<&dyn ConstraintInterface>, + name: &str, + version_data: &IndexMap<String, PhpMixed>, + acceptable_stabilities: Option<&IndexMap<String, i64>>, + stability_flags: Option<&IndexMap<String, i64>>, + ) -> anyhow::Result<bool> { + Self::is_version_acceptable_with_loader( + &ArrayLoader::new_with_parser(VersionParser::new()), + constraint, + name, + version_data, + acceptable_stabilities, + stability_flags, + ) + } + + fn is_version_acceptable_with_loader( + loader: &ArrayLoader, + constraint: Option<&dyn ConstraintInterface>, + name: &str, + version_data: &IndexMap<String, PhpMixed>, + acceptable_stabilities: Option<&IndexMap<String, i64>>, + stability_flags: Option<&IndexMap<String, i64>>, + ) -> anyhow::Result<bool> { + let mut versions: Vec<String> = vec![version_data + .get("version_normalized") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string()]; + + if let Some(alias) = loader.get_branch_alias(version_data) { + versions.push(alias); + } + + for version in versions.iter() { + if acceptable_stabilities.is_some() + && stability_flags.is_some() + && !StabilityFilter::is_package_acceptable( + acceptable_stabilities.unwrap(), + stability_flags.unwrap(), + &[name.to_string()], + &VersionParser::parse_stability(version), + ) + { + continue; + } + + if let Some(c) = constraint { + if !CompilingMatcher::match_(c, Constraint::OP_EQ, version) { + continue; + } + } + + return Ok(true); + } + + Ok(false) + } + + fn get_packages_json_url(&self) -> String { + let json_url_parts = parse_url_all(&strtr(&self.url, "\\", "/")); + + let has_json = json_url_parts + .as_array() + .and_then(|a| a.get("path")) + .and_then(|v| v.as_string()) + .map_or(false, |p| p.contains(".json")); + if has_json { + return self.url.clone(); + } + + format!("{}/packages.json", self.url) + } + + fn load_root_server_file( + &mut self, + root_max_age: Option<i64>, + ) -> anyhow::Result<RootData> { + if let Some(rd) = &self.root_data { + return Ok(clone_root_data(rd)); + } + + if !extension_loaded("openssl") && self.url.starts_with("https") { + return Err(RuntimeException { + message: format!( + "You must enable the openssl extension in your php.ini to load information from {}", + self.url + ), + code: 0, + }.into()); + } + + let mut data: Option<IndexMap<String, PhpMixed>> = None; + if let Some(cached_raw) = self.cache.read("packages.json") { + let cached_decoded = json_decode(&cached_raw, true)?; + if let Some(arr) = cached_decoded.as_array() { + let cached_data: IndexMap<String, PhpMixed> = arr + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + let age = self.cache.get_age("packages.json"); + if root_max_age.is_some() + && age.is_some() + && age.unwrap() <= root_max_age.unwrap() + { + data = Some(cached_data); + } else if let Some(last_modified) = cached_data + .get("last-modified") + .and_then(|v| v.as_string()) + .map(|s| s.to_string()) + { + let response = self.fetch_file_if_last_modified( + &self.get_packages_json_url(), + "packages.json", + &last_modified, + )?; + data = Some(match response { + FetchFileIfLastModifiedResult::NotModified => cached_data, + FetchFileIfLastModifiedResult::Data(d) => d, + }); + } + } + } + + if data.is_none() { + data = Some(self.fetch_file( + &self.get_packages_json_url(), + Some("packages.json"), + None, + true, + )?); + } + + let mut data = data.unwrap(); + + if let Some(notify_batch) = data + .get("notify-batch") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + self.notify_url = Some(self.canonicalize_url(¬ify_batch)?); + } else if let Some(notify) = data + .get("notify") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + self.notify_url = Some(self.canonicalize_url(¬ify)?); + } + + if let Some(search) = data + .get("search") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + self.search_url = Some(self.canonicalize_url(&search)?); + } + + if let Some(mirrors) = data.get("mirrors").and_then(|v| v.as_list()).cloned() { + for mirror_mixed in mirrors.iter() { + let mirror = match mirror_mixed.as_array() { + Some(a) => a, + None => continue, + }; + if let Some(git_url) = mirror + .get("git-url") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + let preferred = mirror + .get("preferred") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + self.source_mirrors + .get_or_insert_with(IndexMap::new) + .entry("git".to_string()) + .or_insert_with(Vec::new) + .push(SourceMirror { + url: git_url, + preferred, + }); + } + if let Some(hg_url) = mirror + .get("hg-url") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + let preferred = mirror + .get("preferred") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + self.source_mirrors + .get_or_insert_with(IndexMap::new) + .entry("hg".to_string()) + .or_insert_with(Vec::new) + .push(SourceMirror { + url: hg_url, + preferred, + }); + } + if let Some(dist_url) = mirror + .get("dist-url") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + let preferred = mirror + .get("preferred") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + self.dist_mirrors + .get_or_insert_with(Vec::new) + .push(DistMirror { + url: self.canonicalize_url(&dist_url)?, + preferred, + }); + } + } + } + + if let Some(providers_lazy_url) = data + .get("providers-lazy-url") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + self.lazy_providers_url = Some(self.canonicalize_url(&providers_lazy_url)?); + self.has_providers = true; + + self.has_partial_packages = data + .get("packages") + .map(|v| match v { + PhpMixed::Array(a) => !a.is_empty(), + PhpMixed::List(l) => !l.is_empty(), + _ => false, + }) + .unwrap_or(false); + } + + // metadata-url indicates V2 repo protocol so it takes over from all the V1 types + // V2 only has lazyProviders and possibly partial packages, but no ability to process anything else, + // V2 also supports async loading + if let Some(metadata_url) = data + .get("metadata-url") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + self.lazy_providers_url = Some(self.canonicalize_url(&metadata_url)?); + self.providers_url = None; + self.has_providers = false; + self.has_partial_packages = data + .get("packages") + .map(|v| match v { + PhpMixed::Array(a) => !a.is_empty(), + PhpMixed::List(l) => !l.is_empty(), + _ => false, + }) + .unwrap_or(false); + self.allow_ssl_downgrade = false; + + // provides a list of package names that are available in this repo + // this disables lazy-provider behavior in the sense that if a list is available we assume it is finite and won't search for other packages in that repo + // while if no list is there lazyProvidersUrl is used when looking for any package name to see if the repo knows it + if let Some(available) = data + .get("available-packages") + .and_then(|v| v.as_list()) + .cloned() + { + if !available.is_empty() { + let avail_packages: Vec<String> = available + .iter() + .filter_map(|v| v.as_string().map(|s| strtolower(s))) + .collect(); + let mut combined: IndexMap<String, String> = IndexMap::new(); + for k in avail_packages.iter() { + combined.insert(k.clone(), k.clone()); + } + self.available_packages = Some(combined); + self.has_available_package_list = true; + } + } + + // Provides a list of package name patterns (using * wildcards to match any substring, e.g. "vendor/*") that are available in this repo + // Disables lazy-provider behavior as with available-packages, but may allow much more compact expression of packages covered by this repository. + // Over-specifying covered packages is safe, but may result in increased traffic to your repository. + if let Some(patterns) = data + .get("available-package-patterns") + .and_then(|v| v.as_list()) + .cloned() + { + if !patterns.is_empty() { + let mapped: Vec<String> = patterns + .iter() + .filter_map(|v| v.as_string()) + .map(|p| BasePackage::package_name_to_regexp(p)) + .collect(); + self.available_package_patterns = Some(mapped); + self.has_available_package_list = true; + } + } + + // Remove legacy keys as most repos need to be compatible with Composer v1 + // as well but we are not interested in the old format anymore at this point + data.shift_remove("providers-url"); + data.shift_remove("providers"); + data.shift_remove("providers-includes"); + + if let Some(sec) = data + .get("security-advisories") + .and_then(|v| v.as_array()) + .cloned() + { + let metadata = sec + .get("metadata") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let api_url_str = sec + .get("api-url") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()); + let api_url = if let Some(s) = api_url_str { + Some(self.canonicalize_url(s)?) + } else { + None + }; + self.security_advisory_config = Some(SecurityAdvisoryConfig { + metadata, + api_url: api_url.clone(), + }); + if api_url.is_none() && !self.has_available_package_list { + return Err(UnexpectedValueException { + message: format!( + "Invalid security advisory configuration on {}: If the repository does not provide a security-advisories.api-url then available-packages or available-package-patterns are required to be provided for performance reason.", + self.get_repo_name() + ), + code: 0, + }.into()); + } + } + } + + if self.allow_ssl_downgrade { + self.url = self.url.replace("https://", "http://"); + self.base_url = self.base_url.replace("https://", "http://"); + } + + if let Some(providers_url) = data + .get("providers-url") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + self.providers_url = Some(self.canonicalize_url(&providers_url)?); + self.has_providers = true; + } + + if let Some(list) = data + .get("list") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + self.list_url = Some(self.canonicalize_url(&list)?); + } + + let providers_non_empty = data + .get("providers") + .map(|v| match v { + PhpMixed::Array(a) => !a.is_empty(), + PhpMixed::List(l) => !l.is_empty(), + _ => false, + }) + .unwrap_or(false); + let providers_includes_non_empty = data + .get("providers-includes") + .map(|v| match v { + PhpMixed::Array(a) => !a.is_empty(), + PhpMixed::List(l) => !l.is_empty(), + _ => false, + }) + .unwrap_or(false); + if providers_non_empty || providers_includes_non_empty { + self.has_providers = true; + } + + if let Some(providers_api) = data + .get("providers-api") + .and_then(|v| v.as_string()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + self.providers_api_url = Some(self.canonicalize_url(&providers_api)?); + } + + self.root_data = Some(RootData::Data(data.clone())); + Ok(RootData::Data(data)) + } + + fn canonicalize_url(&self, url: &str) -> anyhow::Result<String> { + if url.is_empty() { + return Err(InvalidArgumentException { + message: "Expected a string with a value and not an empty string".to_string(), + code: 0, + }.into()); + } + + if url.starts_with('/') { + let mut matches: Vec<String> = Vec::new(); + if Preg::is_match_with_matches( + r"{^[^:]++://[^/]*+}", + &self.url, + &mut matches, + )? { + return Ok(format!( + "{}{}", + matches.get(0).cloned().unwrap_or_default(), + url + )); + } + + return Ok(self.url.clone()); + } + + Ok(url.to_string()) + } + + fn load_data_from_server(&mut self) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> { + let data = self.load_root_server_file(None)?; + let data = match data { + RootData::True => { + return Err(LogicException { + message: "loadRootServerFile should not return true during initialization" + .to_string(), + code: 0, + }.into()); + } + RootData::Data(d) => d, + }; + + self.load_includes(&data) + } + + fn has_partial_packages(&mut self) -> anyhow::Result<bool> { + if self.has_partial_packages && self.partial_packages_by_name.is_none() { + self.initialize_partial_packages()?; + } + + Ok(self.has_partial_packages) + } + + fn load_provider_listings( + &mut self, + data: &IndexMap<String, PhpMixed>, + ) -> anyhow::Result<()> { + if let Some(providers) = data.get("providers").and_then(|v| v.as_array()) { + if self.provider_listing.is_none() { + self.provider_listing = Some(IndexMap::new()); + } + let listing = self.provider_listing.as_mut().unwrap(); + for (k, v) in providers.iter() { + if let Some(arr) = v.as_array() { + if let Some(sha256) = arr.get("sha256").and_then(|v| v.as_string()) { + listing.insert( + k.clone(), + ProviderListingEntry { + sha256: sha256.to_string(), + }, + ); + } + } + } + } + + if self.providers_url.is_some() { + if let Some(includes) = data + .get("provider-includes") + .and_then(|v| v.as_array()) + .cloned() + { + for (include, metadata_mixed) in includes.iter() { + let metadata = match metadata_mixed.as_array() { + Some(a) => a, + None => continue, + }; + let sha256 = metadata + .get("sha256") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let url = format!( + "{}/{}", + self.base_url, + include.replace("%hash%", &sha256) + ); + let cache_key = include.replace("%hash%", "").replace("$", ""); + let included_data: IndexMap<String, PhpMixed> = + if self.cache.sha256(&cache_key).as_deref() == Some(sha256.as_str()) { + let raw = self.cache.read(&cache_key).unwrap_or_default(); + let decoded = json_decode(&raw, true)?; + decoded + .as_array() + .map(|a| { + a.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect() + }) + .unwrap_or_default() + } else { + self.fetch_file(&url, Some(&cache_key), Some(&sha256), false)? + }; + + self.load_provider_listings(&included_data)?; + } + } + } + Ok(()) + } + + fn load_includes( + &mut self, + data: &IndexMap<String, PhpMixed>, + ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> { + let mut packages: Vec<IndexMap<String, PhpMixed>> = Vec::new(); + + // legacy repo handling + if !data.contains_key("packages") && !data.contains_key("includes") { + for (_k, pkg_mixed) in data.iter() { + let pkg = match pkg_mixed.as_array() { + Some(a) => a, + None => continue, + }; + if let Some(versions) = pkg.get("versions").and_then(|v| v.as_array()) { + for (_, metadata) in versions.iter() { + if let Some(m) = metadata.as_array() { + packages.push( + m.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(), + ); + } + } + } + } + + return Ok(packages); + } + + if let Some(pkgs) = data.get("packages").and_then(|v| v.as_array()).cloned() { + for (package, versions_mixed) in pkgs.iter() { + let package_name = strtolower(package); + let versions = match versions_mixed.as_array() { + Some(a) => a.clone(), + None => continue, + }; + for (_version, metadata_mixed) in versions.iter() { + let metadata = match metadata_mixed.as_array() { + Some(a) => a.clone(), + None => continue, + }; + let metadata_map: IndexMap<String, PhpMixed> = metadata + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + packages.push(metadata_map.clone()); + let meta_name = metadata_map + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + if !self.displayed_warning_about_non_matching_package_index + && package_name != strtolower(&meta_name) + { + self.displayed_warning_about_non_matching_package_index = true; + self.io.write_error(&format!( + "<warning>Warning: the packages key '{}' doesn't match the name defined in the package metadata '{}' in repository {}</warning>", + package, meta_name, self.base_url + )); + } + } + } + } + + if let Some(includes) = data.get("includes").and_then(|v| v.as_array()).cloned() { + for (include, metadata_mixed) in includes.iter() { + let metadata = match metadata_mixed.as_array() { + Some(a) => a, + None => continue, + }; + let sha1 = metadata + .get("sha1") + .and_then(|v| v.as_string()) + .map(|s| s.to_string()); + let included_data: IndexMap<String, PhpMixed> = if let Some(ref sha1) = sha1 { + if self.cache.sha1(include).as_deref() == Some(sha1.as_str()) { + let raw = self.cache.read(include).unwrap_or_default(); + let decoded = json_decode(&raw, true)?; + decoded + .as_array() + .map(|a| { + a.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect() + }) + .unwrap_or_default() + } else { + self.fetch_file(include, None, None, false)? + } + } else { + self.fetch_file(include, None, None, false)? + }; + let included_packages = self.load_includes(&included_data)?; + for p in included_packages.into_iter() { + packages.push(p); + } + } + } + + Ok(packages) + } + + fn create_packages_flat( + &mut self, + packages: Vec<IndexMap<String, PhpMixed>>, + source: Option<String>, + ) -> anyhow::Result<Vec<Box<BasePackage>>> { + if packages.is_empty() { + return Ok(vec![]); + } + + let mut packages = packages; + let result = (|| -> anyhow::Result<Vec<Box<BasePackage>>> { + for data in packages.iter_mut() { + if !data.contains_key("notification-url") { + data.insert( + "notification-url".to_string(), + match &self.notify_url { + Some(s) => PhpMixed::String(s.clone()), + None => PhpMixed::Null, + }, + ); + } + } + + let package_instances = self.loader.load_packages(packages.clone())?; + + let mut results: Vec<Box<BasePackage>> = Vec::new(); + for mut package in package_instances.into_iter() { + if let Some(src_type) = package.get_source_type() { + if let Some(mirrors) = self + .source_mirrors + .as_ref() + .and_then(|m| m.get(src_type)) + { + package.set_source_mirrors(mirrors); + } + } + if let Some(dist_mirrors) = self.dist_mirrors.as_ref() { + package.set_dist_mirrors(dist_mirrors); + } + self.configure_package_transport_options(&mut *package); + results.push(package); + } + Ok(results) + })(); + + result.map_err(|e| { + RuntimeException { + message: format!( + "Could not load packages in {}{}: [{}] {}", + self.get_repo_name(), + source + .as_ref() + .map(|s| format!(" from {}", s)) + .unwrap_or_default(), + "Exception", + e.to_string() + ), + code: 0, + } + .into() + }) + } + + fn create_packages_static( + packages: Vec<IndexMap<String, PhpMixed>>, + _source: Option<String>, + ) -> anyhow::Result<Vec<Box<BasePackage>>> { + if packages.is_empty() { + return Ok(vec![]); + } + let loader = ArrayLoader::new_with_parser(VersionParser::new()); + Ok(loader.load_packages(packages)?) + } + + fn fetch_file( + &mut self, + filename: &str, + cache_key: Option<&str>, + sha256: Option<&str>, + store_last_modified_time: bool, + ) -> anyhow::Result<IndexMap<String, PhpMixed>> { + if filename.is_empty() { + return Err(InvalidArgumentException { + message: "$filename should not be an empty string".to_string(), + code: 0, + } + .into()); + } + + let (mut filename, cache_key_owned): (String, Option<String>) = match cache_key { + None => { + let ck = filename.to_string(); + let fn_full = format!("{}/{}", self.base_url, filename); + (fn_full, Some(ck)) + } + Some(ck) => (filename.to_string(), Some(ck.to_string())), + }; + + // url-encode $ signs in URLs as bad proxies choke on them + if let Some(pos) = filename.find('$') { + if pos > 0 && Preg::is_match(r"{^https?://}i", &filename)? { + filename = format!("{}%24{}", &filename[..pos], &filename[pos + 1..]); + } + } + + let mut retries: i64 = 3; + let mut data: Option<IndexMap<String, PhpMixed>> = None; + while { + let cont = retries > 0; + retries -= 1; + cont + } { + let attempt: anyhow::Result<()> = (|| -> anyhow::Result<()> { + let mut options = self.options.clone(); + if let Some(dispatcher) = self.event_dispatcher.as_mut() { + let mut pre_file_download_event = PreFileDownloadEvent::new( + PluginEvents::PRE_FILE_DOWNLOAD.to_string(), + &self.http_downloader, + filename.clone(), + "metadata".to_string(), + { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + // TODO(plugin): pass repository self-reference + m.insert("repository".to_string(), PhpMixed::Null); + m + }, + ); + pre_file_download_event.set_transport_options(self.options.clone()); + dispatcher.dispatch( + &pre_file_download_event.get_name(), + &mut pre_file_download_event, + ); + filename = pre_file_download_event.get_processed_url(); + options = pre_file_download_event.get_transport_options(); + } + + let response = self.http_downloader.get(&filename, &options)?; + let mut json = response.get_body().to_string(); + if let Some(sha256_val) = sha256 { + if sha256_val != hash("sha256", &json) { + // undo downgrade before trying again if http seems to be hijacked or modifying content somehow + if self.allow_ssl_downgrade { + self.url = self.url.replace("http://", "https://"); + self.base_url = self.base_url.replace("http://", "https://"); + filename = filename.replace("http://", "https://"); + } + + if retries > 0 { + std::thread::sleep(std::time::Duration::from_micros(100000)); + return Err(RetryMarker.into()); + } + + // TODO use scarier wording once we know for sure it doesn't do false positives anymore + return Err(RepositorySecurityException(shirabe_php_shim::Exception { + message: format!( + "The contents of {} do not match its signature. This could indicate a man-in-the-middle attack or e.g. antivirus software corrupting files. Try running composer again and report this if you think it is a mistake.", + filename + ), + code: 0, + }).into()); + } + } + + if let Some(dispatcher) = self.event_dispatcher.as_mut() { + let mut post_file_download_event = PostFileDownloadEvent::new( + PluginEvents::POST_FILE_DOWNLOAD.to_string(), + None, + sha256.map(|s| s.to_string()), + filename.clone(), + "metadata".to_string(), + { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + // TODO(plugin): pass response and repository self-reference + m.insert("response".to_string(), PhpMixed::Null); + m.insert("repository".to_string(), PhpMixed::Null); + m + }, + ); + dispatcher.dispatch( + &post_file_download_event.get_name(), + &mut post_file_download_event, + ); + } + + let decoded = response.decode_json()?; + let mut data_local: IndexMap<String, PhpMixed> = decoded + .as_array() + .map(|a| { + a.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect() + }) + .unwrap_or_default(); + HttpDownloader::output_warnings(&*self.io, &self.url, &data_local); + + if let Some(ck) = cache_key_owned.as_ref() { + if !ck.is_empty() && !self.cache.is_read_only() { + if store_last_modified_time { + if let Some(last_modified_date) = response.get_header("last-modified") { + data_local.insert( + "last-modified".to_string(), + PhpMixed::String(last_modified_date), + ); + let as_mixed = PhpMixed::Array( + data_local + .iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone()))) + .collect(), + ); + json = JsonFile::encode(&as_mixed, 0)?; + } + } + self.cache.write(ck, &json); + } + } + + response.collect(); + + data = Some(data_local); + Ok(()) + })(); + + match attempt { + Ok(()) => break, + Err(e) => { + if e.downcast_ref::<RetryMarker>().is_some() { + continue; + } + if e.downcast_ref::<LogicException>().is_some() { + return Err(e); + } + if let Some(te) = e.downcast_ref::<TransportException>() { + if te.get_status_code() == 404 { + return Err(e); + } + } + if e.downcast_ref::<RepositorySecurityException>().is_some() { + return Err(e); + } + + if let Some(ck) = cache_key_owned.as_ref() { + if !ck.is_empty() { + if let Some(contents) = self.cache.read(ck) { + if !self.degraded_mode { + self.io.write_error(&format!( + "<warning>{} could not be fully loaded ({}), package information was loaded from the local cache and may be out of date</warning>", + self.url, + e.to_string() + )); + } + self.degraded_mode = true; + let parsed = JsonFile::parse_json( + &contents, + Some(&format!("{}{}", self.cache.get_root(), ck)), + )?; + let map: IndexMap<String, PhpMixed> = parsed + .as_array() + .map(|a| { + a.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect() + }) + .unwrap_or_default(); + data = Some(map); + + break; + } + } + } + + return Err(e); + } + } + } + + match data { + Some(d) => Ok(d), + None => Err(LogicException { + message: "ComposerRepository: Undefined $data. Please report at https://github.com/composer/composer/issues/new.".to_string(), + code: 0, + }.into()), + } + } + + fn fetch_file_if_last_modified( + &mut self, + filename: &str, + cache_key: &str, + last_modified_time: &str, + ) -> anyhow::Result<FetchFileIfLastModifiedResult> { + if filename.is_empty() { + return Err(InvalidArgumentException { + message: "$filename should not be an empty string".to_string(), + code: 0, + }.into()); + } + + let mut filename = filename.to_string(); + let result: anyhow::Result<FetchFileIfLastModifiedResult> = (|| { + let mut options = self.options.clone(); + if let Some(dispatcher) = self.event_dispatcher.as_mut() { + let mut pre_file_download_event = PreFileDownloadEvent::new( + PluginEvents::PRE_FILE_DOWNLOAD.to_string(), + &self.http_downloader, + filename.clone(), + "metadata".to_string(), + { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + m.insert("repository".to_string(), PhpMixed::Null); + m + }, + ); + pre_file_download_event.set_transport_options(self.options.clone()); + dispatcher.dispatch( + &pre_file_download_event.get_name(), + &mut pre_file_download_event, + ); + filename = pre_file_download_event.get_processed_url(); + options = pre_file_download_event.get_transport_options(); + } + + // cast http.header to array, then append + let http_entry = options + .entry("http".to_string()) + .or_insert(PhpMixed::Array(IndexMap::new())); + if let PhpMixed::Array(ref mut http_map) = http_entry { + if let Some(existing) = http_map.get("header") { + let arr = match &**existing { + PhpMixed::List(l) => l.clone(), + other => vec![Box::new(other.clone())], + }; + http_map.insert("header".to_string(), Box::new(PhpMixed::List(arr))); + } + let mut headers = match http_map.get("header") { + Some(b) => match &**b { + PhpMixed::List(l) => l.clone(), + _ => vec![], + }, + None => vec![], + }; + headers.push(Box::new(PhpMixed::String(format!( + "If-Modified-Since: {}", + last_modified_time + )))); + http_map.insert("header".to_string(), Box::new(PhpMixed::List(headers))); + } + + let response = self.http_downloader.get(&filename, &options)?; + let mut json = response.get_body().to_string(); + if json.is_empty() && response.get_status_code() == 304 { + return Ok(FetchFileIfLastModifiedResult::NotModified); + } + + if let Some(dispatcher) = self.event_dispatcher.as_mut() { + let mut post_file_download_event = PostFileDownloadEvent::new( + PluginEvents::POST_FILE_DOWNLOAD.to_string(), + None, + None, + filename.clone(), + "metadata".to_string(), + { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + m.insert("response".to_string(), PhpMixed::Null); + m.insert("repository".to_string(), PhpMixed::Null); + m + }, + ); + dispatcher.dispatch( + &post_file_download_event.get_name(), + &mut post_file_download_event, + ); + } + + let decoded = response.decode_json()?; + let mut data: IndexMap<String, PhpMixed> = decoded + .as_array() + .map(|a| { + a.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect() + }) + .unwrap_or_default(); + HttpDownloader::output_warnings(&*self.io, &self.url, &data); + + let last_modified_date = response.get_header("last-modified"); + response.collect(); + if let Some(ref lmd) = last_modified_date { + data.insert("last-modified".to_string(), PhpMixed::String(lmd.clone())); + let as_mixed = PhpMixed::Array( + data.iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone()))) + .collect(), + ); + json = JsonFile::encode(&as_mixed, 0)?; + } + if !self.cache.is_read_only() { + self.cache.write(cache_key, &json); + } + + Ok(FetchFileIfLastModifiedResult::Data(data)) + })(); + + match result { + Ok(v) => Ok(v), + Err(e) => { + if e.downcast_ref::<LogicException>().is_some() { + return Err(e); + } + if let Some(te) = e.downcast_ref::<TransportException>() { + if te.get_status_code() == 404 { + return Err(e); + } + } + + if !self.degraded_mode { + self.io.write_error(&format!( + "<warning>{} could not be fully loaded ({}), package information was loaded from the local cache and may be out of date</warning>", + self.url, + e.to_string() + )); + } + self.degraded_mode = true; + + Ok(FetchFileIfLastModifiedResult::NotModified) + } + } + } + + fn async_fetch_file( + &mut self, + filename: &str, + cache_key: &str, + last_modified_time: Option<&str>, + ) -> anyhow::Result<Box<dyn PromiseInterface>> { + if filename.is_empty() { + return Err(InvalidArgumentException { + message: "$filename should not be an empty string".to_string(), + code: 0, + }.into()); + } + + if self.packagesNotFoundCache.contains_key(filename) { + let mut empty: IndexMap<String, PhpMixed> = IndexMap::new(); + empty.insert("packages".to_string(), PhpMixed::Array(IndexMap::new())); + return Ok(react_promise_resolve(PhpMixed::Array( + empty.into_iter().map(|(k, v)| (k, Box::new(v))).collect(), + ))); + } + + if self.freshMetadataUrls.contains_key(filename) && last_modified_time.is_some() { + // make it look like we got a 304 response + let promise = react_promise_resolve(PhpMixed::Bool(true)); + + return Ok(promise); + } + + let mut filename = filename.to_string(); + let mut options = self.options.clone(); + if let Some(dispatcher) = self.event_dispatcher.as_mut() { + let mut pre_file_download_event = PreFileDownloadEvent::new( + PluginEvents::PRE_FILE_DOWNLOAD.to_string(), + &self.http_downloader, + filename.clone(), + "metadata".to_string(), + { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + m.insert("repository".to_string(), PhpMixed::Null); + m + }, + ); + pre_file_download_event.set_transport_options(self.options.clone()); + dispatcher.dispatch( + &pre_file_download_event.get_name(), + &mut pre_file_download_event, + ); + filename = pre_file_download_event.get_processed_url(); + options = pre_file_download_event.get_transport_options(); + } + + if let Some(last_modified_time) = last_modified_time { + let http_entry = options + .entry("http".to_string()) + .or_insert(PhpMixed::Array(IndexMap::new())); + if let PhpMixed::Array(ref mut http_map) = http_entry { + if let Some(existing) = http_map.get("header") { + let arr = match &**existing { + PhpMixed::List(l) => l.clone(), + other => vec![Box::new(other.clone())], + }; + http_map.insert("header".to_string(), Box::new(PhpMixed::List(arr))); + } + let mut headers = match http_map.get("header") { + Some(b) => match &**b { + PhpMixed::List(l) => l.clone(), + _ => vec![], + }, + None => vec![], + }; + headers.push(Box::new(PhpMixed::String(format!( + "If-Modified-Since: {}", + last_modified_time + )))); + http_map.insert("header".to_string(), Box::new(PhpMixed::List(headers))); + } + } + + let filename_for_closures = filename.clone(); + let cache_key_owned = cache_key.to_string(); + let url_owned = self.url.clone(); + let last_modified_time_owned = last_modified_time.map(|s| s.to_string()); + + let packages_not_found_ptr = &mut self.packagesNotFoundCache as *mut _; + let fresh_metadata_ptr = &mut self.freshMetadataUrls as *mut _; + let degraded_ptr = &mut self.degraded_mode as *mut _; + let cache_ptr = &mut self.cache as *mut _; + let io_ptr = self.io.as_ref() as *const dyn IOInterface; + + let accept = { + let filename = filename_for_closures.clone(); + let cache_key_owned = cache_key_owned.clone(); + let url_owned = url_owned.clone(); + move |response_mixed: PhpMixed| -> anyhow::Result<PhpMixed> { + // emulate: $response is a Response object; status code/body/header accessed via methods + let response = Response::from_php_mixed(response_mixed)?; + // package not found is acceptable for a v2 protocol repository + if response.get_status_code() == 404 { + unsafe { + (*packages_not_found_ptr).insert(filename.clone(), true); + } + + let mut empty: IndexMap<String, PhpMixed> = IndexMap::new(); + empty.insert("packages".to_string(), PhpMixed::Array(IndexMap::new())); + return Ok(PhpMixed::Array( + empty.into_iter().map(|(k, v)| (k, Box::new(v))).collect(), + )); + } + + let mut json = response.get_body().to_string(); + if json.is_empty() && response.get_status_code() == 304 { + unsafe { + (*fresh_metadata_ptr).insert(filename.clone(), true); + } + + return Ok(PhpMixed::Bool(true)); + } + + // TODO(plugin): dispatch PostFileDownloadEvent + + let decoded = response.decode_json()?; + let mut data: IndexMap<String, PhpMixed> = decoded + .as_array() + .map(|a| { + a.iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect() + }) + .unwrap_or_default(); + let io_ref = unsafe { &*io_ptr }; + HttpDownloader::output_warnings(io_ref, &url_owned, &data); + + let last_modified_date = response.get_header("last-modified"); + response.collect(); + if let Some(lmd) = last_modified_date { + data.insert("last-modified".to_string(), PhpMixed::String(lmd)); + let as_mixed = PhpMixed::Array( + data.iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone()))) + .collect(), + ); + json = JsonFile::encode( + &as_mixed, + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, + )?; + } + let is_ro = unsafe { (*cache_ptr).is_read_only() }; + if !is_ro { + unsafe { + (*cache_ptr).write(&cache_key_owned, &json); + } + } + unsafe { + (*fresh_metadata_ptr).insert(filename.clone(), true); + } + + Ok(PhpMixed::Array( + data.into_iter().map(|(k, v)| (k, Box::new(v))).collect(), + )) + } + }; + + let reject = { + let filename = filename_for_closures.clone(); + let url_owned = url_owned.clone(); + let last_modified_time = last_modified_time_owned.clone(); + let accept_clone = accept.clone(); + move |e: anyhow::Error| -> anyhow::Result<PhpMixed> { + if let Some(te) = e.downcast_ref::<TransportException>() { + if te.get_status_code() == 404 { + unsafe { + (*packages_not_found_ptr).insert(filename.clone(), true); + } + + return Ok(PhpMixed::Bool(false)); + } + } + + let is_degraded = unsafe { *degraded_ptr }; + if !is_degraded { + let io_ref = unsafe { &*io_ptr }; + io_ref.write_error(&format!( + "<warning>{} could not be fully loaded ({}), package information was loaded from the local cache and may be out of date</warning>", + url_owned, + e.to_string() + )); + } + unsafe { + *degraded_ptr = true; + } + + // if the file is in the cache, we fake a 304 Not Modified to allow the process to continue + if last_modified_time.is_some() { + let resp = Response::new_fake(&url_owned, 304, IndexMap::new(), String::new()); + return accept_clone(resp.to_php_mixed()); + } + + // special error code returned when network is being artificially disabled + if let Some(te) = e.downcast_ref::<TransportException>() { + if te.get_status_code() == 499 { + let resp = + Response::new_fake(&url_owned, 404, IndexMap::new(), String::new()); + return accept_clone(resp.to_php_mixed()); + } + } + + Err(e) + } + }; + + let initial = self.http_downloader.add(&filename, &options)?; + Ok(initial.then_with_reject_boxed(Box::new(accept), Box::new(reject))) + } + + /// This initializes the packages key of a partial packages.json that contain some packages inlined + a providers-lazy-url + /// + /// This should only be called once + fn initialize_partial_packages(&mut self) -> anyhow::Result<()> { + let root_data = self.load_root_server_file(None)?; + let root_data = match root_data { + RootData::True => return Ok(()), + RootData::Data(d) => d, + }; + + self.partial_packages_by_name = Some(IndexMap::new()); + if let Some(packages) = root_data.get("packages").and_then(|v| v.as_array()).cloned() { + for (package, versions_mixed) in packages.iter() { + let versions = match versions_mixed.as_array() { + Some(a) => a.clone(), + None => continue, + }; + for (_v_key, version_mixed) in versions.iter() { + let version = match version_mixed.as_array() { + Some(a) => a, + None => continue, + }; + let name_str = version + .get("name") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let version_package_name = strtolower(&name_str); + let version_map: IndexMap<String, PhpMixed> = version + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); + self.partial_packages_by_name + .as_mut() + .unwrap() + .entry(version_package_name.clone()) + .or_insert_with(Vec::new) + .push(version_map); + if !self.displayed_warning_about_non_matching_package_index + && version_package_name != strtolower(package) + { + self.io.write_error(&format!( + "<warning>Warning: the packages key '{}' doesn't match the name defined in the package metadata '{}' in repository {}</warning>", + package, name_str, self.base_url + )); + self.displayed_warning_about_non_matching_package_index = true; + } + } + } + } + + // wipe rootData as it is fully consumed at this point and this saves some memory + self.root_data = Some(RootData::True); + Ok(()) + } + + /// Checks if the package name is present in this lazy providers repo + /// + /// @return true if the package name is present in availablePackages or matched by availablePackagePatterns + pub(crate) fn lazy_providers_repo_contains(&self, name: &str) -> anyhow::Result<bool> { + if !self.has_available_package_list { + return Err(LogicException { + message: "lazyProvidersRepoContains should not be called unless hasAvailablePackageList is true".to_string(), + code: 0, + }.into()); + } + + if let Some(ref available) = self.available_packages { + if available.contains_key(name) { + return Ok(true); + } + } + + if let Some(ref patterns) = self.available_package_patterns { + for provider_regex in patterns.iter() { + if Preg::is_match(provider_regex, name)? { + return Ok(true); + } + } + } + + Ok(false) + } +} + +pub const SEARCH_FULLTEXT: i64 = 0; +pub const SEARCH_VENDOR: i64 = 2; + +#[derive(Debug)] +enum FetchFileIfLastModifiedResult { + NotModified, + Data(IndexMap<String, PhpMixed>), +} + +#[derive(Debug)] +struct RetryMarker; + +impl std::fmt::Display for RetryMarker { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "RetryMarker") + } +} + +impl std::error::Error for RetryMarker {} + +fn clone_root_data(rd: &RootData) -> RootData { + match rd { + RootData::True => RootData::True, + RootData::Data(d) => RootData::Data(d.clone()), + } +} + +fn dyn_clone_box(_pkg: &BasePackage) -> Box<BasePackage> { + todo!() +} + +fn dyn_clone_constraint(_c: &dyn ConstraintInterface) -> Box<dyn ConstraintInterface> { + todo!() +} + +fn react_promise_resolve(_value: PhpMixed) -> Box<dyn PromiseInterface> { + todo!() +} |
