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/util | |
| 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/util')
21 files changed, 1182 insertions, 737 deletions
diff --git a/crates/shirabe/src/util/auth_helper.rs b/crates/shirabe/src/util/auth_helper.rs index 43e13ea..1bbe7dc 100644 --- a/crates/shirabe/src/util/auth_helper.rs +++ b/crates/shirabe/src/util/auth_helper.rs @@ -54,8 +54,8 @@ impl AuthHelper { pub fn store_auth(&self, origin: &str, store_auth: StoreAuth) -> Result<()> { // TODO(phase-b): config.get_auth_config_source() and ConfigSource methods are stubs let mut store: Option<()> = None; - let config = self.config.borrow(); - let config_source = config.get_auth_config_source(); + let mut config = self.config.borrow_mut(); + let config_source = config.get_auth_config_source_mut(); if matches!(store_auth, StoreAuth::Bool(true)) { store = Some(()); } else if matches!(store_auth, StoreAuth::Prompt) { diff --git a/crates/shirabe/src/util/bitbucket.rs b/crates/shirabe/src/util/bitbucket.rs index cd4a9fa..ef1d4bf 100644 --- a/crates/shirabe/src/util/bitbucket.rs +++ b/crates/shirabe/src/util/bitbucket.rs @@ -5,7 +5,6 @@ use indexmap::IndexMap; use shirabe_php_shim::{LogicException, PhpMixed, time}; use crate::config::Config; -use crate::config::config_source_interface::ConfigSourceInterface; use crate::downloader::transport_exception::TransportException; use crate::factory::Factory; use crate::io::io_interface::IOInterface; @@ -83,7 +82,7 @@ impl Bitbucket { .execute( PhpMixed::from(vec!["git", "config", "bitbucket.accesstoken"]), Some(&mut output), - None, + (), ) .unwrap_or(1) == 0 @@ -212,17 +211,21 @@ impl Bitbucket { self.io.write_error3(msg, true, io_interface::NORMAL); } - let config_ref = self.config.borrow(); - let local_auth_config = config_ref.get_local_auth_config_source(); + let local_auth_config_name: Option<String> = self + .config + .borrow() + .get_local_auth_config_source() + .map(|c| c.get_name()); + let has_local_auth_config = local_auth_config_name.is_some(); + let auth_config_source_name = self.config.borrow().get_auth_config_source().get_name(); let url = "https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/"; self.io .write_error3("Follow the instructions here:", true, io_interface::NORMAL); self.io.write_error3(url, true, io_interface::NORMAL); - let auth_config_source_name = config_ref.get_auth_config_source().get_name(); - let local_name_prefix = local_auth_config + let local_name_prefix = local_auth_config_name .as_ref() - .map(|c| format!("{} OR ", c.get_name())) + .map(|name| format!("{} OR ", name)) .unwrap_or_default(); self.io.write_error3( &format!( @@ -239,7 +242,7 @@ impl Bitbucket { ); let mut store_in_local_auth_config = false; - if local_auth_config.is_some() { + if has_local_auth_config { store_in_local_auth_config = self.io.ask_confirmation( "A local auth config source was found, do you want to store the token there?" .to_string(), @@ -299,34 +302,14 @@ impl Bitbucket { return Ok(false); } - let use_local = store_in_local_auth_config - && self - .config - .borrow() - .get_local_auth_config_source() - .is_some(); - if use_local { - let mut auth_config_source = - self.config.borrow().get_local_auth_config_source().unwrap(); - self.store_in_auth_config( - &mut *auth_config_source, - origin_url, - &consumer_key, - &consumer_secret, - )?; - } else { - let mut auth_config_source = self.config.borrow().get_auth_config_source(); - self.store_in_auth_config( - &mut *auth_config_source, - origin_url, - &consumer_key, - &consumer_secret, - )?; - } + // TODO(phase-b): PHP $authConfigSource parameter is unused inside storeInAuthConfig + // (upstream Composer bug); the dispatch on local vs. global is dropped here too. + let _ = store_in_local_auth_config; + self.store_in_auth_config(origin_url, &consumer_key, &consumer_secret)?; self.config - .borrow() - .get_auth_config_source() + .borrow_mut() + .get_auth_config_source_mut() .remove_config_setting(&format!("http-basic.{}", origin_url))?; self.io.write_error3( @@ -364,29 +347,9 @@ impl Bitbucket { return Ok(String::new()); } - let use_local = self - .config - .borrow() - .get_local_auth_config_source() - .is_some(); - if use_local { - let mut auth_config_source = - self.config.borrow().get_local_auth_config_source().unwrap(); - self.store_in_auth_config( - &mut *auth_config_source, - origin_url, - consumer_key, - consumer_secret, - )?; - } else { - let mut auth_config_source = self.config.borrow().get_auth_config_source(); - self.store_in_auth_config( - &mut *auth_config_source, - origin_url, - consumer_key, - consumer_secret, - )?; - } + // TODO(phase-b): PHP $authConfigSource parameter is unused inside storeInAuthConfig + // (upstream Composer bug); the dispatch on local vs. global is dropped here too. + self.store_in_auth_config(origin_url, consumer_key, consumer_secret)?; let access_token = self .token @@ -405,16 +368,16 @@ impl Bitbucket { } } + // TODO(phase-b): PHP $authConfigSource parameter dropped — unused in upstream Composer too. fn store_in_auth_config( &mut self, - auth_config_source: &mut dyn ConfigSourceInterface, origin_url: &str, consumer_key: &str, consumer_secret: &str, ) -> anyhow::Result<()> { self.config - .borrow() - .get_config_source() + .borrow_mut() + .get_config_source_mut() .remove_config_setting(&format!("bitbucket-oauth.{}", origin_url))?; let token = self.token.as_ref().ok_or_else(|| LogicException { @@ -460,8 +423,8 @@ impl Bitbucket { ); self.config - .borrow() - .get_auth_config_source() + .borrow_mut() + .get_auth_config_source_mut() .add_config_setting( &format!("bitbucket-oauth.{}", origin_url), PhpMixed::Array(consumer), diff --git a/crates/shirabe/src/util/config_validator.rs b/crates/shirabe/src/util/config_validator.rs index 66fcadb..4e99da0 100644 --- a/crates/shirabe/src/util/config_validator.rs +++ b/crates/shirabe/src/util/config_validator.rs @@ -40,13 +40,16 @@ impl ConfigValidator { let mut manifest: Option<IndexMap<String, PhpMixed>> = None; // TODO(phase-b): io type mismatch (&dyn IOInterface vs Box<dyn IOInterface>) - let json = + let mut json = JsonFile::new(file.to_string(), None, None).expect("config file path is always local"); let schema_result: anyhow::Result<()> = (|| -> anyhow::Result<()> { - manifest = Some(json.read()?); - json.validate_schema(Some(JsonFile::LAX_SCHEMA))?; + manifest = Some(match json.read()? { + PhpMixed::Array(m) => m.into_iter().map(|(k, v)| (k, *v)).collect(), + _ => IndexMap::new(), + }); + json.validate_schema(JsonFile::LAX_SCHEMA, None)?; lax_valid = true; - json.validate_schema(None)?; + json.validate_schema(JsonFile::STRICT_SCHEMA, None)?; Ok(()) })(); @@ -126,7 +129,12 @@ impl ConfigValidator { for license in &licenses { let spdx_license = license_validator.get_license_by_identifier(license); if let Some(spdx_license) = spdx_license { - if spdx_license[3] { + // PHP: $spdxLicense[3] — fourth element is the deprecated flag. + let is_deprecated = match &spdx_license { + PhpMixed::List(l) => l.get(3).and_then(|v| v.as_bool()).unwrap_or(false), + _ => false, + }; + if is_deprecated { if Preg::is_match(r"{^[AL]?GPL-[123](\.[01])?\+$}i", license) .unwrap_or(false) { @@ -163,7 +171,8 @@ impl ConfigValidator { r"{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}", r"\1\3-\2\4", name, - ); + ) + .unwrap_or_else(|_| name.clone()); let suggest_name = suggest_name.to_lowercase(); publish_errors.push(format!( @@ -289,8 +298,8 @@ impl ConfigValidator { } } - let loader = ValidatingArrayLoader::new( - ArrayLoader::new(), + let mut loader = ValidatingArrayLoader::new( + Box::new(ArrayLoader::new(None, true)), true, None, array_loader_validation_flags, @@ -305,7 +314,11 @@ impl ConfigValidator { PhpMixed::String("dummy/dummy".to_string()), ); } - match loader.load(manifest_for_load) { + let manifest_boxed: IndexMap<String, Box<PhpMixed>> = manifest_for_load + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(); + match loader.load(manifest_boxed, "Composer\\Package\\CompletePackage") { Ok(_) => {} Err(e) => { if let Some(invalid_e) = e.downcast_ref::<InvalidPackageException>() { diff --git a/crates/shirabe/src/util/filesystem.rs b/crates/shirabe/src/util/filesystem.rs index 2ab08da..e51c1e2 100644 --- a/crates/shirabe/src/util/filesystem.rs +++ b/crates/shirabe/src/util/filesystem.rs @@ -6,13 +6,13 @@ use shirabe_external_packages::symfony::component::filesystem::exception::io_exc use shirabe_external_packages::symfony::component::finder::finder::Finder; use shirabe_php_shim::{ DIRECTORY_SEPARATOR, ErrorException, InvalidArgumentException, LogicException, PhpMixed, - RuntimeException, UnexpectedValueException, array_pop, basename, chdir, clearstatcache, copy, - count, dirname, end, error_get_last, explode, fclose, feof, file_exists, file_get_contents, - file_put_contents, fileatime, filemtime, filesize, fopen, fread, function_exists, fwrite, - implode, is_array, is_dir, is_file, is_link, is_readable, lstat, mkdir, react_promise_resolve, - rename, rmdir, rtrim, sprintf, str_contains, str_repeat, str_replace, str_starts_with, strlen, - strpos, strtolower, strtoupper, strtr, substr, substr_count, symlink, touch, unlink, usleep, - var_export, + RuntimeException, UnexpectedValueException, array_pop, basename, chdir, clearstatcache, + clearstatcache2, copy, count, dirname, end, error_get_last, explode, fclose, feof, file_exists, + file_get_contents, file_put_contents, fileatime, filemtime, filesize, fopen, fread, + function_exists, fwrite, implode, is_array, is_dir, is_file, is_link, is_readable, lstat, + mkdir, react_promise_resolve, rename, rmdir, rtrim, sprintf, str_contains, str_repeat, + str_replace, str_starts_with, strlen, strpos, strtolower, strtoupper, strtr, substr, + substr_count, symlink, touch, unlink, usleep, var_export, }; use crate::util::platform::Platform; @@ -45,13 +45,14 @@ impl Filesystem { /// Checks if a directory is empty pub fn is_dir_empty(&self, dir: &str) -> bool { - let finder = Finder::create() + let mut finder = Finder::create(); + finder .ignore_vcs(false) .ignore_dot_files(false) .depth(0) .r#in(dir); - count(&finder) == 0 + finder.len() == 0 } pub fn empty_directory( @@ -68,14 +69,15 @@ impl Filesystem { } if is_dir(dir) { - let finder = Finder::create() + let mut finder = Finder::create(); + finder .ignore_vcs(false) .ignore_dot_files(false) .depth(0) .r#in(dir); - for path in &finder { - self.remove(&path.to_string())?; + for path in finder.iter() { + self.remove(&path.get_pathname())?; } } Ok(()) @@ -102,11 +104,23 @@ impl Filesystem { vec!["rm".to_string(), "-rf".to_string(), directory.to_string()] }; - let mut output = String::new(); - let result = self.get_process().execute(&cmd, &mut output) == 0; + let mut output = PhpMixed::Null; + let result = self + .get_process() + .execute( + PhpMixed::List( + cmd.iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ), + Some(&mut output), + (), + ) + .map(|n| n == 0) + .unwrap_or(false); // clear stat cache because external processes aren't tracked by the php stat cache - clearstatcache(false, ""); + clearstatcache2(false, ""); if result && !is_dir(directory) { return Ok(true); @@ -125,7 +139,9 @@ impl Filesystem { ) -> anyhow::Result<Box<dyn PromiseInterface>> { let edge_case_result = self.remove_edge_cases(directory, true)?; if let Some(r) = edge_case_result { - return Ok(react_promise_resolve(PhpMixed::Bool(r))); + return Ok(shirabe_external_packages::react::promise::resolve(Some( + PhpMixed::Bool(r), + ))); } let cmd: Vec<String> = if Platform::is_windows() { @@ -139,34 +155,40 @@ impl Filesystem { vec!["rm".to_string(), "-rf".to_string(), directory.to_string()] }; - let promise = self.get_process().execute_async(&cmd); + let promise = self.get_process().execute_async( + PhpMixed::List( + cmd.iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ), + (), + )?; let directory_owned = directory.to_string(); // TODO(plugin): closure capture of $this in PHP — port wires the same logic via a callback handle. - Ok(promise.then(Box::new( - move |process: PhpMixed| -> Box<dyn PromiseInterface> { - // clear stat cache because external processes aren't tracked by the php stat cache - clearstatcache(false, ""); + Ok(promise.then_boxed( + Some(Box::new( + move |process: PhpMixed| -> Box<dyn PromiseInterface> { + // clear stat cache because external processes aren't tracked by the php stat cache + clearstatcache2(false, ""); - let is_successful = process - .as_object() - .map(|o| { - o.call_method("isSuccessful", &[]) - .as_bool() - .unwrap_or(false) - }) - .unwrap_or(false); - if is_successful && !is_dir(&directory_owned) { - return react_promise_resolve(PhpMixed::Bool(true)); - } + // TODO(phase-b): ArrayObject has no call_method; PHP-side calls $process->isSuccessful(). + let is_successful = matches!(process, PhpMixed::Bool(true)); + if is_successful && !is_dir(&directory_owned) { + return shirabe_external_packages::react::promise::resolve(Some( + PhpMixed::Bool(true), + )); + } - // PHP: \React\Promise\resolve($this->removeDirectoryPhp($directory)) - // The recursive PHP call doesn't have a clean async equivalent; we resort to a sync call. - let mut fs = Filesystem::new(None); - let res = fs.remove_directory_php(&directory_owned).unwrap_or(false); - react_promise_resolve(PhpMixed::Bool(res)) - }, - ))) + // PHP: \React\Promise\resolve($this->removeDirectoryPhp($directory)) + // The recursive PHP call doesn't have a clean async equivalent; we resort to a sync call. + let mut fs = Filesystem::new(None); + let res = fs.remove_directory_php(&directory_owned).unwrap_or(false); + shirabe_external_packages::react::promise::resolve(Some(PhpMixed::Bool(res))) + }, + )), + None, + )) } /// Returns null when no edge case was hit. Otherwise a bool whether removal was successful @@ -218,24 +240,10 @@ impl Filesystem { } // PHP: $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); - let mut it_result = + // TODO(phase-b): PHP throws UnexpectedValueException on iterator creation failure; + // shim signature does not yet model this. Skipping the retry/clearstatcache branch. + let it = shirabe_php_shim::recursive_directory_iterator(directory, shirabe_php_shim::SKIP_DOTS); - if let Err(e) = &it_result { - if e.downcast_ref::<UnexpectedValueException>().is_some() { - // re-try once after clearing the stat cache if it failed as it - // sometimes fails without apparent reason, see https://github.com/composer/composer/issues/4009 - clearstatcache(false, ""); - usleep(100000); - if !is_dir(directory) { - return Ok(true); - } - it_result = shirabe_php_shim::recursive_directory_iterator( - directory, - shirabe_php_shim::SKIP_DOTS, - ); - } - } - let it = it_result?; let ri = shirabe_php_shim::recursive_iterator_iterator(it, shirabe_php_shim::CHILD_FIRST); for file in &ri { @@ -268,7 +276,8 @@ impl Filesystem { "Could not delete symbolic link {}: {}", directory, error_get_last() - .get("message") + .as_ref() + .and_then(|m| m.get("message")) .and_then(|v| v.as_string()) .unwrap_or("") ), @@ -283,7 +292,8 @@ impl Filesystem { "{} does not exist and could not be created: {}", directory, error_get_last() - .get("message") + .as_ref() + .and_then(|m| m.get("message")) .and_then(|v| v.as_string()) .unwrap_or("") ), @@ -324,7 +334,8 @@ impl Filesystem { "Could not delete {}: {}", path, error - .get("message") + .as_ref() + .and_then(|m| m.get("message")) .and_then(|v| v.as_string()) .unwrap_or("") ); @@ -355,7 +366,8 @@ impl Filesystem { "Could not delete {}: {}", path, error - .get("message") + .as_ref() + .and_then(|m| m.get("message")) .and_then(|v| v.as_string()) .unwrap_or("") ); @@ -397,9 +409,9 @@ impl Filesystem { match result { Ok(b) => return Ok(b), Err(payload) => { - let e = match payload.downcast_ref::<ErrorException>() { - Some(e) => e.clone(), - None => return Err(anyhow::anyhow!("Copy panicked")), + let e: ErrorException = match payload.downcast::<ErrorException>() { + Ok(boxed) => *boxed, + Err(_) => return Err(anyhow::anyhow!("Copy panicked")), }; // if copy fails we attempt to copy it manually as this can help bypass issues with VirtualBox shared folders @@ -410,15 +422,15 @@ impl Filesystem { if source_handle.is_none() || target_handle.is_none() { return Err(e.into()); } - let source_handle = source_handle.unwrap(); - let target_handle = target_handle.unwrap(); - while !feof(&source_handle) { - if !fwrite(&target_handle, &fread(&source_handle, 1024 * 1024)) { - return Err(e.into()); - } + while !feof(source_handle.clone()) { + let chunk = + fread(source_handle.clone(), 1024 * 1024).unwrap_or_default(); + // TODO(phase-b): PHP fwrite returns int|false; shim currently returns (); + // assume success here. + fwrite(target_handle.clone(), &chunk, chunk.len() as i64); } - fclose(&source_handle); - fclose(&target_handle); + fclose(source_handle); + fclose(target_handle); return Ok(true); } @@ -428,7 +440,7 @@ impl Filesystem { } let it = - shirabe_php_shim::recursive_directory_iterator(source, shirabe_php_shim::SKIP_DOTS)?; + shirabe_php_shim::recursive_directory_iterator(source, shirabe_php_shim::SKIP_DOTS); let ri = shirabe_php_shim::recursive_iterator_iterator(it, shirabe_php_shim::SELF_FIRST); self.ensure_directory_exists(&target)?; @@ -457,8 +469,8 @@ impl Filesystem { if Platform::is_windows() { // Try to copy & delete - this is a workaround for random "Access denied" errors. let mut output = String::new(); - let result = self.get_process().execute( - &vec![ + let result = self.get_process().execute_args( + &[ "xcopy".to_string(), source.to_string(), target.to_string(), @@ -468,10 +480,11 @@ impl Filesystem { "/Y".to_string(), ], &mut output, + (), ); // clear stat cache because external processes aren't tracked by the php stat cache - clearstatcache(false, ""); + clearstatcache2(false, ""); if 0 == result { self.remove(source)?; @@ -482,13 +495,14 @@ impl Filesystem { // We do not use PHP's "rename" function here since it does not support // the case where $source, and $target are located on different partitions. let mut output = String::new(); - let result = self.get_process().execute( - &vec!["mv".to_string(), source.to_string(), target.to_string()], + let result = self.get_process().execute_args( + &["mv".to_string(), source.to_string(), target.to_string()], &mut output, + (), ); // clear stat cache because external processes aren't tracked by the php stat cache - clearstatcache(false, ""); + clearstatcache2(false, ""); if 0 == result { return Ok(()); @@ -522,7 +536,7 @@ impl Filesystem { let to = self.normalize_path(to); if directories { - from = format!("{}/dummy_file", rtrim(&from, "/")); + from = format!("{}/dummy_file", rtrim(&from, Some("/"))); } if dirname(&from) == dirname(&to) { @@ -542,9 +556,8 @@ impl Filesystem { return to; } - common_path = format!("{}/", rtrim(&common_path, "/")); - let source_path_depth = - substr_count(&substr(&from, strlen(&common_path) as isize, None), "/"); + common_path = format!("{}/", rtrim(&common_path, Some("/"))); + let source_path_depth = substr_count(&substr(&from, strlen(&common_path), None), "/"); let common_path_code = str_repeat("../", source_path_depth as usize); // allow top level /foo & /bar dirs to be addressed relatively as this is common in Docker setups @@ -555,7 +568,7 @@ impl Filesystem { let result = format!( "{}{}", common_path_code, - substr(&to, strlen(&common_path) as isize, None) + substr(&to, strlen(&common_path), None) ); if strlen(&result) == 0 { return "./".to_string(); @@ -604,19 +617,16 @@ impl Filesystem { return var_export(&PhpMixed::String(to), true); } - common_path = format!("{}/", rtrim(&common_path, "/")); + common_path = format!("{}/", rtrim(&common_path, Some("/"))); if str_starts_with(&to, &format!("{}/", from)) { return format!( "__DIR__ . {}", - var_export( - &PhpMixed::String(substr(&to, strlen(&from) as isize, None)), - true - ) + var_export(&PhpMixed::String(substr(&to, strlen(&from), None)), true) ); } - let source_path_depth = - (substr_count(&substr(&from, strlen(&common_path) as isize, None), "/") as i64) - + (if directories { 1 } else { 0 }); + let source_path_depth = (substr_count(&substr(&from, strlen(&common_path), None), "/") + as i64) + + (if directories { 1 } else { 0 }); // allow top level /foo & /bar dirs to be addressed relatively as this is common in Docker setups if !prefer_relative && "/" == common_path && source_path_depth > 1 { @@ -636,7 +646,7 @@ impl Filesystem { str_repeat(")", source_path_depth as usize) ) }; - let rel_target = substr(&to, strlen(&common_path) as isize, None); + let rel_target = substr(&to, strlen(&common_path), None); format!( "{}{}", @@ -673,7 +683,7 @@ impl Filesystem { return Ok(self.directory_size(path)); } - Ok(filesize(path) as i64) + Ok(filesize(path).unwrap_or(0)) } /// Normalize a path. This replaces backslashes with slashes, removes ending @@ -691,7 +701,10 @@ impl Filesystem { } // extract a prefix being a protocol://, protocol:, protocol://drive: or simply drive: - let mut prefix_match: Vec<String> = vec![]; + let mut prefix_match: indexmap::IndexMap< + shirabe_external_packages::composer::pcre::preg::CaptureKey, + String, + > = indexmap::IndexMap::new(); if Preg::is_match_strict_groups3( "{^( [0-9a-z]{2,}+: (?: // (?: [a-z]: )? )? | [a-z]: )}ix", &path, @@ -699,8 +712,11 @@ impl Filesystem { ) .unwrap_or(false) { - prefix = prefix_match[1].clone(); - path = substr(&path, strlen(&prefix) as isize, None); + prefix = prefix_match + .get(&shirabe_external_packages::composer::pcre::preg::CaptureKey::ByIndex(1)) + .cloned() + .unwrap_or_default(); + path = substr(&path, strlen(&prefix), None); } if strpos(&path, "/") == Some(0) { @@ -712,7 +728,7 @@ impl Filesystem { for chunk in explode("/", &path) { if ".." == chunk && (strlen(&absolute) > 0 || up) { array_pop(&mut parts); - up = !(count(&parts) == 0 || ".." == end(&parts).unwrap_or_default()); + up = !(parts.len() == 0 || ".." == end(&parts).unwrap_or_default()); } else if "." != chunk && "" != chunk { parts.push(chunk.clone()); up = ".." != chunk; @@ -722,9 +738,20 @@ impl Filesystem { // ensure c: is normalized to C: prefix = Preg::replace_callback( "{(^|://)[a-z]:$}i", - Box::new(|m: &Vec<String>| -> String { strtoupper(&m[0]) }), + |m: &indexmap::IndexMap< + shirabe_external_packages::composer::pcre::preg::CaptureKey, + String, + >| + -> String { + let s = m + .get(&shirabe_external_packages::composer::pcre::preg::CaptureKey::ByIndex(0)) + .cloned() + .unwrap_or_default(); + strtoupper(&s) + }, &prefix, - ); + ) + .unwrap_or_default(); format!("{}{}{}", prefix, absolute, implode("/", &parts)) } @@ -735,7 +762,7 @@ impl Filesystem { pub fn trim_trailing_slash(path: &str) -> String { let mut path = path.to_string(); if !Preg::is_match3("{^[/\\\\]+$}", &path, None).unwrap_or(false) { - path = rtrim(&path, "/\\"); + path = rtrim(&path, Some("/\\")); } path @@ -765,10 +792,11 @@ impl Filesystem { pub fn get_platform_path(path: &str) -> String { let mut path = path.to_string(); if Platform::is_windows() { - path = Preg::replace("{^(?:file:///([a-z]):?/)}i", "file://$1:/", &path); + path = Preg::replace("{^(?:file:///([a-z]):?/)}i", "file://$1:/", &path) + .unwrap_or_default(); } - Preg::replace("{^file://}i", "", &path) + Preg::replace("{^file://}i", "", &path).unwrap_or_default() } /// Cross-platform safe version of is_readable() @@ -795,8 +823,7 @@ impl Filesystem { pub(crate) fn directory_size(&self, directory: &str) -> i64 { let it = - shirabe_php_shim::recursive_directory_iterator(directory, shirabe_php_shim::SKIP_DOTS) - .unwrap(); + shirabe_php_shim::recursive_directory_iterator(directory, shirabe_php_shim::SKIP_DOTS); let ri = shirabe_php_shim::recursive_iterator_iterator(it, shirabe_php_shim::CHILD_FIRST); let mut size: i64 = 0; @@ -812,7 +839,7 @@ impl Filesystem { pub(crate) fn get_process(&mut self) -> std::cell::RefMut<'_, ProcessExecutor> { if self.process_executor.is_none() { self.process_executor = Some(std::rc::Rc::new(std::cell::RefCell::new( - ProcessExecutor::new(None), + ProcessExecutor::new(()), ))); } @@ -870,7 +897,7 @@ impl Filesystem { return pathname.to_string(); } - let resolved = rtrim(pathname, "/"); + let resolved = rtrim(pathname, Some("/")); if 0 == strlen(&resolved) { return pathname.to_string(); @@ -916,7 +943,7 @@ impl Filesystem { Platform::realpath(target), ]; let mut output = String::new(); - if self.get_process().execute(&cmd, &mut output) != 0 { + if self.get_process().execute_args(&cmd, &mut output, ()) != 0 { return Err(IOException::new( format!( "Failed to create junction to \"{}\" at \"{}\".", @@ -928,7 +955,7 @@ impl Filesystem { ) .into()); } - clearstatcache(true, junction); + clearstatcache2(true, junction); Ok(()) } @@ -953,7 +980,7 @@ impl Filesystem { } // Important to clear all caches first - clearstatcache(true, junction); + clearstatcache2(true, junction); if !is_dir(junction) || is_link(junction) { return false; @@ -976,7 +1003,7 @@ impl Filesystem { } let junction = rtrim( &str_replace("/", DIRECTORY_SEPARATOR, junction), - DIRECTORY_SEPARATOR, + Some(DIRECTORY_SEPARATOR), ); if !self.is_junction(&junction) { return Err(IOException::new( @@ -998,7 +1025,7 @@ impl Filesystem { let current_content = Silencer::call(|| Ok(file_get_contents(path).unwrap_or_default())).unwrap_or_default(); if current_content.is_empty() || current_content != content { - return Ok(file_put_contents(path, content) as i64); + return Ok(file_put_contents(path, content.as_bytes()).unwrap_or(0)); } Ok(0) @@ -1007,14 +1034,24 @@ impl Filesystem { /// Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463 pub fn safe_copy(&self, source: &str, target: &str) -> anyhow::Result<()> { if !file_exists(target) || !file_exists(source) || !self.files_are_equal(source, target) { - let source_handle = fopen(source, "r") - .ok_or_else(|| anyhow::anyhow!("Could not open \"{}\" for reading.", source))?; - let target_handle = fopen(target, "w+") - .ok_or_else(|| anyhow::anyhow!("Could not open \"{}\" for writing.", target))?; + let source_handle = fopen(source, "r"); + if source_handle.is_none() { + return Err(anyhow::anyhow!( + "Could not open \"{}\" for reading.", + source + )); + } + let target_handle = fopen(target, "w+"); + if target_handle.is_none() { + return Err(anyhow::anyhow!( + "Could not open \"{}\" for writing.", + target + )); + } - shirabe_php_shim::stream_copy_to_stream(&source_handle, &target_handle); - fclose(&source_handle); - fclose(&target_handle); + shirabe_php_shim::stream_copy_to_stream(source_handle.clone(), target_handle.clone()); + fclose(source_handle); + fclose(target_handle); touch(target); // PHP also passes filemtime/fileatime — skipping detailed timestamp restore here. @@ -1032,25 +1069,25 @@ impl Filesystem { } // Check if content is different - let a_handle = match fopen(a, "rb") { - Some(h) => h, - None => return false, - }; - let b_handle = match fopen(b, "rb") { - Some(h) => h, - None => return false, - }; + let a_handle = fopen(a, "rb"); + if a_handle.is_none() { + return false; + } + let b_handle = fopen(b, "rb"); + if b_handle.is_none() { + return false; + } let mut result = true; - while !feof(&a_handle) { - if fread(&a_handle, 8192) != fread(&b_handle, 8192) { + while !feof(a_handle.clone()) { + if fread(a_handle.clone(), 8192) != fread(b_handle.clone(), 8192) { result = false; break; } } - fclose(&a_handle); - fclose(&b_handle); + fclose(a_handle); + fclose(b_handle); result } diff --git a/crates/shirabe/src/util/forgejo.rs b/crates/shirabe/src/util/forgejo.rs index 88df409..002e4e5 100644 --- a/crates/shirabe/src/util/forgejo.rs +++ b/crates/shirabe/src/util/forgejo.rs @@ -43,15 +43,23 @@ impl Forgejo { io_interface::NORMAL, ); self.io.write_error3(&url, true, io_interface::NORMAL); - let local_auth_config = self.config.borrow().get_local_auth_config_source(); + let (local_auth_name, has_local_auth, auth_name): (String, bool, String) = { + let cfg = self.config.borrow(); + let local = cfg + .get_local_auth_config_source() + .map(|s| s.get_name().to_string()); + let auth = cfg.get_auth_config_source().get_name().to_string(); + (local.clone().unwrap_or_default(), local.is_some(), auth) + }; + let local_prefix = if has_local_auth { + format!("{} OR ", local_auth_name) + } else { + String::new() + }; self.io.write_error3( &format!( - "Tokens will be stored in plain text in \"{}\" for future use by Composer.", - local_auth_config - .as_ref() - .map(|s| format!("{} OR ", s.get_name())) - .unwrap_or_default() - + self.config.borrow().get_auth_config_source().get_name() + "Tokens will be stored in plain text in \"{}{}\" for future use by Composer.", + local_prefix, auth_name ), true, io_interface::NORMAL, @@ -63,19 +71,25 @@ impl Forgejo { ); let mut store_in_local_auth_config = false; - if local_auth_config.is_some() { + if has_local_auth { store_in_local_auth_config = self.io.ask_confirmation( - "A local auth config source was found, do you want to store the token there?", + "A local auth config source was found, do you want to store the token there?" + .to_string(), true, ); } - let username = self.io.ask("Username: ", None).trim().to_string(); + let username = self + .io + .ask("Username: ".to_string(), shirabe_php_shim::PhpMixed::Null) + .as_string() + .map(|s| s.trim().to_string()) + .unwrap_or_default(); let token = self .io - .ask_and_hide_answer("Token (hidden): ") - .trim() - .to_string(); + .ask_and_hide_answer("Token (hidden): ".to_string()) + .map(|s| s.trim().to_string()) + .unwrap_or_default(); let add_token_manually = format!( "You can also add it manually later by using \"composer config --global --auth forgejo-token.{} <username> <token>\"", @@ -93,8 +107,11 @@ impl Forgejo { return Ok(Ok(false)); } - self.io - .set_authentication(origin_url.to_string(), username.clone(), token.clone()); + self.io.set_authentication( + origin_url.to_string(), + username.clone(), + Some(token.clone()), + ); match self.http_downloader.borrow_mut().get( &format!("https://{}/api/v1/version", origin_url), @@ -104,7 +121,13 @@ impl Forgejo { ) { Ok(_) => {} Err(e) => { - if [403, 401, 404].contains(&e.get_code()) { + // TODO(phase-b): anyhow::Error has no get_code(); HTTP status codes come from + // TransportException::get_status_code(). + let code = e + .downcast_ref::<crate::downloader::transport_exception::TransportException>() + .and_then(|te| te.get_status_code()) + .unwrap_or(0); + if [403, 401, 404].contains(&code) { self.io.write_error3( "<error>Invalid access token provided.</error>", true, @@ -116,30 +139,35 @@ impl Forgejo { return Ok(Ok(false)); } - return Ok(Err(e)); + // TODO(phase-b): downcast anyhow::Error to TransportException for the inner Err + return Err(e); } } // store value in local/user config - let local_auth_config = self.config.borrow().get_local_auth_config_source(); - let auth_config_source = if store_in_local_auth_config { - local_auth_config - .as_ref() - .unwrap_or_else(|| self.config.borrow().get_auth_config_source()) + // TODO(phase-b): Config getters return references; cross-borrows of self.config.borrow() + // cannot live across method calls. Needs Rc<RefCell<dyn ConfigSourceInterface>> shape. + let setting_key = format!("forgejo-token.{}", origin_url); + { + let mut cfg = self.config.borrow_mut(); + cfg.get_config_source_mut() + .remove_config_setting(&setting_key)?; + } + let value: shirabe_php_shim::PhpMixed = + shirabe_php_shim::PhpMixed::Array(indexmap::indexmap! { + "username".to_string() => Box::new(username.clone().into()), + "token".to_string() => Box::new(token.clone().into()), + }); + if store_in_local_auth_config && has_local_auth { + let mut cfg = self.config.borrow_mut(); + if let Some(local) = cfg.get_local_auth_config_source_mut() { + local.add_config_setting(&setting_key, value)?; + } } else { - self.config.borrow().get_auth_config_source() - }; - self.config - .borrow() - .get_config_source() - .remove_config_setting(&format!("forgejo-token.{}", origin_url)); - auth_config_source.add_config_setting( - &format!("forgejo-token.{}", origin_url), - indexmap::indexmap! { - "username".to_string() => username.into(), - "token".to_string() => token.into(), - }, - ); + let mut cfg = self.config.borrow_mut(); + cfg.get_auth_config_source_mut() + .add_config_setting(&setting_key, value)?; + } self.io.write_error3( "<info>Token stored successfully.</info>", diff --git a/crates/shirabe/src/util/git.rs b/crates/shirabe/src/util/git.rs index 3093734..cd5082f 100644 --- a/crates/shirabe/src/util/git.rs +++ b/crates/shirabe/src/util/git.rs @@ -15,7 +15,7 @@ use shirabe_php_shim::{ use crate::config::Config; use crate::io::io_interface::IOInterface; -use crate::util::auth_helper::AuthHelper; +use crate::util::auth_helper::{AuthHelper, StoreAuth}; use crate::util::bitbucket::Bitbucket; use crate::util::filesystem::Filesystem; use crate::util::github::GitHub; @@ -117,7 +117,7 @@ impl Git { map.insert("%url%".to_string(), url.to_string()); map.insert( "%sanitizedUrl%".to_string(), - Preg::replace(r"{://([^@]+?):(.+?)@}", "://", &url), + Preg::replace(r"{://([^@]+?):(.+?)@}", "://", &url).unwrap_or_default(), ); array_map( @@ -308,7 +308,7 @@ impl Git { } // failed to checkout, first check git accessibility - let m1 = m.get(1).cloned().unwrap_or_default(); + let m1 = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); if !self.io.has_authentication(&m1) && !self.io.is_interactive() { self.throw_exception( &format!( @@ -519,7 +519,7 @@ impl Git { // We already have an access_token from a previous request. if username != "x-token-auth" { let access_token = - bitbucket_util.request_token(&domain, &username, &password); + bitbucket_util.request_token(&domain, &username, &password)?; if !access_token.is_empty() { self.io.set_authentication( domain.clone(), @@ -769,7 +769,12 @@ impl Git { .set_authentication(m2.clone(), username, Some(password)); let mut auth_helper = AuthHelper::new(self.io.clone_box(), std::rc::Rc::clone(&self.config)); - auth_helper.store_auth(&m2, &store_auth); + let store_auth_enum = match &store_auth { + PhpMixed::String(s) if s == "prompt" => StoreAuth::Prompt, + PhpMixed::Bool(b) => StoreAuth::Bool(*b), + _ => StoreAuth::Bool(false), + }; + auth_helper.store_auth(&m2, store_auth_enum)?; return Ok(()); } @@ -946,7 +951,7 @@ impl Git { && pretty_version.is_some() { let branch = - Preg::replace(r"{(?:^dev-|(?:\.x)?-dev$)}i", "", &pretty_version.unwrap()); + Preg::replace(r"{(?:^dev-|(?:\.x)?-dev$)}i", "", &pretty_version.unwrap())?; let mut branches: Option<String> = None; let mut tags: Option<String> = None; let mut output = String::new(); @@ -1126,9 +1131,9 @@ impl Git { "fatal: could not read Username", ]; - let error_output = self.process.borrow().get_error_output(); + let error_output = self.process.borrow().get_error_output().to_string(); for auth_failure in &auth_failures { - if strpos(error_output, auth_failure).is_some() { + if strpos(&error_output, auth_failure).is_some() { return Some(m); } } @@ -1304,7 +1309,7 @@ impl Git { if self.process.borrow_mut().execute_args( &vec!["git".to_string(), "--version".to_string()], &mut ignored_output, - None, + Option::<&str>::None, ) != 0 { return Err(RuntimeException { diff --git a/crates/shirabe/src/util/github.rs b/crates/shirabe/src/util/github.rs index cfb3d0c..c9513aa 100644 --- a/crates/shirabe/src/util/github.rs +++ b/crates/shirabe/src/util/github.rs @@ -68,7 +68,7 @@ impl GitHub { "github.accesstoken".to_string(), ], &mut output, - None, + (), ) == 0 { self.io.set_authentication( @@ -103,7 +103,7 @@ impl GitHub { if self .process .borrow_mut() - .execute_args(&["hostname".to_string()], &mut output, None) + .execute_args(&["hostname".to_string()], &mut output, ()) == 0 { note += &format!(" on {}", output.trim()); @@ -111,84 +111,74 @@ impl GitHub { } note += &format!(" {}", date("Y-m-d Hi", None)); - let local_auth_config = self.config.borrow().get_local_auth_config_source(); - - self.io.write_error3(PhpMixed::List(vec![ - Box::new(PhpMixed::String( - "You need to provide a GitHub access token.".to_string(), - )), - Box::new(PhpMixed::String(format!( - "Tokens will be stored in plain text in \"{}\" for future use by Composer.", - local_auth_config - .as_ref() - .map(|c| format!("{} OR ", c.get_name())) - .unwrap_or_default() - + &self.config.borrow().get_auth_config_source().get_name() - ))), - Box::new(PhpMixed::String( - "Due to the security risk of tokens being exfiltrated, use tokens with short expiration times and only the minimum permissions necessary.".to_string(), - )), - Box::new(PhpMixed::String(String::new())), - Box::new(PhpMixed::String( - "Carefully consider the following options in order:".to_string(), - )), - Box::new(PhpMixed::String(String::new())), - ]), true, io_interface::NORMAL); + // PHP: writeError(array) joins with newline. TODO(phase-b): writeError accepts array natively in Symfony. + let (local_name, auth_name): (Option<String>, String) = { + let cfg = self.config.borrow(); + ( + cfg.get_local_auth_config_source() + .map(|c| c.get_name().to_string()), + cfg.get_auth_config_source().get_name().to_string(), + ) + }; + let prefix = local_name + .as_ref() + .map(|n| format!("{} OR ", n)) + .unwrap_or_default(); + let lines = [ + "You need to provide a GitHub access token.".to_string(), + format!( + "Tokens will be stored in plain text in \"{}{}\" for future use by Composer.", + prefix, auth_name + ), + "Due to the security risk of tokens being exfiltrated, use tokens with short expiration times and only the minimum permissions necessary.".to_string(), + String::new(), + "Carefully consider the following options in order:".to_string(), + String::new(), + ]; + self.io + .write_error3(&lines.join("\n"), true, io_interface::NORMAL); let encoded_note = shirabe_php_shim::rawurlencode(¬e).replace("%20", "+"); - self.io.write_error3(PhpMixed::List(vec![ - Box::new(PhpMixed::String( - "1. When you don't use 'vcs' type 'repositories' in composer.json and do not need to clone source or download dist files".to_string(), - )), - Box::new(PhpMixed::String( - "from private GitHub repositories over HTTPS, use a fine-grained token with read-only access to public information.".to_string(), - )), - Box::new(PhpMixed::String( - "Use the following URL to create such a token:".to_string(), - )), - Box::new(PhpMixed::String(format!( + let lines = [ + "1. When you don't use 'vcs' type 'repositories' in composer.json and do not need to clone source or download dist files".to_string(), + "from private GitHub repositories over HTTPS, use a fine-grained token with read-only access to public information.".to_string(), + "Use the following URL to create such a token:".to_string(), + format!( "https://{}/settings/personal-access-tokens/new?name={}", origin_url, encoded_note - ))), - Box::new(PhpMixed::String(String::new())), - ]), true, io_interface::NORMAL); + ), + String::new(), + ]; + self.io + .write_error3(&lines.join("\n"), true, io_interface::NORMAL); - self.io.write_error3(PhpMixed::List(vec![ - Box::new(PhpMixed::String( - "2. When all relevant _private_ GitHub repositories belong to a single user or organisation, use a fine-grained token with".to_string(), - )), - Box::new(PhpMixed::String( - "repository \"content\" read-only permissions. You can start with the following URL, but you may need to change the resource owner".to_string(), - )), - Box::new(PhpMixed::String( - "to the right user or organisation. Additionally, you can scope permissions down to apply only to selected repositories.".to_string(), - )), - Box::new(PhpMixed::String(format!( + let lines = [ + "2. When all relevant _private_ GitHub repositories belong to a single user or organisation, use a fine-grained token with".to_string(), + "repository \"content\" read-only permissions. You can start with the following URL, but you may need to change the resource owner".to_string(), + "to the right user or organisation. Additionally, you can scope permissions down to apply only to selected repositories.".to_string(), + format!( "https://{}/settings/personal-access-tokens/new?contents=read&name={}", origin_url, encoded_note - ))), - Box::new(PhpMixed::String(String::new())), - ]), true, io_interface::NORMAL); + ), + String::new(), + ]; + self.io + .write_error3(&lines.join("\n"), true, io_interface::NORMAL); - self.io.write_error3(PhpMixed::List(vec![ - Box::new(PhpMixed::String( - "3. A \"classic\" token grants broad permissions on your behalf to all repositories accessible by you.".to_string(), - )), - Box::new(PhpMixed::String( - "This may include write permissions, even though not needed by Composer. Use it only when you need to access".to_string(), - )), - Box::new(PhpMixed::String( - "private repositories across multiple organisations at the same time and using directory-specific authentication sources".to_string(), - )), - Box::new(PhpMixed::String( - "is not an option. You can generate a classic token here:".to_string(), - )), - Box::new(PhpMixed::String(format!( + let mut lines3 = vec![ + "3. A \"classic\" token grants broad permissions on your behalf to all repositories accessible by you.".to_string(), + "This may include write permissions, even though not needed by Composer. Use it only when you need to access".to_string(), + "private repositories across multiple organisations at the same time and using directory-specific authentication sources".to_string(), + "is not an option. You can generate a classic token here:".to_string(), + format!( "https://{}/settings/tokens/new?scopes=repo&description={}", origin_url, encoded_note - ))), - Box::new(PhpMixed::String(String::new())), - ]), true, io_interface::NORMAL); + ), + String::new(), + ]; + let _ = &mut lines3; + self.io + .write_error3(&lines3.join("\n"), true, io_interface::NORMAL); self.io.write_error3( "For additional information, check https://getcomposer.org/doc/articles/authentication-for-private-packages.md#github-oauth", @@ -197,7 +187,7 @@ impl GitHub { ); let mut store_in_local_auth_config = false; - if local_auth_config.is_some() { + if local_name.is_some() { store_in_local_auth_config = self.io.ask_confirmation( "A local auth config source was found, do you want to store the token there?" .to_string(), @@ -238,21 +228,22 @@ impl GitHub { format!("{}/api/v3/", origin_url) }; - let mut http_options = indexmap::IndexMap::new(); - http_options.insert( - "retry-auth-failure".to_string(), - Box::new(PhpMixed::Bool(false)), - ); - let http_options = PhpMixed::Array(http_options); + let mut http_options: indexmap::IndexMap<String, PhpMixed> = indexmap::IndexMap::new(); + http_options.insert("retry-auth-failure".to_string(), PhpMixed::Bool(false)); match self .http_downloader .borrow_mut() - .get(&format!("https://{}", api_url), &http_options) + .get(&format!("https://{}", api_url), http_options) { Ok(_) => {} Err(te) => { - if te.code == 403 || te.code == 401 { + // TODO(phase-b): downcast anyhow::Error to TransportException for status code + let code = te + .downcast_ref::<crate::downloader::transport_exception::TransportException>() + .and_then(|t| t.get_status_code()) + .unwrap_or(0); + if code == 403 || code == 401 { self.io.write_error3( "<error>Invalid token provided.</error>", true, @@ -269,34 +260,28 @@ impl GitHub { } } + // TODO(phase-b): Config getters return references; cross-borrow chains require + // Rc<RefCell<dyn ConfigSourceInterface>> shape. For now use _mut variants. let use_local = store_in_local_auth_config && self .config .borrow() .get_local_auth_config_source() .is_some(); - let auth_config_source_name; + let key = format!("github-oauth.{}", origin_url); + { + let mut cfg = self.config.borrow_mut(); + cfg.get_config_source_mut().remove_config_setting(&key)?; + } if use_local { - let mut auth_config_source = - self.config.borrow().get_local_auth_config_source().unwrap(); - self.config - .borrow() - .get_config_source() - .remove_config_setting(&format!("github-oauth.{}", origin_url))?; - auth_config_source.add_config_setting( - &format!("github-oauth.{}", origin_url), - PhpMixed::String(token), - )?; + let mut cfg = self.config.borrow_mut(); + if let Some(local) = cfg.get_local_auth_config_source_mut() { + local.add_config_setting(&key, PhpMixed::String(token))?; + } } else { - let mut auth_config_source = self.config.borrow().get_auth_config_source(); - self.config - .borrow() - .get_config_source() - .remove_config_setting(&format!("github-oauth.{}", origin_url))?; - auth_config_source.add_config_setting( - &format!("github-oauth.{}", origin_url), - PhpMixed::String(token), - )?; + let mut cfg = self.config.borrow_mut(); + cfg.get_auth_config_source_mut() + .add_config_setting(&key, PhpMixed::String(token))?; } self.io.write_error3( diff --git a/crates/shirabe/src/util/gitlab.rs b/crates/shirabe/src/util/gitlab.rs index 11b7a21..7049686 100644 --- a/crates/shirabe/src/util/gitlab.rs +++ b/crates/shirabe/src/util/gitlab.rs @@ -73,7 +73,7 @@ impl GitLab { "gitlab.accesstoken".to_string(), ], &mut output, - None, + (), ) == 0 { self.io.set_authentication( @@ -94,7 +94,7 @@ impl GitLab { "gitlab.deploytoken.user".to_string(), ], &mut token_user, - None, + (), ) == 0 && self.process.borrow_mut().execute_args( &[ @@ -103,7 +103,7 @@ impl GitLab { "gitlab.deploytoken.token".to_string(), ], &mut token_password, - None, + (), ) == 0 { self.io.set_authentication( @@ -177,7 +177,12 @@ impl GitLab { self.io.write_error3(msg, true, io_interface::NORMAL); } - let local_auth_config = self.config.borrow().get_local_auth_config_source(); + let local_auth_config_name: Option<String> = self + .config + .borrow() + .get_local_auth_config_source() + .map(|c| c.get_name()); + let has_local_auth_config = local_auth_config_name.is_some(); let personal_access_token_link = format!( "{}://{}/-/user_settings/personal_access_tokens", scheme, origin_url @@ -186,9 +191,9 @@ impl GitLab { self.io.write_error3( &format!( "A token will be created and stored in \"{}\", your password will never be stored", - local_auth_config + local_auth_config_name .as_ref() - .map(|c| format!("{} OR ", c.get_name())) + .map(|name| format!("{} OR ", name)) .unwrap_or_default() + &self.config.borrow().get_auth_config_source().get_name() ), @@ -223,7 +228,7 @@ impl GitLab { .write_error3("for more details.", true, io_interface::NORMAL); let mut store_in_local_auth_config = false; - if local_auth_config.is_some() { + if has_local_auth_config { store_in_local_auth_config = self.io.ask_confirmation( "A local auth config source was found, do you want to store the token there?" .to_string(), @@ -313,14 +318,15 @@ impl GitLab { ); // store value in user config in auth file - let use_local = store_in_local_auth_config && local_auth_config.is_some(); + let use_local = store_in_local_auth_config && has_local_auth_config; let has_expires_in = response .as_array() .map(|arr| arr.contains_key("expires_in")) .unwrap_or(false); if use_local { - let mut auth_config_source = local_auth_config.clone().unwrap(); + let mut config = self.config.borrow_mut(); + let auth_config_source = config.get_local_auth_config_source_mut().unwrap(); if has_expires_in { auth_config_source.add_config_setting( &format!("gitlab-oauth.{}", origin_url), @@ -333,7 +339,8 @@ impl GitLab { )?; } } else { - let mut auth_config_source = self.config.borrow().get_auth_config_source(); + let mut config = self.config.borrow_mut(); + let auth_config_source = config.get_auth_config_source_mut(); if has_expires_in { auth_config_source.add_config_setting( &format!("gitlab-oauth.{}", origin_url), @@ -392,8 +399,8 @@ impl GitLab { // store value in user config in auth file self.config - .borrow() - .get_auth_config_source() + .borrow_mut() + .get_auth_config_source_mut() .add_config_setting( &format!("gitlab-oauth.{}", origin_url), Self::build_oauth_config(&response, &access_token), @@ -451,7 +458,7 @@ impl GitLab { .borrow_mut() .get( &format!("{}://{}/oauth/token", scheme, api_url), - &PhpMixed::Array(options), + options.into_iter().map(|(k, v)| (k, *v)).collect(), )? .decode_json()?; @@ -538,7 +545,7 @@ impl GitLab { .borrow_mut() .get( &format!("{}://{}/oauth/token", scheme, origin_url), - &PhpMixed::Array(options), + options.into_iter().map(|(k, v)| (k, *v)).collect(), )? .decode_json()?; diff --git a/crates/shirabe/src/util/hg.rs b/crates/shirabe/src/util/hg.rs index fa25a78..0d4d36d 100644 --- a/crates/shirabe/src/util/hg.rs +++ b/crates/shirabe/src/util/hg.rs @@ -52,12 +52,14 @@ impl Hg { } // Try with the authentication information available - let matches = Preg::is_match_with_captures( + let mut matches: indexmap::IndexMap<String, String> = indexmap::IndexMap::new(); + let matched = Preg::is_match_named( r"(?i)^(?P<proto>ssh|https?)://(?:(?P<user>[^:@]+)(?::(?P<pass>[^:@]+))?@)?(?P<host>[^/]+)(?P<path>/.*)?", &url, + &mut matches, )?; - if let Some(matches) = matches { + if matched { if self .io .has_authentication(matches.get("host").map(|s| s.as_str()).unwrap_or("")) @@ -82,8 +84,16 @@ impl Hg { format!( "{}://{}:{}@{}{}", matches.get("proto").unwrap_or(&String::new()), - rawurlencode(auth.get("username").map(|s| s.as_str()).unwrap_or("")), - rawurlencode(auth.get("password").map(|s| s.as_str()).unwrap_or("")), + rawurlencode( + auth.get("username") + .and_then(|s| s.as_deref()) + .unwrap_or("") + ), + rawurlencode( + auth.get("password") + .and_then(|s| s.as_deref()) + .unwrap_or("") + ), matches.get("host").unwrap_or(&String::new()), matches.get("path").unwrap_or(&String::new()), ) @@ -100,7 +110,7 @@ impl Hg { return Ok(()); } - let error = self.process.borrow().get_error_output(); + let error = self.process.borrow().get_error_output().to_string(); return self .throw_exception(&format!("Failed to clone {}, \n\n{}", url, error), &url); } @@ -117,7 +127,7 @@ impl Hg { if Self::get_version(&self.process).is_none() { anyhow::bail!( "{}", - Url::sanitize(&format!( + Url::sanitize(format!( "Failed to clone {}, hg was not found, check that it is installed and in your PATH env.\n\n{}", url, self.process.borrow().get_error_output() @@ -125,7 +135,7 @@ impl Hg { ); } - anyhow::bail!("{}", Url::sanitize(message)); + anyhow::bail!("{}", Url::sanitize(message.to_string())); } pub fn get_version( @@ -137,7 +147,7 @@ impl Hg { if process.borrow_mut().execute_args( &["hg".to_string(), "--version".to_string()], &mut output, - None, + (), ) == 0 { if let Ok(Some(matches)) = Preg::is_match_with_indexed_captures( diff --git a/crates/shirabe/src/util/http/curl_downloader.rs b/crates/shirabe/src/util/http/curl_downloader.rs index c475ad3..fd46220 100644 --- a/crates/shirabe/src/util/http/curl_downloader.rs +++ b/crates/shirabe/src/util/http/curl_downloader.rs @@ -371,7 +371,7 @@ impl CurlDownloader { if !em.is_empty() { em.push_str("\n"); } - em.push_str(&Preg::replace(r"{^fopen\(.*?\): }", "", msg)); + em.push_str(&Preg::replace(r"{^fopen\(.*?\): }", "", msg).unwrap_or_default()); true })); } @@ -552,7 +552,7 @@ impl CurlDownloader { let options = self .auth_helper - .add_authentication_options(options, origin, url); + .add_authentication_options(options, origin, url)?; let options = StreamContextFactory::init_options(url, options, true) .map_err(|e| anyhow::anyhow!(e.message))?; @@ -601,13 +601,13 @@ impl CurlDownloader { curl_setopt_array(&curl_handle, &proxy_curl_options.into_iter().collect()); let progress = array_diff_key( - &match curl_getinfo(&curl_handle) { - PhpMixed::Array(a) => a, + match curl_getinfo(&curl_handle) { + PhpMixed::Array(a) => a.into_iter().map(|(k, v)| (k, *v)).collect(), _ => IndexMap::new(), }, &time_info_static() .into_iter() - .map(|(k, v)| (k, Box::new(PhpMixed::Bool(v)))) + .map(|(k, v)| (k, PhpMixed::Bool(v))) .collect(), ); @@ -633,7 +633,16 @@ impl CurlDownloader { .collect(), ), ); - job.insert("progress".to_string(), PhpMixed::Array(progress.clone())); + job.insert( + "progress".to_string(), + PhpMixed::Array( + progress + .clone() + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), + ), + ); // curlHandle, headerHandle, bodyHandle, resolve, reject are PHP resources/callables; // stored as opaque PhpMixed::Null placeholders (real values live in Rust-side fields). // TODO(phase-b): wire handle/closure storage properly. @@ -1049,28 +1058,31 @@ impl CurlDownloader { ); } contents = c; - response = Some(CurlResponse::new( - { - let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); - m.insert( - "url".to_string(), - PhpMixed::String( - job.get("url") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(), - ), - ); - m - }, - status_code, - headers.clone().unwrap_or_default(), - contents.as_string().map(|s| s.to_string()), - progress - .iter() - .map(|(k, v)| (k.clone(), (**v).clone())) - .collect(), - )); + response = Some( + CurlResponse::new( + { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + m.insert( + "url".to_string(), + PhpMixed::String( + job.get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(), + ), + ); + m + }, + status_code, + headers.clone().unwrap_or_default(), + contents.as_string().map(|s| s.to_string()), + progress + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(), + )? + .map_err(|e| anyhow::anyhow!(e.message))?, + ); self.io.write_error3( &format!( "[{}] {}", @@ -1125,28 +1137,31 @@ impl CurlDownloader { ); } - response = Some(CurlResponse::new( - { - let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); - m.insert( - "url".to_string(), - PhpMixed::String( - job.get("url") - .and_then(|v| v.as_string()) - .unwrap_or("") - .to_string(), - ), - ); - m - }, - status_code, - headers.clone().unwrap_or_default(), - contents.as_string().map(|s| s.to_string()), - progress - .iter() - .map(|(k, v)| (k.clone(), (**v).clone())) - .collect(), - )); + response = Some( + CurlResponse::new( + { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + m.insert( + "url".to_string(), + PhpMixed::String( + job.get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(), + ), + ); + m + }, + status_code, + headers.clone().unwrap_or_default(), + contents.as_string().map(|s| s.to_string()), + progress + .iter() + .map(|(k, v)| (k.clone(), (**v).clone())) + .collect(), + )? + .map_err(|e| anyhow::anyhow!(e.message))?, + ); self.io.write_error3( &format!( "[{}] {}", @@ -1404,13 +1419,13 @@ impl CurlDownloader { // $curlHandle = $this->jobs[$i]['curlHandle']; // $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); let progress_now = array_diff_key( - &match curl_getinfo(/* TODO real handle */ &curl_init()) { - PhpMixed::Array(a) => a, + match curl_getinfo(/* TODO real handle */ &curl_init()) { + PhpMixed::Array(a) => a.into_iter().map(|(k, v)| (k, *v)).collect(), _ => IndexMap::new(), }, &time_info_static() .into_iter() - .map(|(k, v)| (k, Box::new(PhpMixed::Bool(v)))) + .map(|(k, v)| (k, PhpMixed::Bool(v))) .collect(), ); @@ -1424,12 +1439,17 @@ impl CurlDownloader { PhpMixed::Array(a) => a.clone(), _ => IndexMap::new(), }; + let progress_now_boxed: IndexMap<String, Box<PhpMixed>> = progress_now + .clone() + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(); - if !maps_equal(&prev_progress_map, &progress_now) { + if !maps_equal(&prev_progress_map, &progress_now_boxed) { if let Some(job) = self.jobs.get_mut(&i) { job.insert( "progress".to_string(), - PhpMixed::Array(progress_now.clone()), + PhpMixed::Array(progress_now_boxed.clone()), ); } @@ -1516,10 +1536,10 @@ impl CurlDownloader { sprintf( "IP \"%s\" is blocked for \"%s\".", &[ - (**primary_ip).clone(), + primary_ip.clone(), progress_now .get("url") - .map(|b| (**b).clone()) + .cloned() .unwrap_or(PhpMixed::Null), ], ), @@ -1581,7 +1601,7 @@ impl CurlDownloader { ), &format!("\\1{}", location_header), job_url, - ); + )?; } else { // Relative path; e.g. foo // This actually differs from PHP which seems to add duplicate slashes. @@ -1590,7 +1610,7 @@ impl CurlDownloader { r"{^(.+/)[^/?]*(?:\?.*)?$}", &format!("\\1{}", location_header), job_url, - ); + )?; } } } @@ -1649,18 +1669,20 @@ impl CurlDownloader { .and_then(|b| b.as_bool()) .unwrap_or(false) { + let status_message = response.inner.get_status_message(); + let body = response.inner.get_body().map(|s| s.to_string()); let result = self.auth_helper.prompt_auth_if_needed( job.get("url").and_then(|v| v.as_string()).unwrap_or(""), job.get("origin").and_then(|v| v.as_string()).unwrap_or(""), response.inner.get_status_code(), - response.inner.get_status_message(), + status_message.as_deref(), response.inner.get_headers().clone(), job.get("attributes") .and_then(|v| v.as_array()) .and_then(|a| a.get("retries")) .and_then(|b| b.as_int()) .unwrap_or(0), - response.inner.get_body().map(|s| s.to_string()), + body.as_deref(), )?; if result.retry { @@ -1689,7 +1711,7 @@ impl CurlDownloader { .inner .get_header("content-type") .unwrap_or_default(), - ) + )? { needs_auth_retry = Some("Bitbucket requires authentication and it was not provided"); } diff --git a/crates/shirabe/src/util/http/proxy_manager.rs b/crates/shirabe/src/util/http/proxy_manager.rs index 2576dcb..13a9920 100644 --- a/crates/shirabe/src/util/http/proxy_manager.rs +++ b/crates/shirabe/src/util/http/proxy_manager.rs @@ -14,7 +14,7 @@ pub struct ProxyManager { error: Option<String>, http_proxy: Option<ProxyItem>, https_proxy: Option<ProxyItem>, - no_proxy_handler: Option<NoProxyPattern>, + no_proxy_handler: std::cell::RefCell<Option<NoProxyPattern>>, } impl ProxyManager { @@ -23,7 +23,7 @@ impl ProxyManager { error: None, http_proxy: None, https_proxy: None, - no_proxy_handler: None, + no_proxy_handler: std::cell::RefCell::new(None), }; if let Err(e) = instance.get_proxy_data() { instance.error = Some(e.to_string()); @@ -102,7 +102,7 @@ impl ProxyManager { let (env, _name) = Self::get_proxy_env("no_proxy"); if let Some(env) = env { - self.no_proxy_handler = Some(NoProxyPattern::new(&env)); + *self.no_proxy_handler.borrow_mut() = Some(NoProxyPattern::new(&env)); } Ok(()) @@ -120,7 +120,7 @@ impl ProxyManager { } fn no_proxy(&self, request_url: &str) -> bool { - match &self.no_proxy_handler { + match self.no_proxy_handler.borrow_mut().as_mut() { None => false, Some(handler) => handler.test(request_url).unwrap_or(false), } diff --git a/crates/shirabe/src/util/http/response.rs b/crates/shirabe/src/util/http/response.rs index 62458d6..ef86ec9 100644 --- a/crates/shirabe/src/util/http/response.rs +++ b/crates/shirabe/src/util/http/response.rs @@ -82,8 +82,16 @@ impl Response { let mut value = None; let pattern = format!("(?i)^{}:\\s*(.+?)\\s*$", preg_quote(name, None)); for header in headers { - if let Some(m) = Preg::match_(&pattern, header) { - value = Some(m[1].clone()); + let mut matches: indexmap::IndexMap< + shirabe_external_packages::composer::pcre::preg::CaptureKey, + String, + > = indexmap::IndexMap::new(); + if Preg::match3(&pattern, header, Some(&mut matches)).unwrap_or(false) { + if let Some(s) = matches + .get(&shirabe_external_packages::composer::pcre::preg::CaptureKey::ByIndex(1)) + { + value = Some(s.clone()); + } } } value @@ -94,7 +102,16 @@ impl Response { todo!() } - pub fn new_fake(_body: Option<String>) -> Self { + pub fn to_php_mixed(&self) -> PhpMixed { + todo!() + } + + pub fn new_fake( + _url: &str, + _code: i64, + _headers: IndexMap<String, PhpMixed>, + _body: String, + ) -> Self { todo!() } } diff --git a/crates/shirabe/src/util/http_downloader.rs b/crates/shirabe/src/util/http_downloader.rs index 71385ce..b57626d 100644 --- a/crates/shirabe/src/util/http_downloader.rs +++ b/crates/shirabe/src/util/http_downloader.rs @@ -55,7 +55,6 @@ pub struct HttpDownloader { allow_async: bool, } -#[derive(Debug)] struct Job { id: i64, status: i64, @@ -69,6 +68,21 @@ struct Job { exception: Option<anyhow::Error>, } +impl std::fmt::Debug for Job { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Job") + .field("id", &self.id) + .field("status", &self.status) + .field("request", &self.request) + .field("sync", &self.sync) + .field("origin", &self.origin) + .field("curl_id", &self.curl_id) + .field("response", &self.response) + .field("exception", &self.exception) + .finish() + } +} + #[derive(Debug, Clone)] struct Request { url: String, @@ -99,28 +113,12 @@ impl HttpDownloader { // The cafile option can be set via config.json let mut self_options: IndexMap<String, PhpMixed> = IndexMap::new(); if disable_tls == false { - self_options = StreamContextFactory::get_tls_defaults(&options, Some(&*io)); + self_options = + StreamContextFactory::get_tls_defaults(&options, Some(&*io)).unwrap_or_default(); } // handle the other externally set options normally. - self_options = array_replace_recursive( - PhpMixed::Array( - self_options - .into_iter() - .map(|(k, v)| (k, Box::new(v))) - .collect(), - ), - PhpMixed::Array( - options - .clone() - .into_iter() - .map(|(k, v)| (k, Box::new(v))) - .collect(), - ), - ) - .as_array() - .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()) - .unwrap_or_default(); + self_options = array_replace_recursive(self_options, options.clone()); let curl = if Self::is_curl_enabled() { Some(CurlDownloader::new( @@ -138,11 +136,16 @@ impl HttpDownloader { std::rc::Rc::clone(&config), options.clone(), disable_tls, + None, )); let mut max_jobs: i64 = 12; let max_jobs_env = Platform::get_env("COMPOSER_MAX_PARALLEL_HTTP"); - if is_numeric(&max_jobs_env) { + let max_jobs_env_mixed = match &max_jobs_env { + Some(s) => PhpMixed::String(s.clone()), + None => PhpMixed::Bool(false), + }; + if is_numeric(&max_jobs_env_mixed) { max_jobs = max( 1, min( @@ -283,19 +286,7 @@ impl HttpDownloader { /// Merges new options pub fn set_options(&mut self, options: IndexMap<String, PhpMixed>) { - self.options = array_replace_recursive( - PhpMixed::Array( - self.options - .clone() - .into_iter() - .map(|(k, v)| (k, Box::new(v))) - .collect(), - ), - PhpMixed::Array(options.into_iter().map(|(k, v)| (k, Box::new(v))).collect()), - ) - .as_array() - .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()) - .unwrap_or_default(); + self.options = array_replace_recursive(self.options.clone(), options); } /// @phpstan-param Request $request @@ -305,25 +296,7 @@ impl HttpDownloader { mut request: Request, sync: bool, ) -> Result<(JobHandle, Box<dyn PromiseInterface>)> { - request.options = array_replace_recursive( - PhpMixed::Array( - self.options - .clone() - .into_iter() - .map(|(k, v)| (k, Box::new(v))) - .collect(), - ), - PhpMixed::Array( - request - .options - .into_iter() - .map(|(k, v)| (k, Box::new(v))) - .collect(), - ), - ) - .as_array() - .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()) - .unwrap_or_default(); + request.options = array_replace_recursive(self.options.clone(), request.options); let id = self.id_gen; self.id_gen += 1; @@ -381,20 +354,21 @@ impl HttpDownloader { // TODO(phase-b): build resolver/canceler closures bound to &mut self.jobs; needs Rc<RefCell> wiring let _ = (&self.rfs, &self.curl); - let resolver: Box<dyn Fn(_, _)> = Box::new(|_resolve, _reject| { - // TODO(phase-b) - }); + let resolver: Box<dyn Fn(Box<dyn Fn(PhpMixed)>, Box<dyn Fn(PhpMixed)>)> = + Box::new(|_resolve, _reject| { + // TODO(phase-b) + }); let canceler: Box<dyn Fn()> = Box::new(|| { // PHP canceler logic — TODO(phase-b) let _ = IrrecoverableDownloadException(shirabe_php_shim::RuntimeException { message: "Download canceled".to_string(), code: 0, }); - let _ = Url::sanitize(""); + let _ = Url::sanitize(String::new()); }); let _ = (resolver, canceler); - let promise = Promise::new(Box::new(|_resolve, _reject| {}), Box::new(|| {})); + let promise = Promise::new(Box::new(|_resolve, _reject| {})); // TODO(phase-b): wire promise.then() side-effects: mark job done & store response/exception let promise: Box<dyn PromiseInterface> = Box::new(promise); @@ -464,17 +438,17 @@ impl HttpDownloader { if has_if_modified_since { let mut req_map: IndexMap<String, PhpMixed> = IndexMap::new(); req_map.insert("url".to_string(), PhpMixed::String(url.clone())); - let _ = Response::new(req_map, 304, IndexMap::new(), String::new()); + let _ = Response::new(req_map, Some(304), Vec::new(), Some(String::new())); // job.resolve(response) — TODO(phase-b) } else { let mut e = TransportException::new( format!( "Network disabled, request canceled: {}", - Url::sanitize(&url) + Url::sanitize(url.clone()) ), 499, ); - e.set_status_code(499); + e.set_status_code(Some(499)); // job.reject(e) — TODO(phase-b) let _ = e; } @@ -597,12 +571,12 @@ impl HttpDownloader { url: &str, data: &IndexMap<String, PhpMixed>, ) -> Result<()> { - let clean_message = |msg: &str| -> String { + let clean_message = |msg: &str| -> anyhow::Result<String> { if !io.is_decorated() { return Preg::replace(&format!("{{{}{}}}u", chr(27), "\\[[;\\d]*m"), "", msg); } - msg.to_string() + Ok(msg.to_string()) }; // legacy warning/info keys @@ -633,8 +607,8 @@ impl HttpDownloader { "<{tp}>{capitalized} from {url}: {msg}</{tp}>", tp = r#type, capitalized = ucfirst(r#type), - url = Url::sanitize(url), - msg = clean_message(entry.unwrap().as_string().unwrap_or("")) + url = Url::sanitize(url.to_string()), + msg = clean_message(entry.unwrap().as_string().unwrap_or(""))? )); } @@ -669,13 +643,13 @@ impl HttpDownloader { "<{tp}>{capitalized} from {url}: {msg}</{tp}>", tp = r#type, capitalized = ucfirst(&r#type), - url = Url::sanitize(url), + url = Url::sanitize(url.to_string()), msg = clean_message( spec_map .get("message") .and_then(|v| v.as_string()) .unwrap_or("") - ) + )? )); } } @@ -711,11 +685,9 @@ impl HttpDownloader { ); http_map.insert("ignore_errors".to_string(), Box::new(PhpMixed::Bool(true))); ctx_options.insert("http".to_string(), PhpMixed::Array(http_map)); - let test_connectivity = file_get_contents( - "https://8.8.8.8", - false, - Some(stream_context_create(ctx_options)), - ); + // TODO(phase-b): file_get_contents only takes a path; stream context arg dropped. + let _ = stream_context_create(&ctx_options, None); + let test_connectivity = file_get_contents("https://8.8.8.8"); Silencer::restore(); if test_connectivity.is_some() { return Some(vec![ diff --git a/crates/shirabe/src/util/loop.rs b/crates/shirabe/src/util/loop.rs index 9ffee8f..9f8493b 100644 --- a/crates/shirabe/src/util/loop.rs +++ b/crates/shirabe/src/util/loop.rs @@ -8,7 +8,6 @@ use shirabe_external_packages::react::promise::promise_interface::PromiseInterfa use shirabe_external_packages::symfony::component::console::helper::progress_bar::ProgressBar; use shirabe_php_shim::microtime; -#[derive(Debug)] pub struct Loop { http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, process_executor: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, @@ -16,6 +15,16 @@ pub struct Loop { wait_index: i64, } +impl std::fmt::Debug for Loop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Loop") + .field("http_downloader", &self.http_downloader) + .field("process_executor", &self.process_executor) + .field("wait_index", &self.wait_index) + .finish() + } +} + impl Loop { pub fn new( http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, @@ -49,15 +58,17 @@ impl Loop { pub fn wait( &mut self, promises: Vec<Box<dyn PromiseInterface>>, - progress: Option<&mut ProgressBar>, + mut progress: Option<&mut ProgressBar>, ) -> Result<()> { - let mut uncaught: Option<anyhow::Error> = None; + let uncaught: Option<anyhow::Error> = None; - shirabe_external_packages::react::promise::all(&promises).then( - || {}, - |e: anyhow::Error| { - uncaught = Some(e); - }, + // TODO(phase-b): Promise::then captures uncaught by Fn; needs a Cell/RefCell wrapper + // and a thunk that matches FnOnce(Option<PhpMixed>) -> Option<PhpMixed>. + let _ = shirabe_external_packages::react::promise::all( + promises + .iter() + .map(|_| todo!("clone Box<dyn PromiseInterface>")) + .collect(), ); // keep track of every group of promises that is waited on, so abortJobs can @@ -66,13 +77,13 @@ impl Loop { self.wait_index += 1; self.current_promises.insert(wait_index, promises); - if let Some(ref progress) = progress { + if let Some(ref mut progress) = progress { let mut total_jobs: i64 = 0; total_jobs += self.http_downloader.borrow_mut().count_active_jobs(None); if let Some(ref pe) = self.process_executor { total_jobs += pe.borrow_mut().count_active_jobs(None); } - progress.start(total_jobs); + progress.start(Some(total_jobs)); } let mut last_update: f64 = 0.0; @@ -84,10 +95,11 @@ impl Loop { active_jobs += pe.borrow_mut().count_active_jobs(None); } - if let Some(ref progress) = progress { + if let Some(ref mut progress) = progress { if microtime(true) - last_update > 0.1 { last_update = microtime(true); - progress.set_progress(progress.get_max_steps() - active_jobs); + let new_progress = progress.get_max_steps() - active_jobs; + progress.set_progress(new_progress); } } @@ -97,7 +109,7 @@ impl Loop { } // as we skip progress updates if they are too quick, make sure we do one last one here at 100% - if let Some(ref progress) = progress { + if let Some(ref mut progress) = progress { progress.finish(); } @@ -111,9 +123,9 @@ impl Loop { pub fn abort_jobs(&self) { for promise_group in self.current_promises.values() { - for promise in promise_group { - // to support react/promise 2.x we wrap the promise in a resolve() call for safety - shirabe_external_packages::react::promise::resolve(Some(promise)).cancel(); + for _promise in promise_group { + // TODO(phase-b): cancel requires CancellablePromiseInterface; PromiseInterface trait + // doesn't expose it. Drop the wrap+cancel until we have the right trait. } } } diff --git a/crates/shirabe/src/util/perforce.rs b/crates/shirabe/src/util/perforce.rs index d4d39ea..0182437 100644 --- a/crates/shirabe/src/util/perforce.rs +++ b/crates/shirabe/src/util/perforce.rs @@ -92,7 +92,7 @@ impl Perforce { "-s".to_string(), ], &mut ignored_output, - None, + Option::<&str>::None, ) == 0 } @@ -169,7 +169,7 @@ impl Perforce { }; self.process .borrow_mut() - .execute_args(&cmd_vec, &mut self.command_result, None) + .execute_args(&cmd_vec, &mut self.command_result, ()) } pub fn get_client(&mut self) -> String { @@ -245,7 +245,8 @@ impl Perforce { /// @return non-empty-string pub fn get_p4_client_spec(&mut self) -> String { - format!("{}/{}.p4.spec", self.path, self.get_client()) + let path = self.path.clone(); + format!("{}/{}.p4.spec", path, self.get_client()) } pub fn get_user(&self) -> Option<String> { @@ -391,12 +392,7 @@ impl Perforce { self.generate_p4_command(vec!["client".to_string(), "-i".to_string()], true); let mut process = Process::new( - PhpMixed::List( - p4_create_client_command - .into_iter() - .map(|s| Box::new(PhpMixed::String(s))) - .collect(), - ), + p4_create_client_command, None, None, file_get_contents(&self.get_p4_client_spec()), @@ -546,18 +542,7 @@ impl Perforce { pub fn windows_login(&mut self, password: Option<&str>) -> i64 { let command = self.generate_p4_command(vec!["login".to_string(), "-a".to_string()], true); - let mut process = Process::new( - PhpMixed::List( - command - .into_iter() - .map(|s| Box::new(PhpMixed::String(s))) - .collect(), - ), - None, - None, - password.map(|s| s.to_string()), - None, - ); + let mut process = Process::new(command, None, None, password.map(|s| s.to_string()), None); process.run(None) } @@ -572,18 +557,7 @@ impl Perforce { let command = self.generate_p4_command(vec!["login".to_string(), "-a".to_string()], false); - let mut process = Process::new( - PhpMixed::List( - command - .into_iter() - .map(|s| Box::new(PhpMixed::String(s))) - .collect(), - ), - None, - None, - password, - None, - ); + let mut process = Process::new(command, None, None, password, None); process.run(None); if !process.is_successful() { @@ -717,8 +691,9 @@ impl Perforce { let branch = Preg::replace( r"/[^A-Za-z0-9 ]/", "", - res_bits.get(4).cloned().unwrap_or_default(), - ); + &res_bits.get(4).cloned().unwrap_or_default(), + ) + .unwrap_or_default(); possible_branches.insert(branch, res_bits.get(1).cloned().unwrap_or_default()); } } @@ -872,7 +847,7 @@ impl Perforce { .get_or_init(|| { let finder = ExecutableFinder::new(); finder - .find("p4", None, vec![]) + .find("p4", None, &[]) .unwrap_or_else(|| "p4".to_string()) }) .clone() diff --git a/crates/shirabe/src/util/platform.rs b/crates/shirabe/src/util/platform.rs index 1f684d8..64b3ae7 100644 --- a/crates/shirabe/src/util/platform.rs +++ b/crates/shirabe/src/util/platform.rs @@ -401,11 +401,11 @@ impl Platform { // TODO(phase-b): PHP_OS_FAMILY constant comparison && true { - let process = ProcessExecutor::new(); + let mut process = ProcessExecutor::new(None); // TODO(phase-b): inner Result for catch(\Exception); use anyhow::Result<Result<_, _>> let mut output = String::new(); let result: Result<()> = (|| { - if process.execute(&["lsmod"], &mut output)? == 0 + if process.execute_args(&["lsmod".to_string()], &mut output, ()) == 0 && shirabe_php_shim::str_contains(&output, "vboxguest") { *cached = Some(true); @@ -431,4 +431,26 @@ impl Platform { "/dev/null".to_string() } + + /// PHP: PHP_OS — returns the OS PHP was built on. + pub fn php_os() -> &'static str { + // TODO(phase-b): map to actual OS name (e.g. "Darwin", "Linux", "WINNT"). + todo!() + } + + /// PHP: rename($from, $to) — wrap the std rename so callers can use Platform::rename. + pub fn rename(from: &str, to: &str) -> bool { + std::fs::rename(from, to).is_ok() + } + + /// PHP: mkdir($pathname, $mode, $recursive) + pub fn mkdir(pathname: &str, _mode: u32, recursive: bool) -> bool { + // TODO(phase-b): honor mode bits on Unix + let result = if recursive { + std::fs::create_dir_all(pathname) + } else { + std::fs::create_dir(pathname) + }; + result.is_ok() + } } diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs index d0410ff..41cb9f1 100644 --- a/crates/shirabe/src/util/process_executor.rs +++ b/crates/shirabe/src/util/process_executor.rs @@ -49,7 +49,6 @@ pub struct ProcessExecutor { allow_async: bool, } -#[derive(Debug)] struct Job { id: i64, status: i64, @@ -60,6 +59,18 @@ struct Job { reject: Option<Box<dyn Fn(PhpMixed) + Send + Sync>>, } +impl std::fmt::Debug for Job { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Job") + .field("id", &self.id) + .field("status", &self.status) + .field("command", &self.command) + .field("cwd", &self.cwd) + .field("process", &self.process) + .finish() + } +} + impl ProcessExecutor { const STATUS_QUEUED: i64 = 1; const STATUS_STARTED: i64 = 2; @@ -79,11 +90,11 @@ impl ProcessExecutor { const GIT_CMDS_NEED_GIT_DIR: &'static [&'static [&'static str]] = &[&["show"], &["log"], &["branch"], &["remote", "set-url"]]; - pub fn new(io: Option<Box<dyn IOInterface>>) -> Self { + pub fn new<I: IntoProcessExecutorIo>(io: I) -> Self { let mut this = Self { capture_output: false, error_output: String::new(), - io, + io: io.into_process_executor_io(), jobs: IndexMap::new(), running_jobs: 0, max_jobs: 10, @@ -101,30 +112,42 @@ impl ProcessExecutor { /// if a callable is passed it will be used as output handler /// @param null|string $cwd the working directory /// @return int statuscode - pub fn execute( - &mut self, - command: PhpMixed, - output: Option<&mut PhpMixed>, - cwd: Option<&str>, - ) -> Result<i64> { + pub fn execute<'o, C, O, W>(&mut self, command: C, output: O, cwd: W) -> Result<i64> + where + C: IntoExecCommand, + O: IntoExecOutput<'o>, + W: IntoExecCwd, + { + let command = command.into_exec_command(); + let mut output = output.into_exec_output(); + let cwd_storage; + let cwd_ref: Option<&str> = match cwd.into_exec_cwd() { + Some(s) => { + cwd_storage = s; + Some(cwd_storage.as_str()) + } + None => None, + }; // PHP: func_num_args() > 1 - let has_output_arg = output.is_some(); - if has_output_arg { - return self.do_execute(command, cwd, false, output); - } - - self.do_execute(command, cwd, false, None) + let has_output_arg = output.has_output(); + let rc = if has_output_arg { + let mut buf = PhpMixed::Null; + let result = self.do_execute(command, cwd_ref, false, Some(&mut buf))?; + output.write_back(buf); + result + } else { + self.do_execute(command, cwd_ref, false, None)? + }; + Ok(rc) } /// Convenience wrapper used by phase-A code that calls /// `process.execute(&[String], &mut String, Option<&str>) == 0`. /// Forwards to `execute`, returning the status code (0 on Err for compatibility). - pub fn execute_args<C: AsRef<str>>( - &mut self, - command: &[String], - output: &mut String, - cwd: Option<C>, - ) -> i64 { + pub fn execute_args<W>(&mut self, command: &[String], output: &mut String, cwd: W) -> i64 + where + W: IntoExecCwd, + { let cmd = PhpMixed::List( command .iter() @@ -132,19 +155,39 @@ impl ProcessExecutor { .collect(), ); let mut buf = PhpMixed::String(String::new()); - let cwd_str: Option<&str> = cwd.as_ref().map(|s| s.as_ref()); - let rc = self.execute(cmd, Some(&mut buf), cwd_str).unwrap_or(1); + let cwd_storage; + let cwd_ref: Option<&str> = match cwd.into_exec_cwd() { + Some(s) => { + cwd_storage = s; + Some(cwd_storage.as_str()) + } + None => None, + }; + let rc = self.execute(cmd, Some(&mut buf), cwd_ref).unwrap_or(1); *output = buf.as_string().unwrap_or("").to_string(); rc } /// runs a process on the commandline in TTY mode - pub fn execute_tty(&mut self, command: PhpMixed, cwd: Option<&str>) -> Result<i64> { + pub fn execute_tty<C, W>(&mut self, command: C, cwd: W) -> Result<i64> + where + C: IntoExecCommand, + W: IntoExecCwd, + { + let command = command.into_exec_command(); + let cwd_storage; + let cwd_ref: Option<&str> = match cwd.into_exec_cwd() { + Some(s) => { + cwd_storage = s; + Some(cwd_storage.as_str()) + } + None => None, + }; if Platform::is_tty(None) { - return self.do_execute(command, cwd, true, None); + return self.do_execute(command, cwd_ref, true, None); } - self.do_execute(command, cwd, false, None) + self.do_execute(command, cwd_ref, false, None) } /// @param string|non-empty-list<string> $command @@ -171,7 +214,7 @@ impl ProcessExecutor { let m1 = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); command_str = substr_replace( &command_str, - &Self::escape(PhpMixed::String(Self::get_executable(&m1))), + &Self::escape(&Self::get_executable(&m1)), 0, strlen(&m1) as usize, ); @@ -183,7 +226,7 @@ impl ProcessExecutor { cwd, env.clone(), None, - Self::get_timeout(), + Some(Self::get_timeout() as f64), ); } else if let PhpMixed::List(ref list) = command { let mut cmd_vec: Vec<String> = list @@ -195,7 +238,13 @@ impl ProcessExecutor { cmd_vec[0] = Self::get_executable(&cmd_vec[0]); } - process = Process::new(cmd_vec, cwd, env, None, Self::get_timeout()); + process = Process::new( + cmd_vec, + cwd.map(String::from), + env, + None, + Some(Self::get_timeout() as f64), + ); } else { return Err(LogicException { message: "Invalid command type".to_string(), @@ -214,7 +263,8 @@ impl ProcessExecutor { } } - let _callback: Box<dyn Fn(&str, &str)> = if is_callable(output.as_deref().cloned()) { + let output_is_callable = output.as_deref().map(|o| is_callable(o)).unwrap_or(false); + let _callback: Box<dyn Fn(&str, &str)> = if output_is_callable { // TODO(phase-b): adapt output PhpMixed callable to closure Box::new(|_t: &str, _b: &str| {}) } else { @@ -226,9 +276,9 @@ impl ProcessExecutor { let io_for_signal = self.io.as_ref().map(|b| &**b as *const dyn IOInterface); let signal_handler = SignalHandler::create( vec![ - SignalHandler::SIGINT, - SignalHandler::SIGTERM, - SignalHandler::SIGHUP, + SignalHandler::SIGINT.to_string(), + SignalHandler::SIGTERM.to_string(), + SignalHandler::SIGHUP.to_string(), ], Box::new(move |signal: String, _h: &SignalHandler| { if let Some(io_ptr) = io_for_signal { @@ -242,9 +292,11 @@ impl ProcessExecutor { ); let result: Result<()> = (|| -> Result<()> { - process.run(/* callback */ Box::new(|_t: &str, _b: &str| {}))?; + let _ = process.run(/* callback */ Some(Box::new(|_t: &str, _b: &str| {}))); - if self.capture_output && !is_callable(output.as_deref().cloned()) { + let output_is_callable_inner = + output.as_deref().map(|o| is_callable(o)).unwrap_or(false); + if self.capture_output && !output_is_callable_inner { if let Some(out) = output.as_mut() { **out = PhpMixed::String(process.get_output()); } @@ -323,11 +375,13 @@ impl ProcessExecutor { } /// starts a process on the commandline in async mode - pub fn execute_async( - &mut self, - command: PhpMixed, - cwd: Option<&str>, - ) -> Result<Box<dyn PromiseInterface>> { + pub fn execute_async<C, W>(&mut self, command: C, cwd: W) -> Result<Box<dyn PromiseInterface>> + where + C: IntoExecCommand, + W: IntoExecCwd, + { + let command = command.into_exec_command(); + let cwd_opt = cwd.into_exec_cwd(); if !self.allow_async { return Err(LogicException { message: "You must use the ProcessExecutor instance which is part of a Composer\\Loop instance to be able to run async processes".to_string(), @@ -342,14 +396,15 @@ impl ProcessExecutor { id, status: Self::STATUS_QUEUED, command, - cwd: cwd.map(String::from), + cwd: cwd_opt, process: None, resolve: None, reject: None, }; // TODO(phase-b): build resolver/canceler closures bound to &mut self.jobs - let resolver: Box<dyn Fn(_, _)> = Box::new(|_resolve, _reject| {}); + let resolver: Box<dyn Fn(Option<PhpMixed>, Option<PhpMixed>)> = + Box::new(|_resolve, _reject| {}); let canceler: Box<dyn Fn()> = Box::new(|| { if defined("SIGINT") { // job.process.signal(SIGINT) @@ -358,7 +413,7 @@ impl ProcessExecutor { }); let _ = (resolver, canceler); - let promise = Promise::new(Box::new(|_resolve, _reject| {}), Box::new(|| {})); + let promise = Promise::new(Box::new(|_resolve, _reject| {})); // TODO(phase-b): wire promise.then() side-effects: mark job done & update status let promise: Box<dyn PromiseInterface> = Box::new(promise); @@ -421,17 +476,17 @@ impl ProcessExecutor { cwd.as_deref(), None, None, - Self::get_timeout(), + Some(Self::get_timeout() as f64), )) } else if let PhpMixed::List(ref list) = command { Ok(Process::new( list.iter() .map(|v| v.as_string().unwrap_or("").to_string()) .collect(), - cwd.as_deref(), + cwd.clone(), None, None, - Self::get_timeout(), + Some(Self::get_timeout() as f64), )) } else { Err(LogicException { @@ -441,7 +496,7 @@ impl ProcessExecutor { .into()) } })(); - let mut process = match process_result { + let process = match process_result { Ok(p) => p, Err(_e) => { // job.reject(e) — TODO(phase-b) @@ -450,12 +505,14 @@ impl ProcessExecutor { }; if let Some(job) = self.jobs.get_mut(&id) { - job.process = Some(process.clone()); + job.process = Some(process); } - if let Err(_e) = process.start() { - // job.reject(e) — TODO(phase-b) - return; + // PHP: $process->start($callback); — we operate on the stored job.process directly + if let Some(job) = self.jobs.get_mut(&id) { + if let Some(p) = job.process.as_mut() { + p.start(None); + } } } @@ -465,7 +522,11 @@ impl ProcessExecutor { pub fn reset_max_jobs(&mut self) { let max_jobs_env = Platform::get_env("COMPOSER_MAX_PARALLEL_PROCESSES"); - if is_numeric(&max_jobs_env) { + let max_jobs_env_mixed = match &max_jobs_env { + Some(s) => PhpMixed::String(s.clone()), + None => PhpMixed::Null, + }; + if is_numeric(&max_jobs_env_mixed) { self.max_jobs = max( 1, min( @@ -568,13 +629,13 @@ impl ProcessExecutor { } /// @return string[] - pub fn split_lines(&self, output: Option<&str>) -> Vec<String> { - let output = trim(output.unwrap_or(""), None); + pub fn split_lines(&self, output: &str) -> Vec<String> { + let output = trim(output, None); if output.is_empty() { vec![] } else { - Preg::split(r"{\r?\n}", &output) + Preg::split(r"{\r?\n}", &output).unwrap_or_default() } } @@ -589,12 +650,12 @@ impl ProcessExecutor { } /// @param int $timeout the timeout in seconds - pub fn set_timeout(timeout: i64) { - *TIMEOUT.lock().unwrap() = timeout; + pub fn set_timeout<T: ToTimeoutSeconds>(timeout: T) { + *TIMEOUT.lock().unwrap() = timeout.to_timeout_seconds(); } /// Escapes a string to be used as a shell argument. - pub fn escape(argument: PhpMixed) -> String { + pub fn escape(argument: &str) -> String { Self::escape_argument(argument) } @@ -608,7 +669,7 @@ impl ProcessExecutor { command.as_string().unwrap_or("").to_string() } else if let PhpMixed::List(list) = command { let parts: Vec<String> = array_map( - |v| Self::escape(v.clone()), + |v| Self::escape(v.as_string().unwrap_or("")), &list.iter().map(|b| (**b).clone()).collect::<Vec<_>>(), ); implode(" ", &parts) @@ -617,11 +678,12 @@ impl ProcessExecutor { }; let safe_command = Preg::replace_callback( r"{://(?P<user>[^:/\s]+):(?P<password>[^@\s/]+)@}i", - |m: &IndexMap<String, String>| -> String { + |m: &IndexMap<CaptureKey, String>| -> String { + let user_key = CaptureKey::ByName("user".to_string()); // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx, github_pat_xxx) we obfuscate that if Preg::is_match( GitHub::GITHUB_TOKEN_REGEX, - m.get("user").cloned().unwrap_or_default().as_str(), + m.get(&user_key).cloned().unwrap_or_default().as_str(), ) .unwrap_or(false) { @@ -629,22 +691,24 @@ impl ProcessExecutor { } if Preg::is_match( r"{^[a-f0-9]{12,}$}", - m.get("user").cloned().unwrap_or_default().as_str(), + m.get(&user_key).cloned().unwrap_or_default().as_str(), ) .unwrap_or(false) { return "://***:***@".to_string(); } - format!("://{}:***@", m.get("user").cloned().unwrap_or_default()) + format!("://{}:***@", m.get(&user_key).cloned().unwrap_or_default()) }, &command_string, - ); + ) + .unwrap_or_default(); let safe_command = Preg::replace( r"{--password (.*[^\\]') }", "--password '***' ", &safe_command, - ); + ) + .unwrap_or_default(); self.io.as_ref().unwrap().write_error(&format!( "Executing{} command ({}): {}", if r#async { " async" } else { "" }, @@ -654,8 +718,8 @@ impl ProcessExecutor { } /// Escapes a string to be used as a shell argument for Symfony Process. - fn escape_argument(argument: PhpMixed) -> String { - let mut argument = argument.as_string().unwrap_or("").to_string(); + fn escape_argument(argument: &str) -> String { + let mut argument = argument.to_string(); if "" == argument { return escapeshellarg(&argument); } @@ -690,10 +754,10 @@ impl ProcessExecutor { // In addition to whitespace, commas need quoting to preserve paths let mut quote = strpbrk(&argument, " \t,").is_some(); - let mut dquotes: i64 = 0; + let mut dquotes: usize = 0; // PHP: Preg::replace('/(\\\\*)"/', '$1$1\\"', $argument, -1, $dquotes) - argument = - Preg::replace_with_count(r#"/(\\*)"/"#, r#"$1$1\""#, &argument, -1, &mut dquotes); + argument = Preg::replace5(r#"/(\\*)"/"#, r#"$1$1\""#, &argument, -1, &mut dquotes) + .unwrap_or_default(); let meta = dquotes > 0 || Preg::is_match(r"/%[^%]+%|![^!]+!/", &argument).unwrap_or(false); if !meta && !quote { @@ -701,12 +765,15 @@ impl ProcessExecutor { } if quote { - argument = format!("\"{}\"", Preg::replace(r"/(\\*)$/", "$1$1", &argument)); + argument = format!( + "\"{}\"", + Preg::replace(r"/(\\*)$/", "$1$1", &argument).unwrap_or_default() + ); } if meta { - argument = Preg::replace(r#"/(["^&|<>()%])/"#, "^$1", &argument); - argument = Preg::replace(r"/(!)/", "^^$1", &argument); + argument = Preg::replace(r#"/(["^&|<>()%])/"#, "^$1", &argument).unwrap_or_default(); + argument = Preg::replace(r"/(!)/", "^^$1", &argument).unwrap_or_default(); } argument @@ -761,7 +828,7 @@ impl ProcessExecutor { let mut executables = EXECUTABLES.lock().unwrap(); if !executables.contains_key(name) { - let path = ExecutableFinder::new().find(name, Some(name)); + let path = ExecutableFinder::new().find(name, Some(name), &[]); if let Some(p) = path { executables.insert(name.to_string(), p); } @@ -791,9 +858,266 @@ impl Clone for ProcessExecutor { } } +/// Phase B helper trait: convert various command argument forms into `PhpMixed`. +pub trait IntoExecCommand { + fn into_exec_command(self) -> PhpMixed; +} + +impl IntoExecCommand for PhpMixed { + fn into_exec_command(self) -> PhpMixed { + self + } +} + +impl IntoExecCommand for &PhpMixed { + fn into_exec_command(self) -> PhpMixed { + self.clone() + } +} + +impl IntoExecCommand for &str { + fn into_exec_command(self) -> PhpMixed { + PhpMixed::String(self.to_string()) + } +} + +impl IntoExecCommand for String { + fn into_exec_command(self) -> PhpMixed { + PhpMixed::String(self) + } +} + +impl IntoExecCommand for &String { + fn into_exec_command(self) -> PhpMixed { + PhpMixed::String(self.clone()) + } +} + +impl IntoExecCommand for Vec<String> { + fn into_exec_command(self) -> PhpMixed { + PhpMixed::List( + self.into_iter() + .map(|s| Box::new(PhpMixed::String(s))) + .collect(), + ) + } +} + +impl IntoExecCommand for &Vec<String> { + fn into_exec_command(self) -> PhpMixed { + PhpMixed::List( + self.iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ) + } +} + +impl<const N: usize> IntoExecCommand for &[&str; N] { + fn into_exec_command(self) -> PhpMixed { + PhpMixed::List( + self.iter() + .map(|s| Box::new(PhpMixed::String(s.to_string()))) + .collect(), + ) + } +} + +impl IntoExecCommand for &[&str] { + fn into_exec_command(self) -> PhpMixed { + PhpMixed::List( + self.iter() + .map(|s| Box::new(PhpMixed::String(s.to_string()))) + .collect(), + ) + } +} + +impl IntoExecCommand for &[String] { + fn into_exec_command(self) -> PhpMixed { + PhpMixed::List( + self.iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ) + } +} + +/// Phase B helper trait: write captured output back to the caller's buffer. +pub trait IntoExecOutput<'a> { + type Sink: ExecOutputSink + 'a; + fn into_exec_output(self) -> Self::Sink; +} + +pub trait ExecOutputSink { + fn has_output(&self) -> bool; + fn write_back(&mut self, value: PhpMixed); +} + +pub struct NoOutput; +impl ExecOutputSink for NoOutput { + fn has_output(&self) -> bool { + false + } + fn write_back(&mut self, _value: PhpMixed) {} +} + +pub struct PhpMixedOutput<'a>(Option<&'a mut PhpMixed>); +impl<'a> ExecOutputSink for PhpMixedOutput<'a> { + fn has_output(&self) -> bool { + self.0.is_some() + } + fn write_back(&mut self, value: PhpMixed) { + if let Some(out) = self.0.as_deref_mut() { + *out = value; + } + } +} + +pub struct StringOutput<'a>(&'a mut String); +impl<'a> ExecOutputSink for StringOutput<'a> { + fn has_output(&self) -> bool { + true + } + fn write_back(&mut self, value: PhpMixed) { + *self.0 = value.as_string().unwrap_or("").to_string(); + } +} + +impl<'a> IntoExecOutput<'a> for () { + type Sink = NoOutput; + fn into_exec_output(self) -> NoOutput { + NoOutput + } +} + +impl<'a> IntoExecOutput<'a> for Option<&'a mut PhpMixed> { + type Sink = PhpMixedOutput<'a>; + fn into_exec_output(self) -> PhpMixedOutput<'a> { + PhpMixedOutput(self) + } +} + +impl<'a> IntoExecOutput<'a> for &'a mut PhpMixed { + type Sink = PhpMixedOutput<'a>; + fn into_exec_output(self) -> PhpMixedOutput<'a> { + PhpMixedOutput(Some(self)) + } +} + +impl<'a> IntoExecOutput<'a> for &'a mut String { + type Sink = StringOutput<'a>; + fn into_exec_output(self) -> StringOutput<'a> { + StringOutput(self) + } +} + +/// Phase B helper trait: convert various cwd argument forms into `Option<String>`. +pub trait IntoExecCwd { + fn into_exec_cwd(self) -> Option<String>; +} + +impl IntoExecCwd for () { + fn into_exec_cwd(self) -> Option<String> { + None + } +} + +impl IntoExecCwd for Option<&str> { + fn into_exec_cwd(self) -> Option<String> { + self.map(|s| s.to_string()) + } +} + +impl IntoExecCwd for Option<String> { + fn into_exec_cwd(self) -> Option<String> { + self + } +} + +impl IntoExecCwd for Option<&String> { + fn into_exec_cwd(self) -> Option<String> { + self.cloned() + } +} + +impl IntoExecCwd for &str { + fn into_exec_cwd(self) -> Option<String> { + Some(self.to_string()) + } +} + +impl IntoExecCwd for String { + fn into_exec_cwd(self) -> Option<String> { + Some(self) + } +} + +impl IntoExecCwd for &String { + fn into_exec_cwd(self) -> Option<String> { + Some(self.clone()) + } +} + +/// Phase B helper: accept either `i64` or `PhpMixed` for `set_timeout`. +pub trait ToTimeoutSeconds { + fn to_timeout_seconds(self) -> i64; +} + +impl ToTimeoutSeconds for i64 { + fn to_timeout_seconds(self) -> i64 { + self + } +} + +impl ToTimeoutSeconds for PhpMixed { + fn to_timeout_seconds(self) -> i64 { + self.as_int().unwrap_or(0) + } +} + +/// Phase B helper: accept various IO forms for `ProcessExecutor::new`. +/// Note: clones the IO via `clone_box` for borrow forms; this is incidental +/// to Phase B — PHP class semantics should use Rc, but that requires broader +/// refactor. TODO(phase-b): switch to shared ownership when call sites are +/// stabilized. +pub trait IntoProcessExecutorIo { + fn into_process_executor_io(self) -> Option<Box<dyn IOInterface>>; +} + +impl IntoProcessExecutorIo for Option<Box<dyn IOInterface>> { + fn into_process_executor_io(self) -> Option<Box<dyn IOInterface>> { + self + } +} + +impl IntoProcessExecutorIo for Box<dyn IOInterface> { + fn into_process_executor_io(self) -> Option<Box<dyn IOInterface>> { + Some(self) + } +} + +impl IntoProcessExecutorIo for () { + fn into_process_executor_io(self) -> Option<Box<dyn IOInterface>> { + None + } +} + +impl IntoProcessExecutorIo for &dyn IOInterface { + fn into_process_executor_io(self) -> Option<Box<dyn IOInterface>> { + Some(self.clone_box()) + } +} + +impl IntoProcessExecutorIo for &mut dyn IOInterface { + fn into_process_executor_io(self) -> Option<Box<dyn IOInterface>> { + Some(self.clone_box()) + } +} + // Suppress unused-import warnings. #[allow(dead_code)] const _USE_PARITY: () = { - let _ = call_user_func; + let _ = call_user_func::<PhpMixed>; let _ = sprintf; }; diff --git a/crates/shirabe/src/util/remote_filesystem.rs b/crates/shirabe/src/util/remote_filesystem.rs index 7c10640..0ad5ff3 100644 --- a/crates/shirabe/src/util/remote_filesystem.rs +++ b/crates/shirabe/src/util/remote_filesystem.rs @@ -5,9 +5,11 @@ use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::preg::{CaptureKey, Preg}; use shirabe_php_shim::{ FILTER_VALIDATE_BOOLEAN, PHP_URL_HOST, PHP_URL_PATH, PHP_VERSION_ID, PhpMixed, - RuntimeException, array_replace_recursive, base64_encode, explode, extension_loaded, - file_put_contents, filter_var, ini_get, json_decode, parse_url, preg_quote, - restore_error_handler, set_error_handler, sprintf, strpos, strtolower, strtr, substr, trim, + RuntimeException, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_PROGRESS, + array_replace_recursive, base64_encode, explode, extension_loaded, file_put_contents, + filter_var, gethostbyname, http_clear_last_response_headers, http_get_last_response_headers, + ini_get, json_decode, parse_url, preg_quote, restore_error_handler, set_error_handler, sprintf, + strpos, strtolower, strtr, substr, trim, zlib_decode, }; use crate::config::Config; @@ -62,7 +64,9 @@ impl RemoteFilesystem { ) -> Self { let (computed_options, disable_tls_set) = if !disable_tls { ( - StreamContextFactory::get_tls_defaults(&options, &*io), + // TODO(phase-b): logger is None placeholder; should pass `&*io` if a Logger view is available. + StreamContextFactory::get_tls_defaults(&options, None) + .unwrap_or_else(|_| IndexMap::new()), false, ) } else { @@ -240,7 +244,7 @@ impl RemoteFilesystem { if self.degraded_mode && strpos(&file_url, "http://repo.packagist.org/") == Some(0) { file_url = format!( "http://{}{}", - Platform::gethostbyname("repo.packagist.org"), + gethostbyname("repo.packagist.org"), substr(&file_url, 20, None) ); degraded_packagist = true; @@ -253,10 +257,20 @@ impl RemoteFilesystem { } // TODO(plugin): `Closure::fromCallable([$this, 'callbackGet'])` for stream notification. - let ctx = StreamContextFactory::get_context(&file_url, &options, &IndexMap::new()); + let ctx = StreamContextFactory::get_context(&file_url, options.clone(), IndexMap::new()) + .map_err(|e| anyhow::anyhow!(e))?; - let proxy = ProxyManager::get_instance().get_proxy_for_request(&file_url); - let using_proxy = proxy.get_status(" using proxy (%s)"); + let using_proxy = { + let proxy_manager_guard = ProxyManager::get_instance().lock().unwrap(); + let proxy = proxy_manager_guard + .as_ref() + .expect("ProxyManager instance") + .get_proxy_for_request(&file_url) + .map_err(|e| anyhow::anyhow!(e))?; + proxy + .get_status(Some(" using proxy (%s)")) + .unwrap_or_default() + }; self.io.write_error3( &format!( "{}{}{}", @@ -265,7 +279,7 @@ impl RemoteFilesystem { } else { "Reading " }, - Url::sanitize(&orig_file_url), + Url::sanitize(orig_file_url.clone()), using_proxy ), true, @@ -319,7 +333,11 @@ impl RemoteFilesystem { .as_deref() .map(|s| json_decode(s, true).unwrap_or(PhpMixed::Null)) .unwrap_or(PhpMixed::Null); - HttpDownloader::output_warnings(&*self.io, origin_url, &parsed); + let parsed_map: IndexMap<String, PhpMixed> = match parsed { + PhpMixed::Array(m) => m.into_iter().map(|(k, v)| (k, *v)).collect(), + _ => IndexMap::new(), + }; + let _ = HttpDownloader::output_warnings(&*self.io, origin_url, &parsed_map); } if [401_i64, 403].contains(&code) && retry_auth_failure { @@ -342,11 +360,14 @@ impl RemoteFilesystem { if let Some(cl) = content_length { let cl_int: i64 = cl.parse().unwrap_or(0); if cl_int > 0 && Platform::strlen(result.as_deref().unwrap_or("")) < cl_int { - let mut e = TransportException::new(format!( - "Content-Length mismatch, received {} bytes out of the expected {}", - Platform::strlen(result.as_deref().unwrap_or("")), - cl_int - )); + let mut e = TransportException::new( + format!( + "Content-Length mismatch, received {} bytes out of the expected {}", + Platform::strlen(result.as_deref().unwrap_or("")), + cl_int + ), + 0, + ); e.set_headers(http_response_header.clone()); e.set_status_code(Self::find_status_code(&http_response_header)); let decoded = self @@ -407,13 +428,15 @@ impl RemoteFilesystem { self.degraded_mode = true; self.io .write_error3("", true, crate::io::io_interface::NORMAL); - self.io.write_error3(PhpMixed::List(vec![ - Box::new(PhpMixed::String(format!("<error>{}</error>", msg_owned))), - Box::new(PhpMixed::String( - "<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>" - .to_string(), - )), - ]), true, crate::io::io_interface::NORMAL); + // TODO(phase-b): PHP writeError accepts an array of lines; joined here with newline. + self.io.write_error3( + &format!( + "<error>{}</error>\n<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>", + msg_owned, + ), + true, + crate::io::io_interface::NORMAL, + ); return self.get( &self.origin_url.clone(), @@ -461,11 +484,9 @@ impl RemoteFilesystem { } } - let gitlab_domains: Vec<String> = self - .config - .borrow_mut() - .get("gitlab-domains") - .and_then(|v| v.as_list()) + let gitlab_domains_value = self.config.borrow_mut().get("gitlab-domains"); + let gitlab_domains: Vec<String> = gitlab_domains_value + .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) @@ -556,17 +577,15 @@ impl RemoteFilesystem { } self.degraded_mode = true; - self.io.write_error3(PhpMixed::List(vec![ - Box::new(PhpMixed::String("".to_string())), - Box::new(PhpMixed::String(format!( - "<error>Failed to decode response: {}</error>", - e - ))), - Box::new(PhpMixed::String( - "<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>" - .to_string(), - )), - ]), true, crate::io::io_interface::NORMAL); + // TODO(phase-b): PHP writeError accepts an array of lines; joined here with newline. + self.io.write_error3( + &format!( + "\n<error>Failed to decode response: {}</error>\n<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>", + e, + ), + true, + crate::io::io_interface::NORMAL, + ); return self.get( &self.origin_url.clone(), @@ -582,10 +601,13 @@ impl RemoteFilesystem { if result.is_some() && file_name.is_some() && !is_redirect { let result_str = result.as_deref().unwrap(); if result_str.is_empty() { - return Err(anyhow::anyhow!(TransportException::new(format!( - "\"{}\" appears broken, and returned an empty 200 response", - self.file_url - )))); + return Err(anyhow::anyhow!(TransportException::new( + format!( + "\"{}\" appears broken, and returned an empty 200 response", + self.file_url + ), + 0, + ))); } let put_error_message = String::new(); @@ -595,12 +617,15 @@ impl RemoteFilesystem { file_put_contents(file_name.as_deref().unwrap(), result_str.as_bytes()); restore_error_handler(); if write_result.is_none() { - return Err(anyhow::anyhow!(TransportException::new(format!( - "The \"{}\" file could not be written to {}: {}", - self.file_url, - file_name.as_deref().unwrap(), - put_error_message - )))); + return Err(anyhow::anyhow!(TransportException::new( + format!( + "The \"{}\" file could not be written to {}: {}", + self.file_url, + file_name.as_deref().unwrap(), + put_error_message + ), + 0, + ))); } let _ = put_error_message; } @@ -617,8 +642,10 @@ impl RemoteFilesystem { )?; if self.store_auth { - self.auth_helper - .store_auth(&self.origin_url, PhpMixed::Bool(self.store_auth)); + let _ = self.auth_helper.store_auth( + &self.origin_url, + crate::util::auth_helper::StoreAuth::Bool(self.store_auth), + ); self.store_auth = false; } @@ -642,13 +669,15 @@ impl RemoteFilesystem { self.degraded_mode = true; self.io .write_error3("", true, crate::io::io_interface::NORMAL); - self.io.write_error3(PhpMixed::List(vec![ - Box::new(PhpMixed::String(format!("<error>{}</error>", msg_owned))), - Box::new(PhpMixed::String( - "<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>" - .to_string(), - )), - ]), true, crate::io::io_interface::NORMAL); + // TODO(phase-b): PHP writeError accepts an array of lines; joined here with newline. + self.io.write_error3( + &format!( + "<error>{}</error>\n<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>", + msg_owned, + ), + true, + crate::io::io_interface::NORMAL, + ); return self.get( &self.origin_url.clone(), @@ -689,7 +718,7 @@ impl RemoteFilesystem { let mut result: Option<String> = None; if PHP_VERSION_ID >= 80400 { - Platform::http_clear_last_response_headers(); + http_clear_last_response_headers(); } let mut caught_e: Option<anyhow::Error> = None; @@ -713,8 +742,8 @@ impl RemoteFilesystem { } if PHP_VERSION_ID >= 80400 { - *response_headers = Platform::http_get_last_response_headers().unwrap_or_default(); - Platform::http_clear_last_response_headers(); + *response_headers = http_get_last_response_headers().unwrap_or_default(); + http_clear_last_response_headers(); } else { // TODO(phase-b): read the magic `$http_response_header` PHP variable. *response_headers = Vec::new(); @@ -737,7 +766,7 @@ impl RemoteFilesystem { bytes_max: i64, ) -> anyhow::Result<()> { match notification_code { - x if x == Platform::STREAM_NOTIFY_FAILURE => { + x if x == STREAM_NOTIFY_FAILURE => { if 400 == message_code { return Err(anyhow::anyhow!(TransportException::new_with_code( format!( @@ -749,10 +778,10 @@ impl RemoteFilesystem { ))); } } - x if x == Platform::STREAM_NOTIFY_FILE_SIZE_IS => { + x if x == STREAM_NOTIFY_FILE_SIZE_IS => { self.bytes_max = bytes_max; } - x if x == Platform::STREAM_NOTIFY_PROGRESS => { + x if x == STREAM_NOTIFY_PROGRESS => { if self.bytes_max > 0 && self.progress { let progression = std::cmp::min( 100_i64, @@ -784,28 +813,36 @@ impl RemoteFilesystem { reason: Option<String>, headers: Vec<String>, ) -> anyhow::Result<()> { + let file_url = self.file_url.clone(); + let origin_url = self.origin_url.clone(); let result = self.auth_helper.prompt_auth_if_needed( - &self.file_url, - &self.origin_url, + &file_url, + &origin_url, http_status, - reason, + reason.as_deref(), headers, 1, - ); + None, + )?; - self.store_auth = result.store_auth; + self.store_auth = matches!( + result.store_auth, + crate::util::auth_helper::StoreAuth::Bool(true) + | crate::util::auth_helper::StoreAuth::Prompt + ); self.retry = result.retry; if self.retry { return Err(anyhow::anyhow!(TransportException::new( - "RETRY".to_string() + "RETRY".to_string(), + 0, ))); } Ok(()) } fn get_options_for_url( - &self, + &mut self, origin_url: &str, additional_options: IndexMap<String, PhpMixed>, ) -> IndexMap<String, PhpMixed> { @@ -842,7 +879,7 @@ impl RemoteFilesystem { .as_string() .unwrap_or("") .to_string(); - let split = explode("\r\n", &trim(&header_str, "\r\n")); + let split = explode("\r\n", &trim(&header_str, Some("\r\n"))); if let Some(PhpMixed::Array(m)) = options.get_mut("http") { m.insert( "header".to_string(), @@ -855,9 +892,11 @@ impl RemoteFilesystem { ); } } + let file_url = self.file_url.clone(); options = self .auth_helper - .add_authentication_options(options, origin_url, &self.file_url); + .add_authentication_options(options, origin_url, &file_url) + .unwrap_or_else(|_| IndexMap::new()); let http_entry = options .entry("http".to_string()) @@ -941,7 +980,7 @@ impl RemoteFilesystem { "Following redirect (%u) %s", &[ PhpMixed::Int(self.redirects), - PhpMixed::String(Url::sanitize(&target_url)), + PhpMixed::String(Url::sanitize(target_url.clone())), ], ), true, @@ -968,11 +1007,14 @@ impl RemoteFilesystem { } if !self.retry { - let mut e = TransportException::new(format!( - "The \"{}\" file could not be downloaded, got redirect without Location ({})", - self.file_url, - response_headers.first().map(|s| s.as_str()).unwrap_or("") - )); + let mut e = TransportException::new( + format!( + "The \"{}\" file could not be downloaded, got redirect without Location ({})", + self.file_url, + response_headers.first().map(|s| s.as_str()).unwrap_or("") + ), + 0, + ); e.set_headers(response_headers.to_vec()); let decoded = self.decode_result(result.as_deref(), response_headers)?; e.set_response(decoded); @@ -998,13 +1040,14 @@ impl RemoteFilesystem { .unwrap_or(false); if decode { - let decoded = Platform::zlib_decode(result.as_deref().unwrap_or("")); + let decoded = zlib_decode(result.as_deref().unwrap_or("")); result = match decoded { Some(d) => Some(d), None => { return Err(anyhow::anyhow!(TransportException::new( - "Failed to decode zlib stream".to_string() + "Failed to decode zlib stream".to_string(), + 0, ))); } }; diff --git a/crates/shirabe/src/util/stream_context_factory.rs b/crates/shirabe/src/util/stream_context_factory.rs index a81a68a..783d47a 100644 --- a/crates/shirabe/src/util/stream_context_factory.rs +++ b/crates/shirabe/src/util/stream_context_factory.rs @@ -44,14 +44,14 @@ impl StreamContextFactory { ); let default_options = { let mut o = default_options; - if let Some(PhpMixed::Array(ref mut http)) = o.get_mut("http") { + if let Some(PhpMixed::Array(http)) = o.get_mut("http") { http.remove("header"); } o }; options = array_replace_recursive(options, default_options); - if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") { + if let Some(PhpMixed::Array(http)) = options.get_mut("http") { if let Some(header) = http.get("header").cloned() { let fixed = Self::fix_http_header_field(&*header); http.insert( @@ -81,7 +81,7 @@ impl StreamContextFactory { .map(|a| a.contains_key("header")) .unwrap_or(false); if !has_header { - if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") { + if let Some(PhpMixed::Array(http)) = options.get_mut("http") { http.insert("header".to_string(), Box::new(PhpMixed::List(vec![]))); } } @@ -93,7 +93,7 @@ impl StreamContextFactory { .map(|v| matches!(**v, PhpMixed::String(_))) .unwrap_or(false); if header_is_string { - if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") { + if let Some(PhpMixed::Array(http)) = options.get_mut("http") { if let Some(PhpMixed::String(header_str)) = http.get("header").map(|v| *v.clone()) { let parts: Vec<Box<PhpMixed>> = header_str .split("\r\n") @@ -118,27 +118,30 @@ impl StreamContextFactory { return Err(TransportException::new( "You must enable the openssl extension to use a secure proxy." .to_string(), + 0, )); } if is_https_request { return Err(TransportException::new( "You must enable the curl extension to make https requests through a secure proxy.".to_string(), + 0, )); } } else if is_https_request && !extension_loaded("openssl") { return Err(TransportException::new( "You must enable the openssl extension to make https requests through a proxy.".to_string(), + 0, )); } // Header will be a Proxy-Authorization string or not set let proxy_http = proxy_options.get("http"); if let Some(proxy_header) = proxy_http.and_then(|h| h.get("header")) { - if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") { - if let Some(PhpMixed::List(ref mut headers)) = + if let Some(PhpMixed::Array(http)) = options.get_mut("http") { + if let Some(PhpMixed::List(headers)) = http.get_mut("header").map(|v| &mut **v) { - headers.push(Box::new(*proxy_header.clone())); + headers.push(Box::new(proxy_header.clone())); } } } @@ -149,7 +152,7 @@ impl StreamContextFactory { let inner: IndexMap<String, Box<PhpMixed>> = v .iter() .filter(|(ik, _)| ik.as_str() != "header") - .map(|(ik, iv)| (ik.clone(), iv.clone())) + .map(|(ik, iv)| (ik.clone(), Box::new(iv.clone()))) .collect(); (k.clone(), PhpMixed::Array(inner)) }) @@ -223,10 +226,8 @@ impl StreamContextFactory { "" }, ); - if let Some(PhpMixed::Array(ref mut http)) = options.get_mut("http") { - if let Some(PhpMixed::List(ref mut headers)) = - http.get_mut("header").map(|v| &mut **v) - { + if let Some(PhpMixed::Array(http)) = options.get_mut("http") { + if let Some(PhpMixed::List(headers)) = http.get_mut("header").map(|v| &mut **v) { headers.push(Box::new(PhpMixed::String(user_agent))); } } @@ -339,11 +340,11 @@ impl StreamContextFactory { if !has_cafile && !has_capath { let result = CaBundle::get_system_ca_root_bundle_path(logger); if shirabe_php_shim::is_dir(&result) { - if let Some(PhpMixed::Array(ref mut ssl)) = defaults.get_mut("ssl") { + if let Some(PhpMixed::Array(ssl)) = defaults.get_mut("ssl") { ssl.insert("capath".to_string(), Box::new(PhpMixed::String(result))); } } else { - if let Some(PhpMixed::Array(ref mut ssl)) = defaults.get_mut("ssl") { + if let Some(PhpMixed::Array(ssl)) = defaults.get_mut("ssl") { ssl.insert("cafile".to_string(), Box::new(PhpMixed::String(result))); } } @@ -359,6 +360,7 @@ impl StreamContextFactory { if !Filesystem::is_readable(cafile) || !CaBundle::validate_ca_file(cafile, logger) { return Err(TransportException::new( "The configured cafile was not valid or could not be read.".to_string(), + 0, )); } } @@ -373,12 +375,13 @@ impl StreamContextFactory { if !shirabe_php_shim::is_dir(capath) || !Filesystem::is_readable(capath) { return Err(TransportException::new( "The configured capath was not valid or could not be read.".to_string(), + 0, )); } } // Disable TLS compression to prevent CRIME attacks where supported. - if let Some(PhpMixed::Array(ref mut ssl)) = defaults.get_mut("ssl") { + if let Some(PhpMixed::Array(ssl)) = defaults.get_mut("ssl") { ssl.insert( "disable_compression".to_string(), Box::new(PhpMixed::Bool(true)), diff --git a/crates/shirabe/src/util/svn.rs b/crates/shirabe/src/util/svn.rs index 107be1b..15809df 100644 --- a/crates/shirabe/src/util/svn.rs +++ b/crates/shirabe/src/util/svn.rs @@ -164,7 +164,7 @@ impl Svn { return Ok(output); } - let error_output = self.process.borrow().get_error_output(); + let error_output = self.process.borrow().get_error_output().to_string(); let full_output = trim( &implode("\n", &[output.clone().unwrap_or_default(), error_output]), None, @@ -430,7 +430,7 @@ impl Svn { if 0 == self.process.borrow_mut().execute_args( &["svn".to_string(), "--version".to_string()], &mut output, - None, + (), ) { let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3(r"{(\d+(?:\.\d+)+)}", &output, Some(&mut matches)) diff --git a/crates/shirabe/src/util/url.rs b/crates/shirabe/src/util/url.rs index 7dc6e4f..9ee5dc9 100644 --- a/crates/shirabe/src/util/url.rs +++ b/crates/shirabe/src/util/url.rs @@ -131,7 +131,7 @@ impl Url { .as_string_opt() .map(|s| s.to_string()) .unwrap_or_default(); - if let Some(port) = parse_url(url, PHP_URL_PORT).as_i64_opt() { + if let Some(port) = parse_url(url, PHP_URL_PORT).as_int() { origin = format!("{}:{}", origin, port); } @@ -156,7 +156,14 @@ impl Url { true, ) { - for gitlab_domain in config.get("gitlab-domains").as_vec_string() { + let gitlab_domains: Vec<String> = match config.get("gitlab-domains") { + PhpMixed::List(list) => list + .iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect(), + _ => vec![], + }; + for gitlab_domain in gitlab_domains { if !gitlab_domain.is_empty() && gitlab_domain.starts_with(&origin) { return gitlab_domain; } |
