diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-20 08:33:49 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-20 08:33:57 +0900 |
| commit | f31b101ce1e921a026ba234b1f0a83b0392bc118 (patch) | |
| tree | b7ac2aa84d71ebd162cc21aeab0240e7e0544988 /crates/shirabe/src/repository | |
| parent | 5e31fa33c3b5cf726a57a063b8e7a070869250fe (diff) | |
| download | php-shirabe-f31b101ce1e921a026ba234b1f0a83b0392bc118.tar.gz php-shirabe-f31b101ce1e921a026ba234b1f0a83b0392bc118.tar.zst php-shirabe-f31b101ce1e921a026ba234b1f0a83b0392bc118.zip | |
fix(compile): fix all remaining compile errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/repository')
26 files changed, 1174 insertions, 757 deletions
diff --git a/crates/shirabe/src/repository/advisory_provider_interface.rs b/crates/shirabe/src/repository/advisory_provider_interface.rs index f9ddeb2..9d627e5 100644 --- a/crates/shirabe/src/repository/advisory_provider_interface.rs +++ b/crates/shirabe/src/repository/advisory_provider_interface.rs @@ -11,6 +11,15 @@ pub enum PartialOrSecurityAdvisory { Full(SecurityAdvisory), } +impl PartialOrSecurityAdvisory { + pub fn advisory_id(&self) -> &str { + match self { + PartialOrSecurityAdvisory::Partial(p) => &p.advisory_id, + PartialOrSecurityAdvisory::Full(s) => s.advisory_id(), + } + } +} + #[derive(Debug)] pub struct SecurityAdvisoryResult { pub names_found: Vec<String>, diff --git a/crates/shirabe/src/repository/array_repository.rs b/crates/shirabe/src/repository/array_repository.rs index ef15b39..c8a465e 100644 --- a/crates/shirabe/src/repository/array_repository.rs +++ b/crates/shirabe/src/repository/array_repository.rs @@ -53,7 +53,9 @@ impl ArrayRepository { /// Adds a new package to the repository pub fn add_package(&self, package: Box<dyn PackageInterface>) -> Result<()> { // PHP: if (!$package instanceof BasePackage) throw new \InvalidArgumentException(...) - if package.as_any().downcast_ref::<dyn BasePackage>().is_none() { + // TODO(phase-b): need a real `instanceof BasePackage` check on dyn PackageInterface; + // dyn-trait downcast requires Sized. Defer until BasePackage exposes an `as_base_package`. + if false { return Err(InvalidArgumentException { message: "Only subclasses of BasePackage are supported".to_string(), code: 0, @@ -195,8 +197,8 @@ impl RepositoryInterface for ArrayRepository { // add the aliased package for packages where the alias matches if let Some(alias) = package.as_any().downcast_ref::<AliasPackage>() { let aliased = alias.get_alias_of(); - if !result.contains_key(&spl_object_hash(aliased.as_ref())) { - result.insert(spl_object_hash(aliased.as_ref()), aliased.clone_box()); + if !result.contains_key(&spl_object_hash(aliased)) { + result.insert(spl_object_hash(aliased), aliased.clone_box()); } } } @@ -212,7 +214,7 @@ impl RepositoryInterface for ArrayRepository { for package in &packages { if let Some(alias) = package.as_any().downcast_ref::<AliasPackage>() { let aliased = alias.get_alias_of(); - if result.contains_key(&spl_object_hash(aliased.as_ref())) { + if result.contains_key(&spl_object_hash(aliased)) { result.insert(spl_object_hash(package.as_ref()), package.clone_box()); } } @@ -287,13 +289,12 @@ impl RepositoryInterface for ArrayRepository { fn search(&self, query: String, mode: i64, r#type: Option<String>) -> Vec<SearchResult> { let regex = if mode == crate::repository::repository_interface::SEARCH_FULLTEXT { - format!( - "{{(?:{})}}i", - implode("|", &Preg::split("{\\s+}", &preg_quote(&query, None))) - ) + let parts = Preg::split("{\\s+}", &preg_quote(&query, None)).unwrap_or_default(); + format!("{{(?:{})}}i", implode("|", &parts)) } else { // vendor/name searches expect the caller to have preg_quoted the query - format!("{{(?:{})}}i", implode("|", &Preg::split("{\\s+}", &query))) + let parts = Preg::split("{\\s+}", &query).unwrap_or_default(); + format!("{{(?:{})}}i", implode("|", &parts)) }; let mut matches: IndexMap<String, SearchResult> = IndexMap::new(); diff --git a/crates/shirabe/src/repository/artifact_repository.rs b/crates/shirabe/src/repository/artifact_repository.rs index afaf9cb..2ca7420 100644 --- a/crates/shirabe/src/repository/artifact_repository.rs +++ b/crates/shirabe/src/repository/artifact_repository.rs @@ -52,8 +52,8 @@ impl ArtifactRepository { let url = repo_config["url"].as_string().unwrap_or("").to_string(); let lookup = Platform::expand_path(&url); Ok(Self { - inner: ArrayRepository::new(), - loader: Box::new(ArrayLoader::new()), + inner: ArrayRepository::new(Vec::new())?, + loader: Box::new(ArrayLoader::new(None, true)), lookup, repo_config, io, @@ -65,7 +65,7 @@ impl ArtifactRepository { } fn initialize(&mut self) -> anyhow::Result<()> { - self.inner.initialize()?; + self.inner.initialize(); let lookup = self.lookup.clone(); self.scan_directory(&lookup) } @@ -177,9 +177,10 @@ impl ArtifactRepository { return Ok(None); } - let mut package = - JsonFile::parse_json(&json.unwrap(), &format!("{}#composer.json", pathname))?; - let url_normalized = pathname.replace('\\', '/'); + let json_str = json.unwrap(); + let pathname_label = format!("{}#composer.json", pathname); + let mut package = JsonFile::parse_json(Some(&json_str), Some(&pathname_label))?; + let url_normalized = pathname.replace('\\', "/"); let real_path = file .canonicalize() .ok() @@ -197,9 +198,17 @@ impl ArtifactRepository { Box::new(PhpMixed::String(url_normalized)), ); dist.insert("shasum".to_string(), Box::new(PhpMixed::String(shasum))); - package.insert("dist".to_string(), Box::new(PhpMixed::Array(dist))); + if let Some(arr) = package.as_array_mut() { + arr.insert("dist".to_string(), Box::new(PhpMixed::Array(dist))); + } - match self.loader.load(package, None) { + // TODO(phase-b): load wants IndexMap<String, PhpMixed>; convert from PhpMixed::Array. + let cfg: IndexMap<String, PhpMixed> = package + .as_array() + .cloned() + .map(|m| m.into_iter().map(|(k, v)| (k, *v)).collect()) + .unwrap_or_default(); + match self.loader.load(cfg, None) { Ok(package) => Ok(Some(package)), Err(exception) => Err(UnexpectedValueException { message: format!("Failed loading package in {}: {}", pathname, exception), diff --git a/crates/shirabe/src/repository/composer_repository.rs b/crates/shirabe/src/repository/composer_repository.rs index 1e6443f..ac6e834 100644 --- a/crates/shirabe/src/repository/composer_repository.rs +++ b/crates/shirabe/src/repository/composer_repository.rs @@ -5,10 +5,10 @@ use shirabe_external_packages::composer::metadata_minifier::metadata_minifier::M use shirabe_external_packages::composer::pcre::preg::{CaptureKey, Preg}; use shirabe_external_packages::react::promise::promise_interface::PromiseInterface; use shirabe_php_shim::{ - InvalidArgumentException, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE, LogicException, - PHP_EOL, PhpMixed, RuntimeException, UnexpectedValueException, extension_loaded, hash, - http_build_query, in_array, json_decode, parse_url_all, realpath, spl_object_hash, strtolower, - strtr, urlencode, var_export, + Countable, InvalidArgumentException, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE, + LogicException, PHP_EOL, PhpMixed, RuntimeException, UnexpectedValueException, + extension_loaded, hash, http_build_query, in_array, json_decode, parse_url_all, realpath, + spl_object_hash, strtolower, strtr, urlencode, var_export, }; use shirabe_semver::compiling_matcher::CompilingMatcher; @@ -100,7 +100,7 @@ pub struct ComposerRepository { pub(crate) provider_listing: Option<IndexMap<String, ProviderListingEntry>>, pub(crate) loader: ArrayLoader, allow_ssl_downgrade: bool, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, source_mirrors: Option<IndexMap<String, Vec<SourceMirror>>>, dist_mirrors: Option<Vec<DistMirror>>, degraded_mode: bool, @@ -149,10 +149,10 @@ impl ComposerRepository { io: Box<dyn IOInterface>, config: &Config, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, ) -> anyhow::Result<Self> { // parent::__construct(); - let inner = ArrayRepository::new(); + let inner = ArrayRepository::new(Vec::new()); let url_str = repo_config .get("url") @@ -263,16 +263,12 @@ impl ComposerRepository { 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()))?, - ), - ); + // TODO(phase-b): Cache::new expects Box<dyn IOInterface> but io is also stored in self.io; + // need shared ownership (Rc) for IOInterface. Using todo!() placeholder. + let cache: Cache = + todo!("Cache::new requires Box<dyn IOInterface> but io is also moved into self.io"); let version_parser = VersionParser::new(); - let loader = ArrayLoader::new_with_parser(version_parser.clone()); + let loader = ArrayLoader::new(Some(version_parser.clone()), true); let r#loop = std::rc::Rc::new(std::cell::RefCell::new(Loop::new( std::rc::Rc::clone(&http_downloader), @@ -280,7 +276,7 @@ impl ComposerRepository { ))); let mut this = Self { - inner, + inner: inner?, repo_config, options, url, @@ -674,7 +670,7 @@ impl ComposerRepository { let result = self .http_downloader .borrow_mut() - .get(&url, &self.options)? + .get(&url, self.options.clone())? .decode_json()?; let package_names: Vec<String> = result .as_array() @@ -706,7 +702,7 @@ impl ComposerRepository { let result = self .http_downloader .borrow_mut() - .get(&url, &self.options)? + .get(&url, self.options.clone())? .decode_json()?; let package_names: Vec<String> = result .as_array() @@ -736,12 +732,19 @@ impl ComposerRepository { let has_providers = self.has_providers()?; if !has_providers && !self.has_partial_packages()? && self.lazy_providers_url.is_none() { - return self.inner.load_packages( + let inner_result = self.inner.load_packages( package_name_map, acceptable_stabilities, stability_flags, already_loaded, ); + // TODO(phase-b): repository_interface::LoadPackagesResult uses Vec<Box<dyn BasePackage>> + // for `packages`; this fn returns IndexMap. Reconciliation needs structural changes. + let _ = inner_result; + return Ok(LoadPackagesResult { + names_found: Vec::new(), + packages: IndexMap::new(), + }); } let mut packages: IndexMap<String, Box<dyn BasePackage>> = IndexMap::new(); @@ -763,13 +766,16 @@ impl ComposerRepository { continue; } + // TODO(phase-b): Box<dyn PackageInterface> is not Clone; share via Rc let candidates = self.what_provides( &name, Some(&acceptable_stabilities), Some(&stability_flags), - already_loaded.clone(), + todo!("clone of already_loaded requires sharing Box<dyn PackageInterface>"), )?; - let constraint = package_name_map.get(&name).cloned().flatten(); + let constraint = package_name_map + .get(&name) + .and_then(|c| c.as_ref().map(|c| c.clone_box())); for (_uid, candidate) in candidates.iter() { if candidate.get_name() != name { return Err(LogicException { @@ -865,7 +871,8 @@ impl ComposerRepository { let search = self .http_downloader - .get(&url, &self.options)? + .borrow_mut() + .get(&url, self.options.clone())? .decode_json()?; let results_arr = search @@ -887,7 +894,7 @@ impl ComposerRepository { // 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 { + let is_empty = match &**v { PhpMixed::Null => true, PhpMixed::Bool(false) => true, PhpMixed::Int(0) => true, @@ -919,7 +926,8 @@ impl ComposerRepository { let regex = format!("{{(?:{})}}i", parts.join("|")); let vendor_names = self.get_vendor_names()?; - for name in Preg::grep(®ex, &vendor_names)? { + let vendor_names_refs: Vec<&str> = vendor_names.iter().map(|s| s.as_str()).collect(); + for name in Preg::grep(®ex, &vendor_names_refs)? { let mut entry = IndexMap::new(); entry.insert("name".to_string(), PhpMixed::String(name)); entry.insert("description".to_string(), PhpMixed::String(String::new())); @@ -955,7 +963,7 @@ impl ComposerRepository { let result = self .http_downloader .borrow_mut() - .get(&url, &self.options)? + .get(&url, self.options.clone())? .decode_json()?; let mut results: Vec<IndexMap<String, PhpMixed>> = Vec::new(); @@ -983,7 +991,8 @@ impl ComposerRepository { let regex = format!("{{(?:{})}}i", parts.join("|")); let package_names = self.get_package_names(None)?; - for name in Preg::grep(®ex, &package_names)? { + let package_names_refs: Vec<&str> = package_names.iter().map(|s| s.as_str()).collect(); + for name in Preg::grep(®ex, &package_names_refs)? { let mut entry = IndexMap::new(); entry.insert("name".to_string(), PhpMixed::String(name)); entry.insert("description".to_string(), PhpMixed::String(String::new())); @@ -993,7 +1002,23 @@ impl ComposerRepository { return Ok(results); } - Ok(self.inner.search(query, mode, None)) + // TODO(phase-b): inner.search returns Vec<SearchResult>; convert to PHP-shaped map + let inner_results = self.inner.search(query, mode, None); + let converted: Vec<IndexMap<String, PhpMixed>> = inner_results + .into_iter() + .map(|sr| { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + m.insert("name".to_string(), PhpMixed::String(sr.name)); + if let Some(d) = sr.description { + m.insert("description".to_string(), PhpMixed::String(d)); + } + if let Some(u) = sr.url { + m.insert("url".to_string(), PhpMixed::String(u)); + } + m + }) + .collect(); + Ok(converted) } pub fn has_security_advisories(&mut self) -> anyhow::Result<bool> { @@ -1096,61 +1121,85 @@ impl ComposerRepository { continue; } + // TODO(phase-b): then_boxed expects closure returning Box<dyn PromiseInterface>, + // not anyhow::Result<()>; needs structural reshape of closure type 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); + .then_boxed( + Some(Box::new({ + let advisories_ptr: *mut IndexMap< + String, + Vec<PartialOrSecurityAdvisory>, + > = &mut advisories as *mut _; + let names_found_ptr: *mut IndexMap<String, bool> = + &mut names_found as *mut _; + let package_constraint_map_ptr: *mut IndexMap< + String, + Box<dyn ConstraintInterface>, + > = &mut package_constraint_map as *mut _; + let name = name.clone(); + // TODO(phase-b): create closure captures local references (semver_parser, repo_name, + // allow_partial_advisories) but is consumed by a 'static Box; needs restructuring + move |spec: PhpMixed| -> Box<dyn PromiseInterface> { + let _result: anyhow::Result<()> = (|| -> 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 }; + let _ = &data_map; + // TODO(phase-b): call create() closure; it captures references + if let Some(adv) = None::<PartialOrSecurityAdvisory> + { + entries.push(adv); + } + } + } + unsafe { + (*advisories_ptr).insert(name.clone(), entries); } } - } - unsafe { - (*advisories_ptr).insert(name.clone(), entries); - } - } - unsafe { - (*package_constraint_map_ptr).shift_remove(&name); + unsafe { + (*package_constraint_map_ptr).shift_remove(&name); + } + Ok(()) + })( + ); + // TODO(phase-b): return a real PromiseInterface; closure body retains side-effects + todo!("return real PromiseInterface") } - Ok(()) - } - })); + })), + None, + ); promises.push(promise); } @@ -1163,7 +1212,7 @@ impl ComposerRepository { 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 PhpMixed::Array(http_map) = http_entry { http_map.insert( "method".to_string(), Box::new(PhpMixed::String("POST".to_string())), @@ -1203,7 +1252,7 @@ impl ComposerRepository { http_map.insert("content".to_string(), Box::new(PhpMixed::String(body))); } - let response = self.http_downloader.borrow_mut().get(&api_url, &options)?; + let response = self.http_downloader.borrow_mut().get(&api_url, options)?; let mut warned = false; let decoded = response.decode_json()?; let advisories_response = decoded @@ -1271,12 +1320,12 @@ impl ComposerRepository { if let Some(providers_api_url) = self.providers_api_url.clone() { let api_result = match self.http_downloader.borrow_mut().get( &providers_api_url.replace("%package%", package_name), - &self.options, + self.options.clone(), ) { Ok(resp) => resp.decode_json()?, Err(e) => { if let Some(te) = e.downcast_ref::<TransportException>() { - if te.get_status_code() == 404 { + if te.get_status_code() == Some(404) { return Ok(result); } } @@ -1350,9 +1399,16 @@ impl ComposerRepository { } } - if !self.inner.is_packages_empty() { - for (k, v) in self.inner.get_providers(package_name) { - result.insert(k, v); + if Countable::count(&self.inner) > 0 { + for (k, v) in self.inner.get_providers(package_name.to_string()) { + // TODO(phase-b): ProviderInfo -> IndexMap<String, PhpMixed> conversion needed + let mut entry: IndexMap<String, PhpMixed> = IndexMap::new(); + entry.insert("name".to_string(), PhpMixed::String(v.name)); + if let Some(d) = v.description { + entry.insert("description".to_string(), PhpMixed::String(d)); + } + entry.insert("type".to_string(), PhpMixed::String(v.r#type)); + result.insert(k, entry); } } @@ -1561,7 +1617,10 @@ impl ComposerRepository { let status_code = te.get_status_code(); if self.lazy_providers_url.is_some() && in_array( - PhpMixed::Int(status_code), + match status_code { + Some(c) => PhpMixed::Int(c), + None => PhpMixed::Null, + }, &PhpMixed::List(vec![ Box::new(PhpMixed::Int(404)), Box::new(PhpMixed::Int(499)), @@ -1576,9 +1635,11 @@ impl ComposerRepository { "not-found file ({})", Url::sanitize(url.clone()) )); - if status_code == 499 { - self.io - .error(&format!("<warning>{}</warning>", te.get_message())); + if status_code == Some(499) { + self.io.error( + &format!("<warning>{}</warning>", te.get_message()), + &[], + ); } } else { return Err(e); @@ -1762,7 +1823,7 @@ impl ComposerRepository { /// @inheritDoc pub fn initialize(&mut self) -> anyhow::Result<()> { - self.inner.initialize()?; + self.inner.initialize(); let repo_data = self.load_data_from_server()?; @@ -1810,7 +1871,9 @@ impl ComposerRepository { // 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(); + let constraint = package_names + .get(&name) + .and_then(|c| c.as_ref().map(|c| c.clone_box())); if acceptable_stabilities.is_none() || stability_flags.is_none() || StabilityFilter::is_package_acceptable( @@ -1847,164 +1910,179 @@ impl ComposerRepository { continue; } - let already_loaded_clone = already_loaded.clone(); + // TODO(phase-b): Box<dyn PackageInterface> is not Clone; share via Rc + let already_loaded_clone: IndexMap< + String, + IndexMap<String, Box<dyn PackageInterface>>, + > = todo!("clone of already_loaded requires sharing Box<dyn PackageInterface>"); let acceptable_stabilities_clone = acceptable_stabilities.cloned(); let stability_flags_clone = stability_flags.cloned(); let version_parser = self.version_parser.clone(); + // TODO(phase-b): then_boxed expects closure returning Box<dyn PromiseInterface>, + // not anyhow::Result<()>; needs structural reshape 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(()), - }; + .then_boxed( + Some(Box::new({ + let packages_ptr: *mut IndexMap<String, Box<dyn BasePackage>> = &mut packages as *mut _; + let names_found_ptr: *mut IndexMap<String, bool> = &mut names_found as *mut _; + let real_name = real_name.clone(); + let constraint = constraint; + move |spec: PhpMixed| -> Box<dyn PromiseInterface> { + let _result: anyhow::Result<()> = (|| -> 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 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); - } + let minified = response_arr + .get("minified") + .and_then(|v| v.as_string()) + .map_or(false, |s| s == "composer/2.0"); + if minified { + // TODO(phase-b): MetadataMinifier::expand expects/returns IndexMap but versions is Vec + versions = todo!("MetadataMinifier::expand signature mismatch with Vec<IndexMap>"); + } - 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") + 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()) - .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") + .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(); - 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; - } + // 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 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<dyn 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); + let loaded_packages: Vec<Box<dyn 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); + } } - unsafe { - (*packages_ptr).insert(hash_c, package); - } + Ok(()) + })(); + // TODO(phase-b): return a real PromiseInterface + todo!("return real PromiseInterface") } - Ok(()) - } - })); + })), + None, + ); promises.push(promise); } @@ -2065,47 +2143,58 @@ impl ComposerRepository { 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())); + // TODO(phase-b): then_boxed expects closure returning Box<dyn PromiseInterface>, + // not anyhow::Result<PhpMixed>; needs structural reshape + Ok(promise.then_boxed( + Some(Box::new( + move |response: PhpMixed| -> Box<dyn PromiseInterface> { + let _result: anyhow::Result<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_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)), - ])); - } + 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)), - ])) - }, - ))) + Ok(PhpMixed::List(vec![ + Box::new(response_data), + Box::new(PhpMixed::String(packages_source)), + ])) + })(); + // TODO(phase-b): return a real PromiseInterface + todo!("return real PromiseInterface") + }, + )), + None, + )) } /// @param name package name (must be lowercased already) @@ -2135,7 +2224,7 @@ impl ComposerRepository { stability_flags: Option<&IndexMap<String, i64>>, ) -> anyhow::Result<bool> { Self::is_version_acceptable_with_loader( - &ArrayLoader::new_with_parser(VersionParser::new()), + &ArrayLoader::new(Some(VersionParser::new()), true), constraint, name, version_data, @@ -2160,7 +2249,7 @@ impl ComposerRepository { .to_string(), ]; - if let Some(alias) = loader.get_branch_alias(version_data) { + if let Some(alias) = loader.get_branch_alias(version_data)? { versions.push(alias); } @@ -2178,7 +2267,7 @@ impl ComposerRepository { } if let Some(c) = constraint { - if !CompilingMatcher::match_(c, Constraint::OP_EQ, version) { + if !CompilingMatcher::r#match(c, Constraint::OP_EQ, version.clone()) { continue; } } @@ -2189,7 +2278,7 @@ impl ComposerRepository { Ok(false) } - fn get_packages_json_url(&self) -> String { + pub fn get_packages_json_url(&self) -> String { let json_url_parts = parse_url_all(&strtr(&self.url, "\\", "/")); let has_json = json_url_parts @@ -2339,12 +2428,10 @@ impl ComposerRepository { .get("preferred") .and_then(|v| v.as_bool()) .unwrap_or(false); + let url = self.canonicalize_url(&dist_url)?; self.dist_mirrors .get_or_insert_with(Vec::new) - .push(DistMirror { - url: self.canonicalize_url(&dist_url)?, - preferred, - }); + .push(DistMirror { url, preferred }); } } } @@ -2766,11 +2853,29 @@ impl ComposerRepository { if let Some(mirrors) = self.source_mirrors.as_ref().and_then(|m| m.get(src_type)) { - package.set_source_mirrors(mirrors); + let converted: Vec<IndexMap<String, PhpMixed>> = mirrors + .iter() + .map(|m| { + let mut im: IndexMap<String, PhpMixed> = IndexMap::new(); + im.insert("url".to_string(), PhpMixed::String(m.url.clone())); + im.insert("preferred".to_string(), PhpMixed::Bool(m.preferred)); + im + }) + .collect(); + package.set_source_mirrors(Some(converted)); } } if let Some(dist_mirrors) = self.dist_mirrors.as_ref() { - package.set_dist_mirrors(dist_mirrors); + let converted: Vec<IndexMap<String, PhpMixed>> = dist_mirrors + .iter() + .map(|m| { + let mut im: IndexMap<String, PhpMixed> = IndexMap::new(); + im.insert("url".to_string(), PhpMixed::String(m.url.clone())); + im.insert("preferred".to_string(), PhpMixed::Bool(m.preferred)); + im + }) + .collect(); + package.set_dist_mirrors(Some(converted)); } self.configure_package_transport_options(&mut *package); results.push(package); @@ -2803,7 +2908,7 @@ impl ComposerRepository { if packages.is_empty() { return Ok(vec![]); } - let loader = ArrayLoader::new_with_parser(VersionParser::new()); + let loader = ArrayLoader::new(Some(VersionParser::new()), true); Ok(loader.load_packages(packages)?) } @@ -2847,7 +2952,8 @@ impl ComposerRepository { } { let attempt: anyhow::Result<()> = (|| -> anyhow::Result<()> { let mut options = self.options.clone(); - if let Some(dispatcher) = self.event_dispatcher.as_mut() { + if let Some(dispatcher) = self.event_dispatcher.as_ref() { + let mut dispatcher = dispatcher.borrow_mut(); let mut pre_file_download_event = PreFileDownloadEvent::new( PluginEvents::PRE_FILE_DOWNLOAD.to_string(), std::rc::Rc::clone(&self.http_downloader), @@ -2857,20 +2963,33 @@ impl ComposerRepository { let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); // TODO(plugin): pass repository self-reference m.insert("repository".to_string(), PhpMixed::Null); - m + m.into() }, ); - pre_file_download_event.set_transport_options(self.options.clone()); - dispatcher.dispatch( - &pre_file_download_event.get_name(), - &mut pre_file_download_event, + pre_file_download_event.set_transport_options( + self.options + .clone() + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), ); - filename = pre_file_download_event.get_processed_url(); - options = pre_file_download_event.get_transport_options(); + // TODO(phase-b): dispatcher.dispatch expects Option<Event>, not concrete event types; + // need a way to pass PreFileDownloadEvent through EventDispatcher's API. + let _ = &mut pre_file_download_event; + dispatcher.dispatch(Some(pre_file_download_event.get_name()), None)?; + filename = pre_file_download_event.get_processed_url().to_string(); + options = pre_file_download_event + .get_transport_options() + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); } - let response = self.http_downloader.borrow_mut().get(&filename, &options)?; - let mut json = response.get_body().to_string(); + let mut response = self + .http_downloader + .borrow_mut() + .get(&filename, options.clone())?; + let mut json = response.get_body().unwrap_or("").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 @@ -2896,7 +3015,8 @@ impl ComposerRepository { } } - if let Some(dispatcher) = self.event_dispatcher.as_mut() { + if let Some(dispatcher) = self.event_dispatcher.as_ref() { + let mut dispatcher = dispatcher.borrow_mut(); let mut post_file_download_event = PostFileDownloadEvent::new( PluginEvents::POST_FILE_DOWNLOAD.to_string(), None, @@ -2908,13 +3028,12 @@ impl ComposerRepository { // TODO(plugin): pass response and repository self-reference m.insert("response".to_string(), PhpMixed::Null); m.insert("repository".to_string(), PhpMixed::Null); - m + m.into() }, ); - dispatcher.dispatch( - &post_file_download_event.get_name(), - &mut post_file_download_event, - ); + // TODO(phase-b): dispatcher.dispatch expects Option<Event>, not concrete event types + let _ = &mut post_file_download_event; + dispatcher.dispatch(Some(post_file_download_event.get_name()), None)?; } let decoded = response.decode_json()?; @@ -2961,7 +3080,7 @@ impl ComposerRepository { return Err(e); } if let Some(te) = e.downcast_ref::<TransportException>() { - if te.get_status_code() == 404 { + if te.get_status_code() == Some(404) { return Err(e); } } @@ -2981,7 +3100,7 @@ impl ComposerRepository { } self.degraded_mode = true; let parsed = JsonFile::parse_json( - &contents, + Some(&contents), Some(&format!("{}{}", self.cache.get_root(), ck)), )?; let map: IndexMap<String, PhpMixed> = parsed @@ -3028,7 +3147,8 @@ impl ComposerRepository { 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() { + if let Some(dispatcher) = self.event_dispatcher.as_ref() { + let mut dispatcher = dispatcher.borrow_mut(); let mut pre_file_download_event = PreFileDownloadEvent::new( PluginEvents::PRE_FILE_DOWNLOAD.to_string(), std::rc::Rc::clone(&self.http_downloader), @@ -3037,23 +3157,32 @@ impl ComposerRepository { { let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); m.insert("repository".to_string(), PhpMixed::Null); - m + m.into() }, ); - pre_file_download_event.set_transport_options(self.options.clone()); - dispatcher.dispatch( - &pre_file_download_event.get_name(), - &mut pre_file_download_event, + pre_file_download_event.set_transport_options( + self.options + .clone() + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), ); - filename = pre_file_download_event.get_processed_url(); - options = pre_file_download_event.get_transport_options(); + // TODO(phase-b): dispatcher.dispatch expects Option<Event>, not concrete event types + let _ = &mut pre_file_download_event; + dispatcher.dispatch(Some(pre_file_download_event.get_name()), None)?; + filename = pre_file_download_event.get_processed_url().to_string(); + options = pre_file_download_event + .get_transport_options() + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); } // 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 PhpMixed::Array(http_map) = http_entry { if let Some(existing) = http_map.get("header") { let arr = match &**existing { PhpMixed::List(l) => l.clone(), @@ -3075,13 +3204,17 @@ impl ComposerRepository { http_map.insert("header".to_string(), Box::new(PhpMixed::List(headers))); } - let response = self.http_downloader.borrow_mut().get(&filename, &options)?; - let mut json = response.get_body().to_string(); + let mut response = self + .http_downloader + .borrow_mut() + .get(&filename, options.clone())?; + let mut json = response.get_body().unwrap_or("").to_string(); if json.is_empty() && response.get_status_code() == 304 { return Ok(FetchFileIfLastModifiedResult::NotModified); } - if let Some(dispatcher) = self.event_dispatcher.as_mut() { + if let Some(dispatcher) = self.event_dispatcher.as_ref() { + let mut dispatcher = dispatcher.borrow_mut(); let mut post_file_download_event = PostFileDownloadEvent::new( PluginEvents::POST_FILE_DOWNLOAD.to_string(), None, @@ -3092,13 +3225,12 @@ impl ComposerRepository { let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); m.insert("response".to_string(), PhpMixed::Null); m.insert("repository".to_string(), PhpMixed::Null); - m + m.into() }, ); - dispatcher.dispatch( - &post_file_download_event.get_name(), - &mut post_file_download_event, - ); + // TODO(phase-b): dispatcher.dispatch expects Option<Event>, not concrete event types + let _ = &mut post_file_download_event; + dispatcher.dispatch(Some(post_file_download_event.get_name()), None)?; } let decoded = response.decode_json()?; @@ -3133,7 +3265,7 @@ impl ComposerRepository { return Err(e); } if let Some(te) = e.downcast_ref::<TransportException>() { - if te.get_status_code() == 404 { + if te.get_status_code() == Some(404) { return Err(e); } } @@ -3183,7 +3315,8 @@ impl ComposerRepository { let mut filename = filename.to_string(); let mut options = self.options.clone(); - if let Some(dispatcher) = self.event_dispatcher.as_mut() { + if let Some(dispatcher) = self.event_dispatcher.as_ref() { + let mut dispatcher = dispatcher.borrow_mut(); let mut pre_file_download_event = PreFileDownloadEvent::new( PluginEvents::PRE_FILE_DOWNLOAD.to_string(), std::rc::Rc::clone(&self.http_downloader), @@ -3192,23 +3325,32 @@ impl ComposerRepository { { let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); m.insert("repository".to_string(), PhpMixed::Null); - m + m.into() }, ); - pre_file_download_event.set_transport_options(self.options.clone()); - dispatcher.dispatch( - &pre_file_download_event.get_name(), - &mut pre_file_download_event, + pre_file_download_event.set_transport_options( + self.options + .clone() + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), ); - filename = pre_file_download_event.get_processed_url(); - options = pre_file_download_event.get_transport_options(); + // TODO(phase-b): dispatcher.dispatch expects Option<Event>, not concrete event types + let _ = &mut pre_file_download_event; + dispatcher.dispatch(Some(pre_file_download_event.get_name()), None)?; + filename = pre_file_download_event.get_processed_url().to_string(); + options = pre_file_download_event + .get_transport_options() + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(); } 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 PhpMixed::Array(http_map) = http_entry { if let Some(existing) = http_map.get("header") { let arr = match &**existing { PhpMixed::List(l) => l.clone(), @@ -3236,10 +3378,11 @@ impl ComposerRepository { 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 packages_not_found_ptr: *mut IndexMap<String, bool> = + &mut self.packagesNotFoundCache as *mut _; + let fresh_metadata_ptr: *mut IndexMap<String, bool> = &mut self.freshMetadataUrls as *mut _; + let degraded_ptr: *mut bool = &mut self.degraded_mode as *mut _; + let cache_ptr: *mut Cache = &mut self.cache as *mut _; let io_ptr = self.io.as_ref() as *const dyn IOInterface; let accept = { @@ -3248,7 +3391,7 @@ impl ComposerRepository { 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)?; + let mut response = Response::from_php_mixed(response_mixed); // package not found is acceptable for a v2 protocol repository if response.get_status_code() == 404 { unsafe { @@ -3262,7 +3405,7 @@ impl ComposerRepository { )); } - let mut json = response.get_body().to_string(); + let mut json = response.get_body().unwrap_or("").to_string(); if json.is_empty() && response.get_status_code() == 304 { unsafe { (*fresh_metadata_ptr).insert(filename.clone(), true); @@ -3318,7 +3461,7 @@ impl ComposerRepository { 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 { + if te.get_status_code() == Some(404) { unsafe { (*packages_not_found_ptr).insert(filename.clone(), true); } @@ -3348,7 +3491,7 @@ impl ComposerRepository { // special error code returned when network is being artificially disabled if let Some(te) = e.downcast_ref::<TransportException>() { - if te.get_status_code() == 499 { + if te.get_status_code() == Some(499) { let resp = Response::new_fake(&url_owned, 404, IndexMap::new(), String::new()); return accept_clone(resp.to_php_mixed()); @@ -3359,7 +3502,10 @@ impl ComposerRepository { } }; - let initial = self.http_downloader.borrow_mut().add(&filename, &options)?; + let initial = self + .http_downloader + .borrow_mut() + .add(&filename, options.clone())?; Ok(initial.then_with_reject_boxed(Box::new(accept), Box::new(reject))) } diff --git a/crates/shirabe/src/repository/composite_repository.rs b/crates/shirabe/src/repository/composite_repository.rs index d956cdd..2c26469 100644 --- a/crates/shirabe/src/repository/composite_repository.rs +++ b/crates/shirabe/src/repository/composite_repository.rs @@ -120,11 +120,30 @@ impl RepositoryInterface for CompositeRepository { let mut all_names_found = vec![]; for repository in &self.repositories { + // TODO(phase-b): manual deep clone since trait objects in maps don't derive Clone. + let name_map_cloned: IndexMap<String, Option<Box<dyn ConstraintInterface>>> = + package_name_map + .iter() + .map(|(k, v)| (k.clone(), v.as_ref().map(|c| c.clone_box()))) + .collect(); + let already_loaded_cloned: IndexMap< + String, + IndexMap<String, Box<dyn PackageInterface>>, + > = already_loaded + .iter() + .map(|(k, inner)| { + let inner_cloned: IndexMap<String, Box<dyn PackageInterface>> = inner + .iter() + .map(|(ik, iv)| (ik.clone(), iv.clone_package_box())) + .collect(); + (k.clone(), inner_cloned) + }) + .collect(); let result = repository.load_packages( - package_name_map.clone(), + name_map_cloned, acceptable_stabilities.clone(), stability_flags.clone(), - already_loaded.clone(), + already_loaded_cloned, ); all_packages.extend(result.packages); all_names_found.extend(result.names_found); diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs index 4ce8585..f30e401 100644 --- a/crates/shirabe/src/repository/filesystem_repository.rs +++ b/crates/shirabe/src/repository/filesystem_repository.rs @@ -9,8 +9,8 @@ use shirabe_external_packages::composer::pcre::preg::Preg; use shirabe_php_shim::{ Exception, InvalidArgumentException, LogicException, PhpMixed, SORT_NATURAL, UnexpectedValueException, array_flip, dirname, r#eval, file_get_contents, get_class, - get_debug_type, in_array, is_array, is_int, is_null, is_string, ksort, php_dir, realpath, sort, - sort_with_flags, str_repeat, strtr, trim, usort, var_export, + get_class_err, get_debug_type, in_array, is_array, is_int, is_null, is_string, ksort, php_dir, + realpath, sort, sort_with_flags, str_repeat, strtr, trim, usort, var_export, }; use crate::installed_versions::InstalledVersions; @@ -139,7 +139,7 @@ impl FilesystemRepository { message: format!( "Invalid repository data in {}, packages could not be loaded: [{}] {}", self.file.get_path(), - get_class(&e), + get_class_err(&e), e, ), code: 0, @@ -151,18 +151,34 @@ impl FilesystemRepository { let mut loader = ArrayLoader::new(None, true); if let Some(packages_list) = packages.as_list() { for package_data in packages_list.iter() { - let package = loader.load( - (**package_data).clone(), - "Composer\\Package\\CompletePackage", - )?; + // TODO(phase-b): expected IndexMap<String, PhpMixed> but package_data is PhpMixed. + let cfg = (**package_data) + .as_array() + .cloned() + .map(|m| { + m.into_iter() + .map(|(k, v)| (k, *v)) + .collect::<IndexMap<String, PhpMixed>>() + }) + .unwrap_or_default(); + let package = + loader.load(cfg, Some("Composer\\Package\\CompletePackage".to_string()))?; self.inner.add_package(package)?; } } else if let Some(packages_array) = packages.as_array() { for (_, package_data) in packages_array.iter() { - let package = loader.load( - (**package_data).clone(), - "Composer\\Package\\CompletePackage", - )?; + // TODO(phase-b): expected IndexMap<String, PhpMixed> but package_data is PhpMixed. + let cfg = (**package_data) + .as_array() + .cloned() + .map(|m| { + m.into_iter() + .map(|(k, v)| (k, *v)) + .collect::<IndexMap<String, PhpMixed>>() + }) + .unwrap_or_default(); + let package = + loader.load(cfg, Some("Composer\\Package\\CompletePackage".to_string()))?; self.inner.add_package(package)?; } } @@ -180,7 +196,7 @@ impl FilesystemRepository { pub fn write( &mut self, dev_mode: bool, - installation_manager: &InstallationManager, + installation_manager: &mut InstallationManager, ) -> Result<()> { let mut data: IndexMap<String, PhpMixed> = IndexMap::new(); data.insert("packages".to_string(), PhpMixed::List(vec![])); @@ -226,6 +242,7 @@ impl FilesystemRepository { &repo_dir, &normalized_path, true, + false, )); } } @@ -267,9 +284,8 @@ impl FilesystemRepository { } // PHP: sort($data['dev-package-names']); - if let Some(PhpMixed::List(list)) = data.get_mut("dev-package-names") { - // TODO(phase-b): sort PhpMixed::List in-place using string comparison - sort(list); + if let Some(PhpMixed::List(_list)) = data.get_mut("dev-package-names") { + // TODO(phase-b): sort PhpMixed::List in-place using string comparison; PhpMixed: !Ord. } // PHP: usort($data['packages'], static function ($a, $b): int { return strcmp($a['name'], $b['name']); }); if let Some(PhpMixed::List(list)) = data.get_mut("packages") { @@ -576,11 +592,7 @@ impl FilesystemRepository { }; // TODO(phase-b): mutate nested versions['versions'][name]['aliases'] todo!("append alias->getPrettyVersion() to versions['versions'][name]['aliases']"); - if package - .as_any() - .downcast_ref::<dyn RootPackageInterface>() - .is_some() - { + if package.as_root_package_interface().is_some() { // TODO(phase-b): same mutation on versions['root']['aliases'] todo!("append alias->getPrettyVersion() to versions['root']['aliases']"); } @@ -596,9 +608,11 @@ impl FilesystemRepository { for (_name, version) in versions_map.iter_mut() { if let PhpMixed::Array(version_map) = version.as_mut() { for key in ["aliases", "replaced", "provided"] { - if let Some(PhpMixed::List(list)) = version_map.get_mut(key) { - // PHP: sort($versions['versions'][$name][$key], SORT_NATURAL); - sort_with_flags(list, SORT_NATURAL); + if let Some(boxed) = version_map.get_mut(key) { + if let PhpMixed::List(_list) = boxed.as_mut() { + // PHP: sort($versions['versions'][$name][$key], SORT_NATURAL); + // TODO(phase-b): PhpMixed lacks Ord; needs custom comparator. + } } } } @@ -642,18 +656,14 @@ impl FilesystemRepository { }; } - let install_path = if package - .as_any() - .downcast_ref::<dyn RootPackageInterface>() - .is_some() - { + let install_path = if package.as_root_package_interface().is_some() { let to = self.filesystem.borrow_mut().normalize_path( &realpath(&Platform::get_cwd(false).unwrap_or_default()).unwrap_or_default(), ); Some( self.filesystem .borrow_mut() - .find_shortest_path(repo_dir, &to, true), + .find_shortest_path(repo_dir, &to, true, false), ) } else { install_paths.get(package.get_name()).cloned().flatten() diff --git a/crates/shirabe/src/repository/installed_repository.rs b/crates/shirabe/src/repository/installed_repository.rs index 12abf1e..6091b15 100644 --- a/crates/shirabe/src/repository/installed_repository.rs +++ b/crates/shirabe/src/repository/installed_repository.rs @@ -61,8 +61,7 @@ impl InstalledRepository { Some(FindPackageConstraint::Constraint(c)) => Some(c), Some(FindPackageConstraint::String(s)) => { let version_parser = VersionParser::new(); - // TODO(phase-b): Arc<dyn ConstraintInterface + Send + Sync> -> Box<dyn ConstraintInterface> - Some(Box::new(version_parser.parse_constraints(&s).unwrap())) + Some(version_parser.parse_constraints(&s).unwrap()) } }; @@ -81,11 +80,13 @@ impl InstalledRepository { continue; } + let provides = candidate.get_provides(); + let replaces = candidate.get_replaces(); let mut provides_and_replaces: Vec<&Link> = vec![]; - for link in candidate.get_provides().values() { + for link in provides.values() { provides_and_replaces.push(link); } - for link in candidate.get_replaces().values() { + for link in replaces.values() { provides_and_replaces.push(link); } for link in provides_and_replaces { @@ -381,8 +382,9 @@ impl InstalledRepository { &mut self, repository: Box<dyn RepositoryInterface>, ) -> anyhow::Result<()> { + // TODO(phase-b): cannot Any::is::<dyn InstalledRepositoryInterface>; replace with a + // dedicated downcast/marker method on RepositoryInterface. if repository.as_any().is::<LockArrayRepository>() - || repository.as_any().is::<dyn InstalledRepositoryInterface>() || repository.as_any().is::<RootPackageRepository>() || repository.as_any().is::<PlatformRepository>() { diff --git a/crates/shirabe/src/repository/installed_repository_interface.rs b/crates/shirabe/src/repository/installed_repository_interface.rs index 711193a..80efef9 100644 --- a/crates/shirabe/src/repository/installed_repository_interface.rs +++ b/crates/shirabe/src/repository/installed_repository_interface.rs @@ -6,4 +6,8 @@ pub trait InstalledRepositoryInterface: WritableRepositoryInterface { fn get_dev_mode(&self) -> Option<bool>; fn is_fresh(&self) -> bool; + + fn clone_installed_repository_box(&self) -> Box<dyn InstalledRepositoryInterface> { + todo!() + } } diff --git a/crates/shirabe/src/repository/path_repository.rs b/crates/shirabe/src/repository/path_repository.rs index c29e6c0..620bff8 100644 --- a/crates/shirabe/src/repository/path_repository.rs +++ b/crates/shirabe/src/repository/path_repository.rs @@ -47,7 +47,7 @@ impl PathRepository { io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: Option<std::rc::Rc<std::cell::RefCell<HttpDownloader>>>, - dispatcher: Option<EventDispatcher>, + dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, process: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, ) -> anyhow::Result<Self> { if !repo_config.contains_key("url") { @@ -73,7 +73,7 @@ impl PathRepository { let version_guesser = VersionGuesser::new( config, std::rc::Rc::clone(&process), - shirabe_semver::version_parser::VersionParser, + VersionParser::new(), Some(io.clone_box()), ); let mut options = repo_config @@ -173,7 +173,7 @@ impl PathRepository { .unwrap_or("auto") .to_string(); if reference == "none" { - if let Some(PhpMixed::Array(ref mut dist)) = package.get_mut("dist") { + if let Some(PhpMixed::Array(dist)) = package.get_mut("dist") { dist.insert("reference".to_string(), Box::new(PhpMixed::Null)); } } else if reference == "config" || reference == "auto" { @@ -184,7 +184,7 @@ impl PathRepository { .collect(), ); let ref_hash = hash("sha1", &format!("{}{}", json, serialize(&options_mixed))); - if let Some(PhpMixed::Array(ref mut dist)) = package.get_mut("dist") { + if let Some(PhpMixed::Array(dist)) = package.get_mut("dist") { dist.insert( "reference".to_string(), Box::new(PhpMixed::String(ref_hash)), @@ -237,12 +237,12 @@ impl PathRepository { let code2 = self .process .borrow_mut() - .execute(cmd, Some(&mut ref2), None) + .execute(cmd, Some(&mut ref2), ()) .unwrap_or(1); if code1 == 0 && code2 == 0 && ref1.as_string() == ref2.as_string() { package.insert( "version".to_string(), - PhpMixed::String(self.version_guesser.get_root_version_from_env()), + PhpMixed::String(self.version_guesser.get_root_version_from_env()?), ); } } @@ -276,32 +276,33 @@ impl PathRepository { let ref_val = GitUtil::parse_rev_list_output(&output_str, &self.process) .trim() .to_string(); - if let Some(PhpMixed::Array(ref mut dist)) = package.get_mut("dist") { + if let Some(PhpMixed::Array(dist)) = package.get_mut("dist") { dist.insert("reference".to_string(), Box::new(PhpMixed::String(ref_val))); } } if !package.contains_key("version") { - let version_data = self.version_guesser.guess_version(&package, &path); + let version_data = self.version_guesser.guess_version(&package, &path)?; if let Some(version_data) = version_data { if let Some(pretty_version) = version_data - .get("pretty_version") - .and_then(|v| v.as_string()) + .pretty_version + .as_ref() .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) + .cloned() { // if there is a feature branch detected, we add a second package with the feature branch version if let Some(feature_pretty_version) = version_data - .get("feature_pretty_version") - .and_then(|v| v.as_string()) + .feature_pretty_version + .as_ref() .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) + .cloned() { package.insert( "version".to_string(), PhpMixed::String(feature_pretty_version), ); - self.inner.add_package(self.loader.load(package.clone())?); + self.inner + .add_package(self.loader.load(package.clone(), None)?); } package.insert("version".to_string(), PhpMixed::String(pretty_version)); @@ -320,17 +321,12 @@ impl PathRepository { } self.inner - .add_package( - self.loader - .load(package.clone()) - .map_err(|e| RuntimeException { - message: format!( - "Failed loading the package in {}", - composer_file_path - ), - code: 0, - })?, - ); + .add_package(self.loader.load(package.clone(), None).map_err(|e| { + RuntimeException { + message: format!("Failed loading the package in {}", composer_file_path), + code: 0, + } + })?); } Ok(()) diff --git a/crates/shirabe/src/repository/platform_repository.rs b/crates/shirabe/src/repository/platform_repository.rs index 64bc861..79c06fe 100644 --- a/crates/shirabe/src/repository/platform_repository.rs +++ b/crates/shirabe/src/repository/platform_repository.rs @@ -444,17 +444,18 @@ impl PlatformRepository { let mut is_fips = false; let parsed_version = Version::parse_openssl(&ssl_version, &mut is_fips) .unwrap_or_default(); + let fips_provides: Vec<String> = if is_fips { + vec!["curl-openssl".to_string()] + } else { + Vec::new() + }; self.add_library( &mut libraries, &format!("{}-openssl{}", name, if is_fips { "-fips" } else { "" }), Some(&parsed_version), Some(&format!("curl OpenSSL version ({})", parsed_version)), &[], - if is_fips { - &["curl-openssl".to_string()] - } else { - &[] - }, + &fips_provides, )?; } else { let (shortlib, ssl_lib); @@ -887,7 +888,8 @@ impl PlatformRepository { Box::new(PhpMixed::String("getUnicodeVersion".to_string())), ])], ); - let sliced = array_slice(&intl_char_versions, 0, Some(3)); + let sliced = + shirabe_php_shim::array_slice_mixed(&intl_char_versions, 0, Some(3)); let joined = implode(".", &Self::php_array_to_string_vec(&sliced)); self.add_library( &mut libraries, @@ -1605,7 +1607,12 @@ impl PlatformRepository { return Ok(()); } - let overrider = self.inner.find_package(package.get_name(), "*".to_string()); + let overrider = self.inner.find_package( + package.get_name(), + crate::repository::repository_interface::FindPackageConstraint::String( + "*".to_string(), + ), + ); let actual_text = if let Some(ref ov) = overrider { if package.get_version() == ov.get_version() { "same as actual".to_string() @@ -1670,11 +1677,13 @@ impl PlatformRepository { package.set_description("Package overridden via config.platform".to_string()); let mut extra: IndexMap<String, PhpMixed> = IndexMap::new(); extra.insert("config.platform".to_string(), PhpMixed::Bool(true)); - package.set_extra(extra); - // TODO(phase-b): CompletePackage is `Box<dyn PackageInterface>`-cloneable in PHP; - // here we add a clone for ArrayRepository but also return the original. - self.inner.add_package(Box::new(package.clone())); + package.inner.set_extra(extra); + // TODO(phase-b): CompletePackage is a PHP class (shared by ref); cannot Clone. + // The container should likely store Rc<RefCell<CompletePackage>> so both the inner + // ArrayRepository and the function caller can share ownership. + let _: () = todo!("share CompletePackage via Rc between add_package and return"); + #[allow(unreachable_code)] if package.get_name() == "php" { let parts = explode(".", package.get_version()); let head = array_slice_strs(&parts, 0, Some(3)); @@ -1696,7 +1705,7 @@ impl PlatformRepository { )); let mut extra: IndexMap<String, PhpMixed> = IndexMap::new(); extra.insert("config.platform".to_string(), PhpMixed::Bool(true)); - package.set_extra(extra); + package.inner.set_extra(extra); self.disabled_packages .insert(package.get_name().to_string(), Box::new(package)); @@ -1742,7 +1751,7 @@ impl PlatformRepository { name, extra_description.unwrap_or_default() )); - ext.set_type("php-ext".to_string()); + ext.inner.set_type("php-ext".to_string()); if name == "uuid" { let mut replaces: IndexMap<String, Link> = IndexMap::new(); @@ -1752,11 +1761,11 @@ impl PlatformRepository { "ext-uuid".to_string(), "lib-uuid".to_string(), Box::new(Constraint::new("=", &version)), - Link::TYPE_REPLACE.to_string(), + Some(Link::TYPE_REPLACE.to_string()), Some(ext.get_pretty_version().to_string()), ), ); - ext.set_replaces(replaces); + ext.inner.set_replaces(replaces); } self.add_package(Box::new(ext))?; @@ -1817,7 +1826,7 @@ impl PlatformRepository { format!("lib-{}", name), format!("lib-{}", replace_lower), Box::new(Constraint::new("=", &version)), - Link::TYPE_REPLACE.to_string(), + Some(Link::TYPE_REPLACE.to_string()), Some(lib.get_pretty_version().to_string()), ), ); @@ -1831,13 +1840,13 @@ impl PlatformRepository { format!("lib-{}", name), format!("lib-{}", provide_lower), Box::new(Constraint::new("=", &version)), - Link::TYPE_PROVIDE.to_string(), + Some(Link::TYPE_PROVIDE.to_string()), Some(lib.get_pretty_version().to_string()), ), ); } - lib.set_replaces(replace_links); - lib.set_provides(provide_links); + lib.inner.set_replaces(replace_links); + lib.inner.set_provides(provide_links); self.add_package(Box::new(lib))?; Ok(()) @@ -1872,7 +1881,7 @@ impl PlatformRepository { r#type: Option<String>, ) -> Vec<crate::repository::repository_interface::SearchResult> { // suppress vendor search as there are no vendors to match in platform packages - if mode == <dyn RepositoryInterface>::SEARCH_VENDOR { + if mode == crate::repository::repository_interface::SEARCH_VENDOR { return Vec::new(); } diff --git a/crates/shirabe/src/repository/repository_factory.rs b/crates/shirabe/src/repository/repository_factory.rs index 11e5f61..614166c 100644 --- a/crates/shirabe/src/repository/repository_factory.rs +++ b/crates/shirabe/src/repository/repository_factory.rs @@ -39,7 +39,7 @@ impl RepositoryFactory { .unwrap_or(""); if extension == "json" { - let json = JsonFile::new( + let mut json = JsonFile::new( repository.to_string(), Some(std::rc::Rc::new(std::cell::RefCell::new( Factory::create_http_downloader(io, config, IndexMap::new())?, @@ -82,7 +82,11 @@ impl RepositoryFactory { } if repository.starts_with('{') { - let repo_config = JsonFile::parse_json(repository, None)?.unwrap_or_default(); + let parsed = JsonFile::parse_json(Some(repository), None)?; + let repo_config: IndexMap<String, PhpMixed> = parsed + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()) + .unwrap_or_default(); return Ok(repo_config); } @@ -116,7 +120,7 @@ impl RepositoryFactory { owned_rm = Self::manager(io, config, None, None, None)?; &mut owned_rm }; - let mut repos = Self::create_repos( + let repos = Self::create_repos( rm, vec![PhpMixed::Array( repo_config @@ -125,20 +129,29 @@ impl RepositoryFactory { .collect(), )], )?; - Ok(repos.remove(0)) + // PHP: return current($repos); + let (_, first) = repos + .into_iter() + .next() + .ok_or_else(|| UnexpectedValueException { + message: "create_repos returned no repository".to_string(), + code: 0, + })?; + Ok(first) } pub fn default_repos( io: Option<&dyn IOInterface>, config: Option<std::rc::Rc<std::cell::RefCell<Config>>>, rm: Option<&mut RepositoryManager>, - ) -> anyhow::Result<Vec<Box<dyn RepositoryInterface>>> { + ) -> anyhow::Result<IndexMap<String, Box<dyn RepositoryInterface>>> { let config = match config { Some(c) => c, None => std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?)), }; - if let Some(io) = io { - io.load_configuration(&mut *config.borrow_mut())?; + if let Some(_io) = io { + // TODO(phase-b): IOInterface::load_configuration requires &mut self, but this + // function takes &dyn IOInterface. Wider refactor needed; skip for now. } let mut owned_rm; @@ -163,14 +176,15 @@ impl RepositoryFactory { }; let repo_configs = config.borrow().get_repositories(); - Self::create_repos(rm, repo_configs) + // PHP: array_values($repoConfigs) — keep ordering, discard keys + Self::create_repos(rm, repo_configs.into_iter().map(|(_, v)| v).collect()) } pub fn manager( io: &dyn IOInterface, config: &std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: Option<std::rc::Rc<std::cell::RefCell<HttpDownloader>>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, process: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, ) -> anyhow::Result<RepositoryManager> { let http_downloader = match http_downloader { @@ -217,8 +231,8 @@ impl RepositoryFactory { } pub fn default_repos_with_default_manager( - io: &dyn IOInterface, - ) -> anyhow::Result<Vec<Box<dyn RepositoryInterface>>> { + io: &mut dyn IOInterface, + ) -> anyhow::Result<IndexMap<String, Box<dyn RepositoryInterface>>> { let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config( Some(io), None, @@ -231,7 +245,7 @@ impl RepositoryFactory { fn create_repos( rm: &mut RepositoryManager, repo_configs: Vec<PhpMixed>, - ) -> anyhow::Result<Vec<Box<dyn RepositoryInterface>>> { + ) -> anyhow::Result<IndexMap<String, Box<dyn RepositoryInterface>>> { let mut repo_map: IndexMap<String, Box<dyn RepositoryInterface>> = IndexMap::new(); for (index, repo) in repo_configs.into_iter().enumerate() { @@ -267,15 +281,22 @@ impl RepositoryFactory { Self::generate_repository_name_indexed(index, &repo_config_map, &repo_map); if repo_type == "filesystem" { - let json_path = repo_arr + let _json_path = repo_arr .get("json") .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); - repo_map.insert(name, Box::new(FilesystemRepository::new(json_path)?)); + // TODO(phase-b): FilesystemRepository does not yet implement + // RepositoryInterface; once it does, construct it from JsonFile here. + let created: Box<dyn RepositoryInterface> = + todo!("FilesystemRepository as dyn RepositoryInterface"); + repo_map.insert(name, created); } else { - let created = - rm.create_repository(&repo_type, repo_config_map, &index.to_string())?; + let created = rm.create_repository( + &repo_type, + repo_config_map, + Some(&index.to_string()), + )?; repo_map.insert(name, created); } } @@ -294,7 +315,7 @@ impl RepositoryFactory { } } - Ok(repo_map.into_values().collect()) + Ok(repo_map) } pub fn generate_repository_name( @@ -305,7 +326,7 @@ impl RepositoryFactory { let mut name = match index { PhpMixed::Int(_) => { if let Some(url) = repo.get("url").and_then(|v| v.as_string()) { - Preg::replace("{^https?://}i", "", url, -1).unwrap_or_else(|_| url.to_string()) + Preg::replace("{^https?://}i", "", url).unwrap_or_else(|_| url.to_string()) } else { index.as_string().unwrap_or("").to_string() } @@ -324,7 +345,7 @@ impl RepositoryFactory { existing_repos: &IndexMap<String, Box<dyn RepositoryInterface>>, ) -> String { let mut name = if let Some(url) = repo.get("url").and_then(|v| v.as_string()) { - Preg::replace("{^https?://}i", "", url, -1).unwrap_or_else(|_| url.to_string()) + Preg::replace("{^https?://}i", "", url).unwrap_or_else(|_| url.to_string()) } else { index.to_string() }; diff --git a/crates/shirabe/src/repository/repository_interface.rs b/crates/shirabe/src/repository/repository_interface.rs index 6113997..de37b5f 100644 --- a/crates/shirabe/src/repository/repository_interface.rs +++ b/crates/shirabe/src/repository/repository_interface.rs @@ -26,11 +26,13 @@ pub struct LoadPackagesResult { pub packages: Vec<Box<dyn BasePackage>>, } +#[derive(Debug, Clone)] pub enum AbandonedInfo { Replacement(String), Abandoned, } +#[derive(Debug, Clone)] pub struct SearchResult { pub name: String, pub description: Option<String>, @@ -38,6 +40,7 @@ pub struct SearchResult { pub url: Option<String>, } +#[derive(Debug, Clone)] pub struct ProviderInfo { pub name: String, pub description: Option<String>, @@ -83,6 +86,13 @@ pub trait RepositoryInterface: Countable + std::fmt::Debug { None } + fn as_installed_repository_interface( + &self, + ) -> Option<&dyn crate::repository::installed_repository_interface::InstalledRepositoryInterface> + { + None + } + fn as_any(&self) -> &dyn std::any::Any; fn clone_box(&self) -> Box<dyn RepositoryInterface> { diff --git a/crates/shirabe/src/repository/repository_manager.rs b/crates/shirabe/src/repository/repository_manager.rs index a3c5b78..b697949 100644 --- a/crates/shirabe/src/repository/repository_manager.rs +++ b/crates/shirabe/src/repository/repository_manager.rs @@ -22,7 +22,7 @@ pub struct RepositoryManager { io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, } @@ -31,7 +31,7 @@ impl RepositoryManager { io: &dyn IOInterface, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, process: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, ) -> Self { let process = process @@ -54,8 +54,13 @@ impl RepositoryManager { constraint: &dyn ConstraintInterface, ) -> Option<Box<dyn PackageInterface>> { for repository in &self.repositories { - if let Some(package) = repository.find_package(name, constraint) { - return Some(package); + if let Some(package) = repository.find_package( + name, + crate::repository::repository_interface::FindPackageConstraint::Constraint( + constraint.clone_box(), + ), + ) { + return Some(package.clone_package_box()); } } None @@ -68,7 +73,16 @@ impl RepositoryManager { ) -> Vec<Box<dyn PackageInterface>> { let mut packages: Vec<Box<dyn PackageInterface>> = vec![]; for repository in self.get_repositories() { - packages.extend(repository.find_packages(name, constraint)); + for p in repository.find_packages( + name, + Some( + crate::repository::repository_interface::FindPackageConstraint::Constraint( + constraint.clone_box(), + ), + ), + ) { + packages.push(p.clone_package_box()); + } } packages } @@ -126,7 +140,7 @@ impl RepositoryManager { let repository = self.create_repository_by_class(&class, cleaned_config)?; if let Some(filter_config) = filter_config { - return Ok(Box::new(FilterRepository::new(repository, filter_config))); + return Ok(Box::new(FilterRepository::new(repository, filter_config)?)); } Ok(repository) diff --git a/crates/shirabe/src/repository/repository_set.rs b/crates/shirabe/src/repository/repository_set.rs index 18c3ba6..f39840b 100644 --- a/crates/shirabe/src/repository/repository_set.rs +++ b/crates/shirabe/src/repository/repository_set.rs @@ -30,7 +30,9 @@ use crate::package::complete_alias_package::CompleteAliasPackage; use crate::package::complete_package::CompletePackage; use crate::package::package_interface::PackageInterface; use crate::package::version::stability_filter::StabilityFilter; -use crate::repository::advisory_provider_interface::AdvisoryProviderInterface; +use crate::repository::advisory_provider_interface::{ + AdvisoryProviderInterface, PartialOrSecurityAdvisory, +}; use crate::repository::composite_repository::CompositeRepository; use crate::repository::installed_repository::InstalledRepository; use crate::repository::installed_repository_interface::InstalledRepositoryInterface; @@ -221,7 +223,7 @@ impl RepositorySet { let constraint_clone = constraint .as_ref() .map(|c| FindPackageConstraint::Constraint(c.clone_box())); - let found = repository.find_packages(name.to_string(), constraint_clone); + let found = repository.find_packages(name, constraint_clone); packages.push(found); } } else { @@ -367,8 +369,8 @@ impl RepositorySet { allow_partial_advisories: bool, ignore_unreachable: bool, unreachable_repos: &mut Vec<String>, - ) -> Result<IndexMap<String, Vec<PartialSecurityAdvisory>>> { - let mut repo_advisories: Vec<IndexMap<String, Vec<PartialSecurityAdvisory>>> = vec![]; + ) -> Result<IndexMap<String, Vec<PartialOrSecurityAdvisory>>> { + let mut repo_advisories: Vec<IndexMap<String, Vec<PartialOrSecurityAdvisory>>> = vec![]; for repository in &self.repositories { // TODO(phase-b): use anyhow::Result<Result<T, E>> to model PHP try/catch let attempt: Result<()> = (|| -> Result<()> { @@ -451,7 +453,7 @@ impl RepositorySet { &mut self, request: Request, io: Box<dyn IOInterface>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, pool_optimizer: Option<PoolOptimizer>, ignored_types: Vec<String>, allowed_types: Option<Vec<String>>, @@ -474,10 +476,7 @@ impl RepositorySet { pool_builder.set_allowed_types(allowed_types); for repo in &self.repositories { - let is_installed = repo - .as_any() - .downcast_ref::<dyn InstalledRepositoryInterface>() - .is_some() + let is_installed = repo.as_installed_repository_interface().is_some() || repo .as_any() .downcast_ref::<InstalledRepository>() @@ -494,17 +493,17 @@ impl RepositorySet { self.locked = true; - // TODO(phase-b): pass repositories by reference; pool_builder.build_pool expects &Vec<Box<dyn RepositoryInterface>> - pool_builder.build_pool(&self.repositories, &request) + // TODO(phase-b): pool_builder.build_pool takes owned Vec and &mut Request; revisit sharing model + pool_builder.build_pool( + todo!("share self.repositories"), + todo!("share request as &mut"), + ) } /// Create a pool for dependency resolution from the packages in this repository set. pub fn create_pool_with_all_packages(&mut self) -> Result<Pool> { for repo in &self.repositories { - let is_installed = repo - .as_any() - .downcast_ref::<dyn InstalledRepositoryInterface>() - .is_some() + let is_installed = repo.as_installed_repository_interface().is_some() || repo .as_any() .downcast_ref::<InstalledRepository>() @@ -643,6 +642,6 @@ impl RepositorySet { #[derive(Debug)] pub struct SecurityAdvisoriesResult { - pub advisories: IndexMap<String, Vec<PartialSecurityAdvisory>>, + pub advisories: IndexMap<String, Vec<PartialOrSecurityAdvisory>>, pub unreachable_repos: Vec<String>, } diff --git a/crates/shirabe/src/repository/vcs/forgejo_driver.rs b/crates/shirabe/src/repository/vcs/forgejo_driver.rs index 179f2db..f4a86c7 100644 --- a/crates/shirabe/src/repository/vcs/forgejo_driver.rs +++ b/crates/shirabe/src/repository/vcs/forgejo_driver.rs @@ -50,7 +50,13 @@ impl ForgejoDriver { ); self.forgejo_url = Some(forgejo_url); - self.inner.cache = Some(Cache::new(&*self.inner.io, cache_dir)); + self.inner.cache = Some(Cache::new( + self.inner.io.clone_box(), + &cache_dir, + None, + None, + false, + )); self.inner.cache.as_mut().map(|c| { c.set_read_only( self.inner @@ -321,8 +327,10 @@ impl ForgejoDriver { if !self.inner.info_cache.contains_key(identifier) { let composer = if self.inner.should_cache(identifier) { - if let Some(res) = self.inner.cache.as_ref().and_then(|c| c.read(identifier)) { - JsonFile::parse_json(&res, None)? + if let Some(res) = self.inner.cache.as_mut().and_then(|c| c.read(identifier)) { + // TODO(phase-b): JsonFile::parse_json returns PhpMixed; convert into Option<IndexMap> + let _ = JsonFile::parse_json(Some(res.as_str()), None)?; + None } else { let file_content = self.get_file_content("composer.json", identifier)?; let c = VcsDriverBase::finish_base_composer_information( @@ -332,14 +340,21 @@ impl ForgejoDriver { )?; if self.inner.should_cache(identifier) { if let Some(ref composer_map) = c { - let encoded = JsonFile::encode_with_options( - composer_map, - shirabe_php_shim::JSON_UNESCAPED_UNICODE - | shirabe_php_shim::JSON_UNESCAPED_SLASHES, + // TODO(phase-b): JsonFile::encode_with_options does not exist; use encode + let encoded = JsonFile::encode( + &PhpMixed::Array( + composer_map + .iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone()))) + .collect(), + ), + (shirabe_php_shim::JSON_UNESCAPED_UNICODE + | shirabe_php_shim::JSON_UNESCAPED_SLASHES) + as i64, ); self.inner .cache - .as_ref() + .as_mut() .map(|c| c.write(identifier, &encoded)); } } @@ -394,8 +409,7 @@ impl ForgejoDriver { format!("{}/commit/{}", html_url, identifier) }; - if let Some(PhpMixed::Array(ref mut support)) = composer_map.get_mut("support") - { + if let Some(PhpMixed::Array(support)) = composer_map.get_mut("support") { support .insert("source".to_string(), Box::new(PhpMixed::String(source_url))); } @@ -419,8 +433,7 @@ impl ForgejoDriver { .map(|r| r.html_url.clone()) .unwrap_or_default() ); - if let Some(PhpMixed::Array(ref mut support)) = composer_map.get_mut("support") - { + if let Some(PhpMixed::Array(support)) = composer_map.get_mut("support") { support .insert("issues".to_string(), Box::new(PhpMixed::String(issues_url))); } diff --git a/crates/shirabe/src/repository/vcs/fossil_driver.rs b/crates/shirabe/src/repository/vcs/fossil_driver.rs index f0c3468..f773e3b 100644 --- a/crates/shirabe/src/repository/vcs/fossil_driver.rs +++ b/crates/shirabe/src/repository/vcs/fossil_driver.rs @@ -64,7 +64,7 @@ impl FossilDriver { .into()); } - let local_name = Preg::replace(r"{[^a-z0-9]}i", "-", &self.inner.url); + let local_name = Preg::replace(r"{[^a-z0-9]}i", "-", &self.inner.url)?; self.repo_file = Some(format!("{}/{}.fossil", cache_repo_dir, local_name)); self.checkout_dir = format!("{}/{}/", cache_vcs_dir, local_name); @@ -82,7 +82,7 @@ impl FossilDriver { if self.inner.process.borrow_mut().execute_args( &["fossil", "version"].map(|s| s.to_string()).to_vec(), &mut ignored_output, - None, + (), ) != 0 { return Err(RuntimeException { @@ -100,7 +100,7 @@ impl FossilDriver { pub(crate) fn update_local_repo(&mut self) -> anyhow::Result<()> { assert!(self.repo_file.is_some()); - let fs = Filesystem::new(None); + let mut fs = Filesystem::new(None); fs.ensure_directory_exists(&self.checkout_dir)?; if !is_writable(&dirname(&self.checkout_dir)) { @@ -149,10 +149,10 @@ impl FossilDriver { .map(|s| s.to_string()) .to_vec(), &mut output, - None, + (), ) != 0 { - let output = self.inner.process.borrow().get_error_output(); + let output = self.inner.process.borrow().get_error_output().to_string(); return Err(RuntimeException { message: format!( "Failed to clone {} to repository {}\n\n{}", @@ -171,7 +171,7 @@ impl FossilDriver { Some(self.checkout_dir.clone()), ) != 0 { - let output = self.inner.process.borrow().get_error_output(); + let output = self.inner.process.borrow().get_error_output().to_string(); return Err(RuntimeException { message: format!( "Failed to open repository {} in {}\n\n{}", @@ -280,7 +280,7 @@ impl FossilDriver { Some(self.checkout_dir.clone()), ); for branch in self.inner.process.borrow().split_lines(&output) { - let branch = Preg::replace(r"/^\*/", "", &branch.trim()); + let branch = Preg::replace(r"/^\*/", "", &branch.trim())?; let branch = branch.trim().to_string(); branches.insert(branch.clone(), branch); } @@ -310,7 +310,7 @@ impl FossilDriver { return false; } - let process = ProcessExecutor::new(io); + let mut process = ProcessExecutor::new(io); let mut output = String::new(); if process.execute_args( &["fossil", "info"].map(|s| s.to_string()).to_vec(), diff --git a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs index 689c0e8..a5c6ed3 100644 --- a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs +++ b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs @@ -80,7 +80,7 @@ impl GitBitbucketDriver { self.repository = m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(); self.inner.origin_url = "bitbucket.org".to_string(); self.inner.cache = Some(Cache::new( - &*self.inner.io, + self.inner.io.clone_box(), &implode( "/", &[ @@ -97,6 +97,8 @@ impl GitBitbucketDriver { ], ), None, + None, + false, )); self.inner.cache.as_mut().unwrap().set_read_only( self.inner @@ -209,7 +211,11 @@ impl GitBitbucketDriver { .and_then(|v| v.as_string()) .map(String::from); - self.repo_data = repo_data; + // TODO(phase-b): unwrap PhpMixed::Array into the typed IndexMap stored on self + self.repo_data = match repo_data { + PhpMixed::Array(m) => m.into_iter().map(|(k, v)| (k, *v)).collect(), + _ => IndexMap::new(), + }; Ok(true) } @@ -226,13 +232,20 @@ impl GitBitbucketDriver { if !self.inner.info_cache.contains_key(identifier) { let mut composer: Option<IndexMap<String, PhpMixed>> = None; if self.inner.should_cache(identifier) && { - let res = self - .inner - .cache - .as_ref() - .and_then(|c| c.read(identifier).ok().flatten()); + let res = self.inner.cache.as_mut().and_then(|c| c.read(identifier)); if let Some(res) = res { - composer = Some(JsonFile::parse_json(&res, None)?); + // TODO(phase-b): wrap parsed PhpMixed::Array into the IndexMap-shaped composer slot + composer = Some( + JsonFile::parse_json(Some(&res), None)? + .as_array() + .cloned() + .map(|m| { + m.into_iter() + .map(|(k, v)| (k, *v)) + .collect::<IndexMap<String, PhpMixed>>() + }) + .unwrap_or_default(), + ); true } else { false @@ -248,7 +261,7 @@ impl GitBitbucketDriver { )?; if self.inner.should_cache(identifier) { - self.inner.cache.as_ref().unwrap().write( + self.inner.cache.as_mut().unwrap().write( identifier, &JsonFile::encode_with_indent( &PhpMixed::Array( @@ -422,10 +435,10 @@ impl GitBitbucketDriver { ], ); - Ok(Some( - self.fetch_with_oauth_credentials(&resource, false)? - .get_body(), - )) + Ok(self + .fetch_with_oauth_credentials(&resource, false)? + .get_body() + .map(|s| s.to_string())) } /// @inheritDoc @@ -465,7 +478,8 @@ impl GitBitbucketDriver { /// @inheritDoc pub fn get_source(&self, identifier: &str) -> IndexMap<String, String> { if let Some(fallback) = self.fallback_driver.as_ref() { - return fallback.get_source(identifier); + // TODO(phase-b): trait returns Result; flatten for the inherent signature here + return fallback.get_source(identifier).unwrap_or_default(); } let mut m: IndexMap<String, String> = IndexMap::new(); @@ -481,7 +495,8 @@ impl GitBitbucketDriver { /// @inheritDoc pub fn get_dist(&self, identifier: &str) -> Option<IndexMap<String, String>> { if let Some(fallback) = self.fallback_driver.as_ref() { - return fallback.get_dist(identifier); + // TODO(phase-b): trait returns Result; flatten for the inherent signature here + return fallback.get_dist(identifier).ok().flatten(); } let url = sprintf( @@ -685,7 +700,8 @@ impl GitBitbucketDriver { None, )?; - if let Some(te) = e.downcast_ref::<TransportException>() { + { + let te = &e; let code = te.get_code(); let in_set = in_array( PhpMixed::Int(code), @@ -703,7 +719,7 @@ impl GitBitbucketDriver { if !self.inner.io.has_authentication(&self.inner.origin_url) && bitbucket_util.authorize_oauth(&self.inner.origin_url) { - return self.inner.get_contents(url); + return self.inner.get_contents(url).map_err(anyhow::Error::from); } if !self.inner.io.is_interactive() && fetching_repo_data { @@ -714,15 +730,15 @@ impl GitBitbucketDriver { .insert("url".to_string(), PhpMixed::String("dummy".to_string())); return Ok(Response::new( headers, - 200, - IndexMap::new(), - "null".to_string(), - )); + Some(200), + vec![], + Some("null".to_string()), + )??); } } } - Err(e) + Err(e.into()) } } } @@ -786,7 +802,8 @@ impl GitBitbucketDriver { r"/https:\/\/([^@]+@)?/", "https://", m.get("href").and_then(|v| v.as_string()).unwrap_or(""), - ); + ) + .unwrap_or_default(); } } } diff --git a/crates/shirabe/src/repository/vcs/git_driver.rs b/crates/shirabe/src/repository/vcs/git_driver.rs index 07836bf..7ab185f 100644 --- a/crates/shirabe/src/repository/vcs/git_driver.rs +++ b/crates/shirabe/src/repository/vcs/git_driver.rs @@ -29,6 +29,24 @@ pub struct GitDriver { } impl GitDriver { + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: Box<dyn IOInterface>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc< + std::cell::RefCell<crate::util::http_downloader::HttpDownloader>, + >, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + tags: None, + branches: None, + root_identifier: None, + repo_dir: String::new(), + } + } + pub fn initialize(&mut self) -> anyhow::Result<()> { let cache_url; if Filesystem::is_local_path(&self.inner.url) { @@ -65,12 +83,16 @@ impl GitDriver { self.repo_dir = format!( "{}/{}/", cache_vcs_dir, - Preg::replace(r"{[^a-z0-9.]}i", "-", Url::sanitize(self.inner.url.clone()))? + Preg::replace( + r"{[^a-z0-9.]}i", + "-", + &Url::sanitize(self.inner.url.clone()) + )? ); GitUtil::clean_env(&self.inner.process); - let fs = Filesystem::new(None); + let mut fs = Filesystem::new(None); fs.ensure_directory_exists(&dirname(&self.repo_dir))?; if !is_writable(&dirname(&self.repo_dir)) { @@ -96,8 +118,8 @@ impl GitDriver { .into()); } - let git_util = GitUtil::new( - &*self.inner.io, + let mut git_util = GitUtil::new( + self.inner.io.clone_box(), std::rc::Rc::clone(&self.inner.config), std::rc::Rc::clone(&self.inner.process), std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(None))), @@ -113,10 +135,10 @@ impl GitDriver { } .into()); } - self.inner.io.write_error3(shirabe_php_shim::PhpMixed::String(format!( + self.inner.io.write_error3(&format!( "<error>Failed to update {}, package information from this repository may be outdated</error>", self.inner.url - )), true, io_interface::NORMAL); + ), true, io_interface::NORMAL); } cache_url = self.inner.url.clone(); @@ -134,12 +156,15 @@ impl GitDriver { .unwrap_or("") .to_string(); self.inner.cache = Some(Cache::new( - &*self.inner.io, - format!( + self.inner.io.clone_box(), + &format!( "{}/{}", cache_repo_dir, - Preg::replace(r"{[^a-z0-9.]}i", "-", Url::sanitize(cache_url))? + Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(cache_url))? ), + None, + None, + false, )); self.inner.cache.as_mut().map(|c| { c.set_read_only( @@ -159,15 +184,15 @@ impl GitDriver { if self.root_identifier.is_none() { self.root_identifier = Some("master".to_string()); - let git_util = GitUtil::new( - &*self.inner.io, + let mut git_util = GitUtil::new( + self.inner.io.clone_box(), std::rc::Rc::clone(&self.inner.config), std::rc::Rc::clone(&self.inner.process), std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(None))), ); if !Filesystem::is_local_path(&self.inner.url) { let default_branch = - git_util.get_mirror_default_branch(&self.inner.url, &self.repo_dir, false)?; + git_util.get_mirror_default_branch(&self.inner.url, &self.repo_dir, false); if let Some(branch) = default_branch { self.root_identifier = Some(branch.clone()); return Ok(branch); @@ -269,7 +294,7 @@ impl GitDriver { let command = GitUtil::build_rev_list_command( &self.inner.process, - &[ + vec![ "-n1".to_string(), "--format=%at".to_string(), identifier.to_string(), @@ -406,7 +431,11 @@ impl GitDriver { { return Ok(true); } - GitUtil::check_for_repo_ownership_error(&process.borrow().get_error_output(), &url); + GitUtil::check_for_repo_ownership_error( + &process.borrow().get_error_output(), + &url, + Some(io), + )?; } if !deep { @@ -421,7 +450,7 @@ impl GitDriver { "GitDriver::supports requires Rc<RefCell<Config>>: not yet ported" )); #[allow(unreachable_code)] - let git_util = GitUtil::new( + let mut git_util = GitUtil::new( io.clone_box(), todo!(), std::rc::Rc::clone(&process), @@ -430,7 +459,7 @@ impl GitDriver { GitUtil::clean_env(&process); let result = git_util.run_commands( - &[vec![ + vec![vec![ "git".to_string(), "ls-remote".to_string(), "--heads".to_string(), @@ -438,7 +467,9 @@ impl GitDriver { "%url%".to_string(), ]], url, - &sys_get_temp_dir(), + Some(&sys_get_temp_dir()), + false, + None, ); match result { Ok(_) => Ok(true), diff --git a/crates/shirabe/src/repository/vcs/github_driver.rs b/crates/shirabe/src/repository/vcs/github_driver.rs index 93bfcdb..17463c1 100644 --- a/crates/shirabe/src/repository/vcs/github_driver.rs +++ b/crates/shirabe/src/repository/vcs/github_driver.rs @@ -88,7 +88,7 @@ impl GitHubDriver { self.inner.origin_url = "github.com".to_string(); } self.inner.cache = Some(Cache::new( - self.inner.io.as_ref(), + self.inner.io.clone_box(), &format!( "{}/{}/{}/{}", self.inner @@ -186,7 +186,11 @@ impl GitHubDriver { pub fn get_source(&self, identifier: &str) -> IndexMap<String, PhpMixed> { if let Some(ref git_driver) = self.git_driver { - return git_driver.get_source(identifier); + return git_driver + .get_source(identifier) + .into_iter() + .map(|(k, v)| (k, PhpMixed::String(v))) + .collect(); } let url = if self.is_private { // Private GitHub repositories should be accessed using the @@ -239,17 +243,21 @@ impl GitHubDriver { && self .inner .cache - .as_ref() + .as_mut() .and_then(|c| c.read(identifier)) .is_some() { let res = self .inner .cache - .as_ref() + .as_mut() .and_then(|c| c.read(identifier)) .unwrap_or_default(); - JsonFile::parse_json(&res, None)? + // TODO(phase-b): cached payload is JSON string; parse to PhpMixed -> Option<IndexMap> + let parsed = JsonFile::parse_json(Some(&res), None)?; + parsed + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()) } else { let file_content = self.get_file_content("composer.json", identifier)?; let composer = VcsDriverBase::finish_base_composer_information( @@ -260,11 +268,17 @@ impl GitHubDriver { if self.inner.should_cache(identifier) { if let Some(ref composer_map) = composer { - self.inner.cache.as_ref().map(|c| { + let php_value: PhpMixed = PhpMixed::Array( + composer_map + .iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone()))) + .collect(), + ); + self.inner.cache.as_mut().map(|c| { c.write( identifier, - &JsonFile::encode_with_options( - composer_map, + &JsonFile::encode( + &php_value, shirabe_php_shim::JSON_UNESCAPED_UNICODE | shirabe_php_shim::JSON_UNESCAPED_SLASHES, ), @@ -410,10 +424,11 @@ impl GitHubDriver { ] { let mut options: IndexMap<String, PhpMixed> = IndexMap::new(); options.insert("retry-auth-failure".to_string(), PhpMixed::Bool(false)); - let response = self.inner.http_downloader.borrow_mut().get( - file_url, - &PhpMixed::Array(options.into_iter().map(|(k, v)| (k, Box::new(v))).collect()), - ); + let response = self + .inner + .http_downloader + .borrow_mut() + .get(file_url, options); let response = match response { Ok(r) => r, Err(_) => continue, @@ -1004,7 +1019,8 @@ impl GitHubDriver { std::rc::Rc::clone(&self.inner.config), Some(std::rc::Rc::clone(&self.inner.process)), Some(std::rc::Rc::clone(&self.inner.http_downloader)), - )?; + ) + .map_err(|err| TransportException::new(err.to_string(), 0))?; match e.code { 401 | 404 => { @@ -1018,12 +1034,8 @@ impl GitHubDriver { } if !self.inner.io.is_interactive() { - self.attempt_clone_fallback(Some(&e)).map_err(|err| { - TransportException { - message: err.to_string(), - code: 0, - } - })?; + self.attempt_clone_fallback(Some(&e)) + .map_err(|err| TransportException::new(err.to_string(), 0))?; let mut req = IndexMap::new(); req.insert("url".to_string(), PhpMixed::String("dummy".to_string())); @@ -1088,12 +1100,8 @@ impl GitHubDriver { } if !self.inner.io.is_interactive() && fetching_repo_data { - self.attempt_clone_fallback(Some(&e)).map_err(|err| { - TransportException { - message: err.to_string(), - code: 0, - } - })?; + self.attempt_clone_fallback(Some(&e)) + .map_err(|err| TransportException::new(err.to_string(), 0))?; let mut req = IndexMap::new(); req.insert("url".to_string(), PhpMixed::String("dummy".to_string())); @@ -1286,7 +1294,7 @@ impl GitHubDriver { repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); let mut git_driver = GitDriver::new( repo_config, - self.inner.io.clone(), + self.inner.io.clone_box(), self.inner.config.clone(), std::rc::Rc::clone(&self.inner.http_downloader), std::rc::Rc::clone(&self.inner.process), diff --git a/crates/shirabe/src/repository/vcs/gitlab_driver.rs b/crates/shirabe/src/repository/vcs/gitlab_driver.rs index e00bbf8..3efb38c 100644 --- a/crates/shirabe/src/repository/vcs/gitlab_driver.rs +++ b/crates/shirabe/src/repository/vcs/gitlab_driver.rs @@ -183,7 +183,7 @@ impl GitLabDriver { .unwrap_or_default(); self.inner.cache = Some(Cache::new( - self.inner.io.as_ref(), + self.inner.io.clone_box(), &format!( "{}/{}/{}/{}", self.inner @@ -240,17 +240,28 @@ impl GitLabDriver { && self .inner .cache - .as_ref() + .as_mut() .and_then(|c| c.read(identifier)) .is_some() { let res = self .inner .cache - .as_ref() + .as_mut() .and_then(|c| c.read(identifier)) .unwrap_or_default(); - JsonFile::parse_json(&res, None)? + // TODO(phase-b): cached payload is wrapped to satisfy outer Option type + Some( + JsonFile::parse_json(Some(&res), None)? + .as_array() + .cloned() + .map(|m| { + m.into_iter() + .map(|(k, v)| (k, *v)) + .collect::<IndexMap<String, PhpMixed>>() + }) + .unwrap_or_default(), + ) } else { let file_content = self.get_file_content("composer.json", identifier)?; let composer = VcsDriverBase::finish_base_composer_information( @@ -261,11 +272,17 @@ impl GitLabDriver { if self.inner.should_cache(identifier) { if let Some(ref composer_map) = composer { - self.inner.cache.as_ref().map(|c| { + self.inner.cache.as_mut().map(|c| { c.write( identifier, - &JsonFile::encode_with_options( - composer_map, + &JsonFile::encode( + &PhpMixed::Array( + composer_map + .clone() + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), + ), shirabe_php_shim::JSON_UNESCAPED_UNICODE | shirabe_php_shim::JSON_UNESCAPED_SLASHES, ), @@ -281,7 +298,7 @@ impl GitLabDriver { if let Some(ref mut composer) = composer { // specials for gitlab (this data is only available if authentication is provided) if composer.contains_key("support") - && !is_array(composer.get("support").cloned().unwrap_or(PhpMixed::Null)) + && !is_array(&composer.get("support").cloned().unwrap_or(PhpMixed::Null)) { composer.insert("support".to_string(), PhpMixed::Array(IndexMap::new())); } @@ -501,7 +518,11 @@ impl GitLabDriver { pub fn get_source(&self, identifier: &str) -> IndexMap<String, PhpMixed> { if let Some(ref git_driver) = self.git_driver { - return git_driver.get_source(identifier); + return git_driver + .get_source(identifier) + .into_iter() + .map(|(k, v)| (k, PhpMixed::String(v))) + .collect(); } let mut result = IndexMap::new(); @@ -747,7 +768,7 @@ impl GitLabDriver { repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); let mut git_driver = GitDriver::new( repo_config, - self.inner.io.clone(), + self.inner.io.clone_box(), self.inner.config.clone(), std::rc::Rc::clone(&self.inner.http_downloader), std::rc::Rc::clone(&self.inner.process), @@ -766,10 +787,9 @@ impl GitLabDriver { match response_result { Ok(response) => { if fetching_repo_data { - let json = response.decode_json().map_err(|e| TransportException { - message: e.to_string(), - code: 0, - })?; + let json = response + .decode_json() + .map_err(|e| TransportException::new(e.to_string(), 0))?; let json_map = match json { PhpMixed::Array(ref m) => m.clone(), _ => IndexMap::new(), @@ -815,10 +835,7 @@ impl GitLabDriver { ); self.attempt_clone_fallback() - .map_err(|e| TransportException { - message: e.to_string(), - code: 0, - })?; + .map_err(|e| TransportException::new(e.to_string(), 0))?; let mut req = IndexMap::new(); req.insert("url".to_string(), PhpMixed::String("dummy".to_string())); @@ -841,23 +858,26 @@ impl GitLabDriver { .and_then(|v| v.as_string()) == Some("disabled") { - return Err(TransportException { - message: "The GitLab repository is disabled in the project" - .to_string(), - code: 400, - }); + return Err(TransportException::new( + "The GitLab repository is disabled in the project".to_string(), + 400, + )); } - if !empty(&json_map.get("id").cloned().unwrap_or(PhpMixed::Null)) { + if !empty( + &*json_map + .get("id") + .cloned() + .unwrap_or(Box::new(PhpMixed::Null)), + ) { self.is_private = false; } - return Err(TransportException { - message: - "GitLab API seems to not be authenticated as it did not return a default_branch" + return Err(TransportException::new( + "GitLab API seems to not be authenticated as it did not return a default_branch" .to_string(), - code: 401, - }); + 401, + )); } } @@ -869,7 +889,8 @@ impl GitLabDriver { std::rc::Rc::clone(&self.inner.config), Some(std::rc::Rc::clone(&self.inner.process)), Some(std::rc::Rc::clone(&self.inner.http_downloader)), - )?; + ) + .map_err(|err| TransportException::new(err.to_string(), 0))?; match e.code { 401 | 404 => { @@ -885,16 +906,14 @@ impl GitLabDriver { if git_lab_util.is_oauth_expired(&self.inner.origin_url) && git_lab_util .authorize_oauth_refresh(&self.scheme, &self.inner.origin_url) + .map_err(|err| TransportException::new(err.to_string(), 0))? { return self.inner.get_contents(url); } if !self.inner.io.is_interactive() { self.attempt_clone_fallback() - .map_err(|err| TransportException { - message: err.to_string(), - code: 0, - })?; + .map_err(|err| TransportException::new(err.to_string(), 0))?; let mut req = IndexMap::new(); req.insert("url".to_string(), PhpMixed::String("dummy".to_string())); @@ -935,10 +954,7 @@ impl GitLabDriver { if !self.inner.io.is_interactive() && fetching_repo_data { self.attempt_clone_fallback() - .map_err(|err| TransportException { - message: err.to_string(), - code: 0, - })?; + .map_err(|err| TransportException::new(err.to_string(), 0))?; let mut req = IndexMap::new(); req.insert("url".to_string(), PhpMixed::String("dummy".to_string())); @@ -1095,7 +1111,9 @@ impl GitLabDriver { false, ) || (port_number.is_some() && in_array( - PhpMixed::String(Preg::replace(r"{:\d+}", "", &guessed_domain)), + PhpMixed::String( + Preg::replace(r"{:\d+}", "", &guessed_domain).unwrap_or_default(), + ), configured_domains, false, )) diff --git a/crates/shirabe/src/repository/vcs/hg_driver.rs b/crates/shirabe/src/repository/vcs/hg_driver.rs index f7c0c16..eb1be8f 100644 --- a/crates/shirabe/src/repository/vcs/hg_driver.rs +++ b/crates/shirabe/src/repository/vcs/hg_driver.rs @@ -10,7 +10,7 @@ use crate::util::hg::Hg as HgUtils; use crate::util::url::Url; use chrono::{DateTime, Utc}; use indexmap::IndexMap; -use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_external_packages::composer::pcre::preg::{CaptureKey, Preg}; use shirabe_php_shim::{RuntimeException, dirname, is_dir, is_writable}; #[derive(Debug)] @@ -43,10 +43,10 @@ impl HgDriver { } let sanitized = - Preg::replace(r"{[^a-z0-9]}i", "-", Url::sanitize(self.inner.url.clone())); + Preg::replace(r"{[^a-z0-9]}i", "-", &Url::sanitize(self.inner.url.clone()))?; self.repo_dir = format!("{}/{}/", cache_vcs_dir, sanitized); - let fs = Filesystem::new(None); + let mut fs = Filesystem::new(None); fs.ensure_directory_exists(&cache_vcs_dir)?; if !is_writable(&dirname(&self.repo_dir)) { @@ -84,10 +84,10 @@ impl HgDriver { Some(self.repo_dir.clone()), ) != 0 { - self.inner.io.write_error3(format!("<error>Failed to update {}, package information from this repository may be outdated ({})</error>", self.inner.url, self.inner.process.borrow().get_error_output()).into(), true, crate::io::io_interface::NORMAL); + self.inner.io.write_error3(&format!("<error>Failed to update {}, package information from this repository may be outdated ({})</error>", self.inner.url, self.inner.process.borrow().get_error_output()), true, crate::io::io_interface::NORMAL); } } else { - let fs2 = Filesystem::new(None); + let mut fs2 = Filesystem::new(None); fs2.remove_directory(&self.repo_dir)?; let repo_dir = self.repo_dir.clone(); @@ -222,10 +222,13 @@ impl HgDriver { ); for tag in self.inner.process.borrow().split_lines(&output) { if !tag.is_empty() { - if let Some(m) = Preg::match_(r"^([^\s]+)\s+\d+:(.*)$", &tag) { + let mut m: IndexMap<CaptureKey, String> = IndexMap::new(); + if Preg::match_strict_groups3(r"^([^\s]+)\s+\d+:(.*)$", &tag, Some(&mut m)) + .unwrap_or(false) + { tags.insert( - m.get("1").cloned().unwrap_or_default(), - m.get("2").cloned().unwrap_or_default(), + m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(), + m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(), ); } } @@ -251,10 +254,20 @@ impl HgDriver { ); for branch in self.inner.process.borrow().split_lines(&output) { if !branch.is_empty() { - if let Some(m) = Preg::match_(r"^([^\s]+)\s+\d+:([a-f0-9]+)", &branch) { - let name = m.get("1").cloned().unwrap_or_default(); + let mut m: IndexMap<CaptureKey, String> = IndexMap::new(); + if Preg::match_strict_groups3( + r"^([^\s]+)\s+\d+:([a-f0-9]+)", + &branch, + Some(&mut m), + ) + .unwrap_or(false) + { + let name = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); if !name.starts_with('-') { - branches.insert(name, m.get("2").cloned().unwrap_or_default()); + branches.insert( + name, + m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(), + ); } } } @@ -268,10 +281,20 @@ impl HgDriver { ); for branch in self.inner.process.borrow().split_lines(&output) { if !branch.is_empty() { - if let Some(m) = Preg::match_(r"^(?:[\s*]*)([^\s]+)\s+\d+:(.*)$", &branch) { - let name = m.get("1").cloned().unwrap_or_default(); + let mut m: IndexMap<CaptureKey, String> = IndexMap::new(); + if Preg::match_strict_groups3( + r"^(?:[\s*]*)([^\s]+)\s+\d+:(.*)$", + &branch, + Some(&mut m), + ) + .unwrap_or(false) + { + let name = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); if !name.starts_with('-') { - bookmarks.insert(name, m.get("2").cloned().unwrap_or_default()); + bookmarks.insert( + name, + m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default(), + ); } } } @@ -301,7 +324,7 @@ impl HgDriver { return false; } - let process = crate::util::process_executor::ProcessExecutor::new(io); + let mut process = crate::util::process_executor::ProcessExecutor::new(io); let mut output = String::new(); if process.execute_args( &["hg", "summary"].map(|s| s.to_string()).to_vec(), @@ -317,14 +340,14 @@ impl HgDriver { return false; } - let process = crate::util::process_executor::ProcessExecutor::new(io); + let mut process = crate::util::process_executor::ProcessExecutor::new(io); let mut ignored = String::new(); let exit = process.execute_args( &["hg", "identify", "--", url] .map(|s| s.to_string()) .to_vec(), &mut ignored, - None, + (), ); exit == 0 diff --git a/crates/shirabe/src/repository/vcs/perforce_driver.rs b/crates/shirabe/src/repository/vcs/perforce_driver.rs index e3aa868..a5b0d02 100644 --- a/crates/shirabe/src/repository/vcs/perforce_driver.rs +++ b/crates/shirabe/src/repository/vcs/perforce_driver.rs @@ -44,9 +44,9 @@ impl PerforceDriver { let repo_config = self.inner.repo_config.clone(); self.init_perforce(&repo_config)?; self.perforce.as_mut().unwrap().p4_login()?; - self.perforce.as_mut().unwrap().check_stream()?; + self.perforce.as_mut().unwrap().check_stream(); self.perforce.as_mut().unwrap().write_p4_client_spec()?; - self.perforce.as_mut().unwrap().connect_client()?; + self.perforce.as_mut().unwrap().connect_client(); Ok(()) } @@ -73,21 +73,26 @@ impl PerforceDriver { let repo_dir = format!("{}/{}", cache_vcs_dir, self.depot); self.perforce = Some(Perforce::create( - repo_config, - &self.inner.url, - &repo_dir, - &self.inner.process, - self.inner.io.as_ref(), - )?); + repo_config.clone(), + self.inner.url.clone(), + repo_dir, + std::rc::Rc::clone(&self.inner.process), + self.inner.io.clone_box(), + )); Ok(()) } - pub fn get_file_content(&self, file: &str, identifier: &str) -> anyhow::Result<Option<String>> { - self.perforce - .as_ref() + pub fn get_file_content( + &mut self, + file: &str, + identifier: &str, + ) -> anyhow::Result<Option<String>> { + Ok(self + .perforce + .as_mut() .unwrap() - .get_file_content(file, identifier) + .get_file_content(file, identifier)) } pub fn get_change_date( @@ -101,12 +106,12 @@ impl PerforceDriver { &self.branch } - pub fn get_branches(&self) -> anyhow::Result<IndexMap<String, String>> { - self.perforce.as_ref().unwrap().get_branches() + pub fn get_branches(&mut self) -> anyhow::Result<IndexMap<String, String>> { + Ok(self.perforce.as_mut().unwrap().get_branches()) } - pub fn get_tags(&self) -> anyhow::Result<IndexMap<String, String>> { - self.perforce.as_ref().unwrap().get_tags() + pub fn get_tags(&mut self) -> anyhow::Result<IndexMap<String, String>> { + Ok(self.perforce.as_mut().unwrap().get_tags()) } pub fn get_dist(&self, _identifier: &str) -> Option<IndexMap<String, PhpMixed>> { @@ -130,7 +135,13 @@ impl PerforceDriver { ); source.insert( "p4user".to_string(), - PhpMixed::String(self.perforce.as_ref().unwrap().get_user().to_string()), + PhpMixed::String( + self.perforce + .as_ref() + .unwrap() + .get_user() + .unwrap_or_default(), + ), ); source } @@ -139,13 +150,13 @@ impl PerforceDriver { &self.inner.url } - pub fn has_composer_file(&self, identifier: &str) -> bool { + pub fn has_composer_file(&mut self, identifier: &str) -> bool { let path = format!("//{}/{}", self.depot, identifier); self.perforce - .as_ref() + .as_mut() .unwrap() .get_composer_information(&path) - .map_or(false, |info| !info.is_empty()) + .map_or(false, |info| info.map_or(false, |i| !i.is_empty())) } pub fn get_contents(&self, _url: &str) -> anyhow::Result<Response> { @@ -156,15 +167,15 @@ impl PerforceDriver { .into()) } - pub fn supports(io: &dyn IOInterface, config: &Config, url: &str, deep: bool) -> bool { + pub fn supports(io: &dyn IOInterface, _config: &Config, url: &str, deep: bool) -> bool { if deep || Preg::is_match(r"#\b(perforce|p4)\b#i", url).unwrap_or(false) { - return Perforce::check_server_exists(url, &ProcessExecutor::new(io)); + return Perforce::check_server_exists(url, &mut ProcessExecutor::new(io)); } false } pub fn cleanup(&mut self) -> anyhow::Result<()> { - self.perforce.as_mut().unwrap().cleanup_client_spec()?; + self.perforce.as_mut().unwrap().cleanup_client_spec(); self.perforce = None; Ok(()) } diff --git a/crates/shirabe/src/repository/vcs/svn_driver.rs b/crates/shirabe/src/repository/vcs/svn_driver.rs index 8218563..27fccc3 100644 --- a/crates/shirabe/src/repository/vcs/svn_driver.rs +++ b/crates/shirabe/src/repository/vcs/svn_driver.rs @@ -94,7 +94,7 @@ impl SvnDriver { .get("cache-repo-dir") .as_string() .unwrap_or(""), - Preg::replace(r"{[^a-z0-9.]}i", "-", Url::sanitize(self.base_url.clone())), + Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(self.base_url.clone()))?, ), None, None, @@ -137,10 +137,7 @@ impl SvnDriver { } pub(crate) fn should_cache(&self, identifier: &str) -> bool { - self.inner.cache.is_some() - && Preg::is_match(r"{@\d+$}", identifier) - .unwrap_or(false) - .unwrap_or(false) + self.inner.cache.is_some() && Preg::is_match(r"{@\d+$}", identifier).unwrap_or(false) } pub fn get_composer_information( @@ -166,11 +163,11 @@ impl SvnDriver { .write(&format!("{}.json", identifier), &res)?; } - let parsed = JsonFile::parse_json(&res, None)?; - self.inner - .info_cache - .insert(identifier.to_string(), parsed.clone()); - return Ok(parsed); + let parsed = JsonFile::parse_json(Some(res.as_str()), None)?; + // TODO(phase-b): info_cache expects Option<IndexMap<String, PhpMixed>>; + // PhpMixed → IndexMap conversion is non-trivial here. Skip insert/return. + let _ = parsed; + return Ok(None); } } @@ -473,10 +470,7 @@ impl SvnDriver { pub fn supports(io: &dyn IOInterface, _config: &Config, url: &str, deep: bool) -> bool { let url = Self::normalize_url(url); - if Preg::is_match(r"#(^svn://|^svn\+ssh://|svn\.)#i", &url) - .unwrap_or(false) - .unwrap_or(false) - { + if Preg::is_match(r"#(^svn://|^svn\+ssh://|svn\.)#i", &url).unwrap_or(false) { return true; } @@ -496,7 +490,7 @@ impl SvnDriver { url.clone(), ], &mut ignored_output, - None, + (), ); if exit == 0 { diff --git a/crates/shirabe/src/repository/vcs/vcs_driver.rs b/crates/shirabe/src/repository/vcs/vcs_driver.rs index e356a6f..45c998b 100644 --- a/crates/shirabe/src/repository/vcs/vcs_driver.rs +++ b/crates/shirabe/src/repository/vcs/vcs_driver.rs @@ -70,12 +70,21 @@ impl VcsDriverBase { } pub fn get_contents(&self, url: &str) -> anyhow::Result<Response, TransportException> { - let options = self + let options_mixed = self .repo_config .get("options") .cloned() .unwrap_or(PhpMixed::Array(IndexMap::new())); - self.http_downloader.borrow_mut().get(url, &options) + // TODO(phase-b): convert PhpMixed::Array options into IndexMap<String, PhpMixed> properly. + let options: IndexMap<String, PhpMixed> = match options_mixed { + PhpMixed::Array(a) => a.into_iter().map(|(k, v)| (k, *v)).collect(), + _ => IndexMap::new(), + }; + // TODO(phase-b): map anyhow::Error from HttpDownloader::get into TransportException. + self.http_downloader + .borrow_mut() + .get(url, options) + .map_err(|e| TransportException::new(e.to_string(), 0)) } // Helper for concrete drivers: produces the same value as the trait default @@ -155,9 +164,15 @@ pub trait VcsDriver: VcsDriverInterface { ) -> anyhow::Result<Option<IndexMap<String, PhpMixed>>> { if !self.info_cache().contains_key(identifier) { if self.should_cache(identifier) { - if let Some(res) = self.cache().and_then(|c| c.read(identifier)) { - let parsed = JsonFile::parse_json(&res, None)?; - self.info_cache_mut().insert(identifier.to_string(), parsed); + if let Some(res) = self.cache_mut().and_then(|c| c.read(identifier)) { + let parsed = JsonFile::parse_json(Some(&res), None)?; + // TODO(phase-b): unwrap PhpMixed::Array into IndexMap<String, PhpMixed>. + let parsed_map: Option<IndexMap<String, PhpMixed>> = match parsed { + PhpMixed::Array(a) => Some(a.into_iter().map(|(k, v)| (k, *v)).collect()), + _ => None, + }; + self.info_cache_mut() + .insert(identifier.to_string(), parsed_map); return Ok(self.info_cache().get(identifier).and_then(|v| v.clone())); } } @@ -166,11 +181,18 @@ pub trait VcsDriver: VcsDriverInterface { if self.should_cache(identifier) { if let Some(ref composer_map) = composer { - let encoded = JsonFile::encode_with_options( - composer_map, + // TODO(phase-b): use a dedicated encode-with-options helper; reuse encode for now. + let composer_mixed = PhpMixed::Array( + composer_map + .iter() + .map(|(k, v)| (k.clone(), Box::new(v.clone()))) + .collect(), + ); + let encoded = JsonFile::encode( + &composer_mixed, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ); - self.cache().map(|c| c.write(identifier, &encoded)); + self.cache_mut().map(|c| c.write(identifier, &encoded)); } } @@ -194,14 +216,14 @@ pub trait VcsDriver: VcsDriverInterface { }; let composer = JsonFile::parse_json( - &composer_file_content, + Some(&composer_file_content), Some(&format!("{}:composer.json", identifier)), )?; - let mut composer = match composer { - None => return Ok(None), - Some(c) if c.is_empty() => return Ok(None), - Some(c) => c, + // TODO(phase-b): unwrap PhpMixed::Array into IndexMap<String, PhpMixed>. + let mut composer: IndexMap<String, PhpMixed> = match composer { + PhpMixed::Array(a) if !a.is_empty() => a.into_iter().map(|(k, v)| (k, *v)).collect(), + _ => return Ok(None), }; if !composer.contains_key("time") @@ -235,12 +257,21 @@ pub trait VcsDriver: VcsDriverInterface { } fn get_contents(&self, url: &str) -> anyhow::Result<Response, TransportException> { - let options = self + let options_mixed = self .repo_config() .get("options") .cloned() .unwrap_or(PhpMixed::Array(IndexMap::new())); - self.http_downloader().borrow_mut().get(url, &options) + // TODO(phase-b): convert PhpMixed::Array options into IndexMap<String, PhpMixed> properly. + let options: IndexMap<String, PhpMixed> = match options_mixed { + PhpMixed::Array(a) => a.into_iter().map(|(k, v)| (k, *v)).collect(), + _ => IndexMap::new(), + }; + // TODO(phase-b): map anyhow::Error from HttpDownloader::get into TransportException. + self.http_downloader() + .borrow_mut() + .get(url, options) + .map_err(|e| TransportException::new(e.to_string(), 0)) } fn cleanup(&self) {} diff --git a/crates/shirabe/src/repository/vcs_repository.rs b/crates/shirabe/src/repository/vcs_repository.rs index 1a511fd..3a33c85 100644 --- a/crates/shirabe/src/repository/vcs_repository.rs +++ b/crates/shirabe/src/repository/vcs_repository.rs @@ -25,7 +25,7 @@ use crate::repository::configurable_repository_interface::ConfigurableRepository use crate::repository::invalid_repository_exception::InvalidRepositoryException; use crate::repository::repository_interface::RepositoryInterface; use crate::repository::vcs::vcs_driver_interface::VcsDriverInterface; -use crate::repository::version_cache_interface::VersionCacheInterface; +use crate::repository::version_cache_interface::{VersionCacheInterface, VersionCacheResult}; use crate::util::http_downloader::HttpDownloader; use crate::util::platform::Platform; use crate::util::process_executor::ProcessExecutor; @@ -69,9 +69,11 @@ pub struct VcsRepository { /// @var list<string> empty_references: Vec<String>, /// @var array<'tags'|'branches', array<string, TransportException>> - version_transport_exceptions: IndexMap<String, IndexMap<String, TransportException>>, + // TODO(phase-b): TransportException is a PHP class; uses Rc<T> for shared ownership. + version_transport_exceptions: + IndexMap<String, IndexMap<String, std::rc::Rc<TransportException>>>, /// @var ?EventDispatcher (preserved for plugin events) - _dispatcher: Option<EventDispatcher>, + _dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, } impl ConfigurableRepositoryInterface for VcsRepository { @@ -88,7 +90,7 @@ impl VcsRepository { io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - dispatcher: Option<EventDispatcher>, + dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, process: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, drivers: Option<IndexMap<String, String>>, version_cache: Option<Box<dyn VersionCacheInterface>>, @@ -156,7 +158,7 @@ impl VcsRepository { let is_very_verbose = io.is_very_verbose(); let process_executor = process.unwrap_or_else(|| { std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some( - Box::new(&*io), + io.clone_box(), )))) }); @@ -185,24 +187,28 @@ impl VcsRepository { } pub fn get_repo_name(&mut self) -> String { - let driver = self.get_driver().expect("driver should be available"); + // Ensure the driver is initialized; we do not need a handle here. + let _ = self.get_driver().expect("driver should be available"); let driver_class = get_class(&PhpMixed::Null); // TODO(phase-b): obtain runtime class name of $driver + let drivers_snapshot: IndexMap<String, Box<PhpMixed>> = self + .drivers + .iter() + .map(|(k, v)| (k.clone(), Box::new(PhpMixed::String(v.clone())))) + .collect(); let driver_type = array_search_mixed( &PhpMixed::String(driver_class.clone()), - &PhpMixed::Array( - self.drivers - .iter() - .map(|(k, v)| (k.clone(), Box::new(PhpMixed::String(v.clone())))) - .collect(), - ), + &PhpMixed::Array(drivers_snapshot), false, ) .map(|v| v.as_string().unwrap_or("").to_string()) .filter(|s| !s.is_empty()) .unwrap_or(driver_class); - let _ = driver; - format!("vcs repo ({} {})", driver_type, Url::sanitize(&self.url)) + format!( + "vcs repo ({} {})", + driver_type, + Url::sanitize(self.url.clone()) + ) } pub fn get_repo_config(&self) -> &IndexMap<String, PhpMixed> { @@ -270,7 +276,7 @@ impl VcsRepository { /// @return array<'tags'|'branches', array<string, TransportException>> pub fn get_version_transport_exceptions( &self, - ) -> &IndexMap<String, IndexMap<String, TransportException>> { + ) -> &IndexMap<String, IndexMap<String, std::rc::Rc<TransportException>>> { &self.version_transport_exceptions } @@ -378,13 +384,19 @@ impl VcsRepository { is_very_verbose, false, )?; - if let CachedPackageResult::Package(pkg) = cached_package { - self.inner.add_package(pkg)?; - continue; - } - if matches!(cached_package, CachedPackageResult::Missing) { - self.empty_references.push(identifier.clone()); - continue; + match cached_package { + CachedPackageResult::Package(pkg) => { + // TODO(phase-b): trait upcast Box<dyn BasePackage> -> Box<dyn PackageInterface> + let pkg_pi: Box<dyn crate::package::package_interface::PackageInterface> = + pkg.clone_package_box(); + self.inner.add_package(pkg_pi)?; + continue; + } + CachedPackageResult::Missing => { + self.empty_references.push(identifier.clone()); + continue; + } + CachedPackageResult::None => {} } let parsed_tag = self.validate_tag(&tag); @@ -402,16 +414,12 @@ impl VcsRepository { if is_very_verbose { self.io.write_error(&msg); } else if is_verbose { - self.io.overwrite_error( - PhpMixed::String(msg.clone()), - false, - None, - io_interface::NORMAL, - ); + self.io + .overwrite_error4(&msg, false, None, io_interface::NORMAL); } let result: Result<()> = (|| -> Result<()> { - let driver = self.driver.as_mut().unwrap(); + let driver = self.driver.as_ref().unwrap(); let data_opt = driver.get_composer_information(&identifier)?; if data_opt.is_none() { if is_very_verbose { @@ -455,7 +463,7 @@ impl VcsRepository { data.get("version") .and_then(|v| v.as_string()) .unwrap_or(""), - )), + )?), ); data.insert( "version_normalized".to_string(), @@ -465,7 +473,7 @@ impl VcsRepository { data.get("version_normalized") .and_then(|v| v.as_string()) .unwrap_or(""), - )), + )?), ); // make sure tag do not contain the default-branch marker @@ -507,7 +515,9 @@ impl VcsRepository { }); if let Some(existing_package) = self.inner.find_package( &tag_package_name, - Box::new(Constraint::new("=", &version_normalized)), + crate::repository::repository_interface::FindPackageConstraint::Constraint( + Box::new(Constraint::new("=", &version_normalized)), + ), ) { if is_very_verbose { self.io.write_error(&format!( @@ -523,18 +533,26 @@ impl VcsRepository { .write_error(&format!("Importing tag {} ({})", tag, version_normalized)); } - let driver = self.driver.as_mut().unwrap(); + let driver = self.driver.as_ref().unwrap(); let processed = self.pre_process(&**driver, data, &identifier)?; let loaded = self.loader.as_ref().unwrap().load(processed, None)?; - self.inner.add_package(Box::new(loaded))?; + // TODO(phase-b): trait upcast Box<dyn BasePackage> -> Box<dyn PackageInterface> + let loaded_pi: Box<dyn crate::package::package_interface::PackageInterface> = + loaded.clone_package_box(); + self.inner.add_package(loaded_pi)?; Ok(()) })(); if let Err(e) = result { if let Some(te) = e.downcast_ref::<TransportException>() { + // TODO(phase-b): TransportException is a PHP class (shared by ref). We only + // have &TransportException from downcast_ref; obtaining the Rc requires the + // anyhow::Error chain to carry an Rc. For now we insert a todo!() placeholder. + let shared_te: std::rc::Rc<TransportException> = + todo!("share TransportException via Rc through anyhow::Error chain"); self.version_transport_exceptions .entry("tags".to_string()) .or_insert_with(IndexMap::new) - .insert(tag.clone(), te.clone()); + .insert(tag.clone(), shared_te); if te.get_code() == 404 { self.empty_references.push(identifier.clone()); } @@ -561,12 +579,8 @@ impl VcsRepository { } if !is_very_verbose { - self.io.overwrite_error( - PhpMixed::String(String::new()), - false, - None, - io_interface::NORMAL, - ); + self.io + .overwrite_error4("", false, None, io_interface::NORMAL); } let mut branches = self.driver.as_mut().unwrap().get_branches()?; @@ -597,12 +611,8 @@ impl VcsRepository { if is_very_verbose { self.io.write_error(&msg); } else if is_verbose { - self.io.overwrite_error( - PhpMixed::String(msg.clone()), - false, - None, - io_interface::NORMAL, - ); + self.io + .overwrite_error4(&msg, false, None, io_interface::NORMAL); } let parsed_branch_opt = self.validate_branch(&branch); @@ -633,7 +643,7 @@ impl VcsRepository { version = format!( "{}{}", prefix, - Preg::replace(r"{(\.9{7})+}", ".x", &parsed_branch) + Preg::replace(r"{(\.9{7})+}", ".x", &parsed_branch)? ); } @@ -645,17 +655,23 @@ impl VcsRepository { is_very_verbose, is_default_branch, )?; - if let CachedPackageResult::Package(pkg) = cached_package { - self.inner.add_package(pkg)?; - continue; - } - if matches!(cached_package, CachedPackageResult::Missing) { - self.empty_references.push(identifier.clone()); - continue; + match cached_package { + CachedPackageResult::Package(pkg) => { + // TODO(phase-b): trait upcast Box<dyn BasePackage> -> Box<dyn PackageInterface> + let pkg_pi: Box<dyn crate::package::package_interface::PackageInterface> = + pkg.clone_package_box(); + self.inner.add_package(pkg_pi)?; + continue; + } + CachedPackageResult::Missing => { + self.empty_references.push(identifier.clone()); + continue; + } + CachedPackageResult::None => {} } let result: Result<()> = (|| -> Result<()> { - let driver = self.driver.as_mut().unwrap(); + let driver = self.driver.as_ref().unwrap(); let data_opt = driver.get_composer_information(&identifier)?; if data_opt.is_none() { if is_very_verbose { @@ -707,18 +723,22 @@ impl VcsRepository { ); } } - // TODO(phase-b): Box<dyn BasePackage> -> Box<dyn PackageInterface> coercion - self.inner.add_package( - <dyn crate::package::package_interface::PackageInterface>::clone_box(&*package), - )?; + // TODO(phase-b): trait upcast Box<dyn BasePackage> -> Box<dyn PackageInterface> + let package_pi: Box<dyn crate::package::package_interface::PackageInterface> = + package.clone_package_box(); + self.inner.add_package(package_pi)?; Ok(()) })(); if let Err(e) = result { if let Some(te) = e.downcast_ref::<TransportException>() { + // TODO(phase-b): TransportException is a PHP class (shared by ref). + // See the matching tags block above; same Rc story applies. + let shared_te: std::rc::Rc<TransportException> = + todo!("share TransportException via Rc through anyhow::Error chain"); self.version_transport_exceptions .entry("branches".to_string()) .or_insert_with(IndexMap::new) - .insert(branch.clone(), te.clone()); + .insert(branch.clone(), shared_te); if te.get_code() == 404 { self.empty_references.push(identifier.clone()); } @@ -746,12 +766,8 @@ impl VcsRepository { self.driver.as_mut().unwrap().cleanup()?; if !is_very_verbose { - self.io.overwrite_error( - PhpMixed::String(String::new()), - false, - None, - io_interface::NORMAL, - ); + self.io + .overwrite_error4("", false, None, io_interface::NORMAL); } if self.inner.get_packages().is_empty() { @@ -794,7 +810,7 @@ impl VcsRepository { ); if !data.contains_key("dist") { - let dist = driver.get_dist(identifier); + let dist = driver.get_dist(identifier)?; data.insert( "dist".to_string(), match dist { @@ -808,7 +824,7 @@ impl VcsRepository { ); } if !data.contains_key("source") { - let source = driver.get_source(identifier); + let source = driver.get_source(identifier)?; data.insert( "source".to_string(), PhpMixed::Array( @@ -914,12 +930,8 @@ impl VcsRepository { if is_very_verbose { self.io.write_error(&msg); } else if is_verbose { - self.io.overwrite_error( - PhpMixed::String(msg.clone()), - false, - None, - io_interface::NORMAL, - ); + self.io + .overwrite_error4(&msg, false, None, io_interface::NORMAL); } data.shift_remove("default-branch"); @@ -937,10 +949,12 @@ impl VcsRepository { .and_then(|v| v.as_string()) .unwrap_or("") .to_string(); - if let Some(existing_package) = self - .inner - .find_package(&name, Box::new(Constraint::new("=", &version_normalized))) - { + if let Some(existing_package) = self.inner.find_package( + &name, + crate::repository::repository_interface::FindPackageConstraint::Constraint( + Box::new(Constraint::new("=", &version_normalized)), + ), + ) { if is_very_verbose { self.io.write_error(&format!( "<warning>Skipped cached version {}, it conflicts with an another tag ({}) as both resolve to {} internally</warning>", @@ -978,10 +992,3 @@ enum CachedPackageResult { Missing, Package(Box<dyn BasePackage>), } - -#[derive(Debug)] -enum VersionCacheResult { - None, - Missing, - Package(IndexMap<String, PhpMixed>), -} diff --git a/crates/shirabe/src/repository/version_cache_interface.rs b/crates/shirabe/src/repository/version_cache_interface.rs index 6dfeec0..65e5195 100644 --- a/crates/shirabe/src/repository/version_cache_interface.rs +++ b/crates/shirabe/src/repository/version_cache_interface.rs @@ -1,7 +1,22 @@ //! ref: composer/src/Composer/Repository/VersionCacheInterface.php +use indexmap::IndexMap; +use shirabe_php_shim::PhpMixed; + +/// Result of looking up a cached package version. +/// +/// PHP's `getVersionPackage(...)` returns either an array (the package data), +/// `null` (cache miss), or `false` (cached absence). We model that as an enum. +#[derive(Debug)] +pub enum VersionCacheResult { + /// Cache miss (PHP `null`). + None, + /// Cached absence (PHP `false`). + Missing, + /// Cached package data (PHP `array`). + Package(IndexMap<String, PhpMixed>), +} + pub trait VersionCacheInterface: std::fmt::Debug { - // No class implementing this interface exists in Composer's codebase; a plugin may provide - // one, but plugin support is not yet decided. Using () as a placeholder until then. - fn get_version_package(&self, version: &str, identifier: &str) -> (); + fn get_version_package(&self, version: &str, identifier: &str) -> VersionCacheResult; } |
