aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/util
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-20 08:33:49 +0900
committernsfisis <nsfisis@gmail.com>2026-05-20 08:33:57 +0900
commitf31b101ce1e921a026ba234b1f0a83b0392bc118 (patch)
treeb7ac2aa84d71ebd162cc21aeab0240e7e0544988 /crates/shirabe/src/util
parent5e31fa33c3b5cf726a57a063b8e7a070869250fe (diff)
downloadphp-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')
-rw-r--r--crates/shirabe/src/util/auth_helper.rs4
-rw-r--r--crates/shirabe/src/util/bitbucket.rs87
-rw-r--r--crates/shirabe/src/util/config_validator.rs31
-rw-r--r--crates/shirabe/src/util/filesystem.rs297
-rw-r--r--crates/shirabe/src/util/forgejo.rs98
-rw-r--r--crates/shirabe/src/util/git.rs23
-rw-r--r--crates/shirabe/src/util/github.rs183
-rw-r--r--crates/shirabe/src/util/gitlab.rs35
-rw-r--r--crates/shirabe/src/util/hg.rs26
-rw-r--r--crates/shirabe/src/util/http/curl_downloader.rs146
-rw-r--r--crates/shirabe/src/util/http/proxy_manager.rs8
-rw-r--r--crates/shirabe/src/util/http/response.rs23
-rw-r--r--crates/shirabe/src/util/http_downloader.rs116
-rw-r--r--crates/shirabe/src/util/loop.rs44
-rw-r--r--crates/shirabe/src/util/perforce.rs47
-rw-r--r--crates/shirabe/src/util/platform.rs26
-rw-r--r--crates/shirabe/src/util/process_executor.rs474
-rw-r--r--crates/shirabe/src/util/remote_filesystem.rs203
-rw-r--r--crates/shirabe/src/util/stream_context_factory.rs33
-rw-r--r--crates/shirabe/src/util/svn.rs4
-rw-r--r--crates/shirabe/src/util/url.rs11
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(&note).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;
}