aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-16 13:04:59 +0900
committernsfisis <nsfisis@gmail.com>2026-05-16 13:04:59 +0900
commitc561c020e4c895ae605599b4c55d15613be82ba6 (patch)
tree5434abe9ca52174a4fc30f9604aea91ee64d3f57 /crates/shirabe
parentc03a623725c7abd6f484af22765b315e8421357c (diff)
downloadphp-shirabe-c561c020e4c895ae605599b4c55d15613be82ba6.tar.gz
php-shirabe-c561c020e4c895ae605599b4c55d15613be82ba6.tar.zst
php-shirabe-c561c020e4c895ae605599b4c55d15613be82ba6.zip
feat(port): port BaseIO.php
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/io/base_io.rs513
1 files changed, 513 insertions, 0 deletions
diff --git a/crates/shirabe/src/io/base_io.rs b/crates/shirabe/src/io/base_io.rs
index 81c3dd0..f1cff3c 100644
--- a/crates/shirabe/src/io/base_io.rs
+++ b/crates/shirabe/src/io/base_io.rs
@@ -1 +1,514 @@
//! ref: composer/src/Composer/IO/BaseIO.php
+
+use indexmap::IndexMap;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_external_packages::psr::log::log_level::LogLevel;
+use shirabe_php_shim::{
+ array_merge, in_array, json_encode_ex, PhpMixed, UnexpectedValueException,
+ JSON_INVALID_UTF8_IGNORE, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE,
+};
+use crate::config::Config;
+use crate::io::io_interface::IOInterface;
+use crate::util::process_executor::ProcessExecutor;
+use crate::util::silencer::Silencer;
+
+#[derive(Debug)]
+pub struct BaseIO {
+ pub(crate) authentications: IndexMap<String, IndexMap<String, Option<String>>>,
+}
+
+impl BaseIO {
+ pub fn get_authentications(&self) -> IndexMap<String, IndexMap<String, Option<String>>> {
+ self.authentications.clone()
+ }
+
+ pub fn reset_authentications(&mut self) {
+ self.authentications = IndexMap::new();
+ }
+
+ pub fn has_authentication(&self, repository_name: &str) -> bool {
+ self.authentications.contains_key(repository_name)
+ }
+
+ pub fn get_authentication(&self, repository_name: &str) -> IndexMap<String, Option<String>> {
+ if let Some(auth) = self.authentications.get(repository_name) {
+ return auth.clone();
+ }
+ let mut result = IndexMap::new();
+ result.insert("username".to_string(), None);
+ result.insert("password".to_string(), None);
+ result
+ }
+
+ pub fn set_authentication(
+ &mut self,
+ repository_name: String,
+ username: String,
+ password: Option<String>,
+ ) {
+ let mut auth = IndexMap::new();
+ auth.insert("username".to_string(), Some(username));
+ auth.insert("password".to_string(), password);
+ self.authentications.insert(repository_name, auth);
+ }
+
+ pub fn write_raw(&self, messages: PhpMixed, newline: bool, verbosity: i64) {
+ self.write(messages, newline, verbosity);
+ }
+
+ pub fn write_error_raw(&self, messages: PhpMixed, newline: bool, verbosity: i64) {
+ self.write_error(messages, newline, verbosity);
+ }
+
+ pub(crate) fn check_and_set_authentication(
+ &mut self,
+ repository_name: String,
+ username: String,
+ password: Option<String>,
+ ) {
+ if self.has_authentication(&repository_name) {
+ let auth = self.get_authentication(&repository_name);
+ if auth.get("username").and_then(|v| v.as_deref()) == Some(username.as_str())
+ && *auth.get("password").unwrap_or(&None) == password
+ {
+ return;
+ }
+ self.write_error(
+ PhpMixed::String(format!(
+ "<warning>Warning: You should avoid overwriting already defined auth settings for {}.</warning>",
+ repository_name
+ )),
+ true,
+ IOInterface::NORMAL,
+ );
+ }
+ self.set_authentication(repository_name, username, password);
+ }
+
+ pub fn load_configuration(&mut self, config: &mut Config) -> anyhow::Result<()> {
+ let bitbucket_oauth = config.get("bitbucket-oauth");
+ let github_oauth = config.get("github-oauth");
+ let gitlab_oauth = config.get("gitlab-oauth");
+ let gitlab_token = config.get("gitlab-token");
+ let forgejo_token = config.get("forgejo-token");
+ let http_basic = config.get("http-basic");
+ let bearer_token = config.get("bearer");
+ let custom_headers = config.get("custom-headers");
+ let client_certificate = config.get("client-certificate");
+
+ if let Some(map) = bitbucket_oauth.as_ref().and_then(|v| v.as_array()) {
+ for (domain, cred) in map.clone() {
+ if let Some(cred_map) = cred.as_array() {
+ let consumer_key = cred_map
+ .get("consumer-key")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ let consumer_secret = cred_map
+ .get("consumer-secret")
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string());
+ self.check_and_set_authentication(domain, consumer_key, consumer_secret);
+ }
+ }
+ }
+
+ if let Some(map) = github_oauth.as_ref().and_then(|v| v.as_array()) {
+ for (domain, token) in map.clone() {
+ let token_str = token.as_string().unwrap_or("").to_string();
+ let github_domains = config.get("github-domains");
+ if domain != "github.com"
+ && !in_array(
+ PhpMixed::String(domain.clone()),
+ &github_domains.clone().unwrap_or(PhpMixed::List(vec![])),
+ true,
+ )
+ {
+ self.debug(
+ PhpMixed::String(format!(
+ "{} is not in the configured github-domains, adding it implicitly as authentication is configured for this domain",
+ domain
+ )),
+ IndexMap::new(),
+ );
+ let merged = array_merge(
+ github_domains.unwrap_or(PhpMixed::List(vec![])),
+ PhpMixed::List(vec![Box::new(PhpMixed::String(domain.clone()))]),
+ );
+ let mut inner = IndexMap::new();
+ inner.insert("github-domains".to_string(), Box::new(merged));
+ let mut outer = IndexMap::new();
+ outer.insert("config".to_string(), Box::new(PhpMixed::Array(inner)));
+ config.merge(PhpMixed::Array(outer), "implicit-due-to-auth");
+ }
+
+ if !Preg::is_match(r"^[.A-Za-z0-9_]+$", &token_str).unwrap_or(false) {
+ return Err(anyhow::anyhow!(UnexpectedValueException {
+ message: format!(
+ "Your github oauth token for {} contains invalid characters: \"{}\"",
+ domain, token_str
+ ),
+ code: 0,
+ }));
+ }
+ self.check_and_set_authentication(
+ domain,
+ token_str,
+ Some("x-oauth-basic".to_string()),
+ );
+ }
+ }
+
+ if let Some(map) = gitlab_oauth.as_ref().and_then(|v| v.as_array()) {
+ for (domain, token) in map.clone() {
+ let gitlab_domains = config.get("gitlab-domains");
+ if domain != "gitlab.com"
+ && !in_array(
+ PhpMixed::String(domain.clone()),
+ &gitlab_domains.clone().unwrap_or(PhpMixed::List(vec![])),
+ true,
+ )
+ {
+ self.debug(
+ PhpMixed::String(format!(
+ "{} is not in the configured gitlab-domains, adding it implicitly as authentication is configured for this domain",
+ domain
+ )),
+ IndexMap::new(),
+ );
+ let merged = array_merge(
+ gitlab_domains.unwrap_or(PhpMixed::List(vec![])),
+ PhpMixed::List(vec![Box::new(PhpMixed::String(domain.clone()))]),
+ );
+ let mut inner = IndexMap::new();
+ inner.insert("gitlab-domains".to_string(), Box::new(merged));
+ let mut outer = IndexMap::new();
+ outer.insert("config".to_string(), Box::new(PhpMixed::Array(inner)));
+ config.merge(PhpMixed::Array(outer), "implicit-due-to-auth");
+ }
+
+ let token_str = if let Some(arr) = token.as_array() {
+ arr.get("token")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string()
+ } else {
+ token.as_string().unwrap_or("").to_string()
+ };
+ self.check_and_set_authentication(domain, token_str, Some("oauth2".to_string()));
+ }
+ }
+
+ if let Some(map) = gitlab_token.as_ref().and_then(|v| v.as_array()) {
+ for (domain, token) in map.clone() {
+ let gitlab_domains = config.get("gitlab-domains");
+ if domain != "gitlab.com"
+ && !in_array(
+ PhpMixed::String(domain.clone()),
+ &gitlab_domains.clone().unwrap_or(PhpMixed::List(vec![])),
+ true,
+ )
+ {
+ self.debug(
+ PhpMixed::String(format!(
+ "{} is not in the configured gitlab-domains, adding it implicitly as authentication is configured for this domain",
+ domain
+ )),
+ IndexMap::new(),
+ );
+ let merged = array_merge(
+ gitlab_domains.unwrap_or(PhpMixed::List(vec![])),
+ PhpMixed::List(vec![Box::new(PhpMixed::String(domain.clone()))]),
+ );
+ let mut inner = IndexMap::new();
+ inner.insert("gitlab-domains".to_string(), Box::new(merged));
+ let mut outer = IndexMap::new();
+ outer.insert("config".to_string(), Box::new(PhpMixed::Array(inner)));
+ config.merge(PhpMixed::Array(outer), "implicit-due-to-auth");
+ }
+
+ let (username, password) = if let Some(arr) = token.as_array() {
+ (
+ arr.get("username")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string(),
+ arr.get("token")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string(),
+ )
+ } else {
+ (
+ token.as_string().unwrap_or("").to_string(),
+ "private-token".to_string(),
+ )
+ };
+ self.check_and_set_authentication(domain, username, Some(password));
+ }
+ }
+
+ if let Some(map) = forgejo_token.as_ref().and_then(|v| v.as_array()) {
+ for (domain, cred) in map.clone() {
+ let forgejo_domains = config.get("forgejo-domains");
+ if !in_array(
+ PhpMixed::String(domain.clone()),
+ &forgejo_domains.clone().unwrap_or(PhpMixed::List(vec![])),
+ true,
+ ) {
+ self.debug(
+ PhpMixed::String(format!(
+ "{} is not in the configured forgejo-domains, adding it implicitly as authentication is configured for this domain",
+ domain
+ )),
+ IndexMap::new(),
+ );
+ let merged = array_merge(
+ forgejo_domains.unwrap_or(PhpMixed::List(vec![])),
+ PhpMixed::List(vec![Box::new(PhpMixed::String(domain.clone()))]),
+ );
+ let mut inner = IndexMap::new();
+ inner.insert("forgejo-domains".to_string(), Box::new(merged));
+ let mut outer = IndexMap::new();
+ outer.insert("config".to_string(), Box::new(PhpMixed::Array(inner)));
+ config.merge(PhpMixed::Array(outer), "implicit-due-to-auth");
+ }
+
+ if let Some(cred_map) = cred.as_array() {
+ let username = cred_map
+ .get("username")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ let token = cred_map
+ .get("token")
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string());
+ self.check_and_set_authentication(domain, username, token);
+ }
+ }
+ }
+
+ if let Some(map) = http_basic.as_ref().and_then(|v| v.as_array()) {
+ for (domain, cred) in map.clone() {
+ if let Some(cred_map) = cred.as_array() {
+ let username = cred_map
+ .get("username")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ let password = cred_map
+ .get("password")
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string());
+ self.check_and_set_authentication(domain, username, password);
+ }
+ }
+ }
+
+ if let Some(map) = bearer_token.as_ref().and_then(|v| v.as_array()) {
+ for (domain, token) in map.clone() {
+ let token_str = token.as_string().unwrap_or("").to_string();
+ self.check_and_set_authentication(domain, token_str, Some("bearer".to_string()));
+ }
+ }
+
+ if let Some(map) = custom_headers.as_ref().and_then(|v| v.as_array()) {
+ for (domain, headers) in map.clone() {
+ if !headers.is_null() {
+ let json_str = json_encode_ex(&headers, 0).unwrap_or_default();
+ self.check_and_set_authentication(
+ domain,
+ json_str,
+ Some("custom-headers".to_string()),
+ );
+ }
+ }
+ }
+
+ if let Some(map) = client_certificate.as_ref().and_then(|v| v.as_array()) {
+ for (domain, cred) in map.clone() {
+ if let Some(cred_map) = cred.as_array() {
+ let local_cert = cred_map
+ .get("local_cert")
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string());
+ let local_pk = cred_map
+ .get("local_pk")
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string());
+ let passphrase = cred_map
+ .get("passphrase")
+ .and_then(|v| v.as_string())
+ .map(|s| s.to_string());
+
+ let mut ssl_options: IndexMap<String, Box<PhpMixed>> = IndexMap::new();
+ if let Some(cert) = local_cert {
+ ssl_options.insert(
+ "local_cert".to_string(),
+ Box::new(PhpMixed::String(cert)),
+ );
+ }
+ if let Some(pk) = local_pk {
+ ssl_options
+ .insert("local_pk".to_string(), Box::new(PhpMixed::String(pk)));
+ }
+ if let Some(pass) = passphrase {
+ ssl_options.insert(
+ "passphrase".to_string(),
+ Box::new(PhpMixed::String(pass)),
+ );
+ }
+
+ if !ssl_options.contains_key("local_cert") {
+ self.write_error(
+ PhpMixed::String(format!(
+ "<warning>Warning: Client certificate configuration is missing key `local_cert` for {}.</warning>",
+ domain
+ )),
+ true,
+ IOInterface::NORMAL,
+ );
+ continue;
+ }
+
+ let json_str =
+ json_encode_ex(&PhpMixed::Array(ssl_options), 0).unwrap_or_default();
+ self.check_and_set_authentication(
+ domain,
+ "client-certificate".to_string(),
+ Some(json_str),
+ );
+ }
+ }
+ }
+
+ ProcessExecutor::set_timeout(config.get("process-timeout"));
+
+ Ok(())
+ }
+
+ pub fn emergency(&mut self, message: PhpMixed, context: IndexMap<String, Box<PhpMixed>>) {
+ self.log(
+ PhpMixed::String(LogLevel::EMERGENCY.to_string()),
+ message,
+ context,
+ );
+ }
+
+ pub fn alert(&mut self, message: PhpMixed, context: IndexMap<String, Box<PhpMixed>>) {
+ self.log(
+ PhpMixed::String(LogLevel::ALERT.to_string()),
+ message,
+ context,
+ );
+ }
+
+ pub fn critical(&mut self, message: PhpMixed, context: IndexMap<String, Box<PhpMixed>>) {
+ self.log(
+ PhpMixed::String(LogLevel::CRITICAL.to_string()),
+ message,
+ context,
+ );
+ }
+
+ pub fn error(&mut self, message: PhpMixed, context: IndexMap<String, Box<PhpMixed>>) {
+ self.log(
+ PhpMixed::String(LogLevel::ERROR.to_string()),
+ message,
+ context,
+ );
+ }
+
+ pub fn warning(&mut self, message: PhpMixed, context: IndexMap<String, Box<PhpMixed>>) {
+ self.log(
+ PhpMixed::String(LogLevel::WARNING.to_string()),
+ message,
+ context,
+ );
+ }
+
+ pub fn notice(&mut self, message: PhpMixed, context: IndexMap<String, Box<PhpMixed>>) {
+ self.log(
+ PhpMixed::String(LogLevel::NOTICE.to_string()),
+ message,
+ context,
+ );
+ }
+
+ pub fn info(&mut self, message: PhpMixed, context: IndexMap<String, Box<PhpMixed>>) {
+ self.log(
+ PhpMixed::String(LogLevel::INFO.to_string()),
+ message,
+ context,
+ );
+ }
+
+ pub fn debug(&mut self, message: PhpMixed, context: IndexMap<String, Box<PhpMixed>>) {
+ self.log(
+ PhpMixed::String(LogLevel::DEBUG.to_string()),
+ message,
+ context,
+ );
+ }
+
+ pub fn log(
+ &mut self,
+ level: PhpMixed,
+ message: PhpMixed,
+ context: IndexMap<String, Box<PhpMixed>>,
+ ) {
+ let mut message_str = message.as_string().unwrap_or("").to_string();
+
+ if !context.is_empty() {
+ let json = Silencer::call(|| {
+ json_encode_ex(
+ &PhpMixed::Array(context.clone()),
+ JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
+ )
+ });
+ if let Ok(Some(json_str)) = json {
+ message_str += " ";
+ message_str += &json_str;
+ }
+ }
+
+ let level_str = level.as_string().unwrap_or("");
+ if in_array(
+ level.clone(),
+ &PhpMixed::List(vec![
+ Box::new(PhpMixed::String(LogLevel::EMERGENCY.to_string())),
+ Box::new(PhpMixed::String(LogLevel::ALERT.to_string())),
+ Box::new(PhpMixed::String(LogLevel::CRITICAL.to_string())),
+ Box::new(PhpMixed::String(LogLevel::ERROR.to_string())),
+ ]),
+ false,
+ ) {
+ self.write_error(
+ PhpMixed::String(format!("<error>{}</error>", message_str)),
+ true,
+ IOInterface::NORMAL,
+ );
+ } else if level_str == LogLevel::WARNING {
+ self.write_error(
+ PhpMixed::String(format!("<warning>{}</warning>", message_str)),
+ true,
+ IOInterface::NORMAL,
+ );
+ } else if level_str == LogLevel::NOTICE {
+ self.write_error(
+ PhpMixed::String(format!("<info>{}</info>", message_str)),
+ true,
+ IOInterface::VERBOSE,
+ );
+ } else if level_str == LogLevel::INFO {
+ self.write_error(
+ PhpMixed::String(format!("<info>{}</info>", message_str)),
+ true,
+ IOInterface::VERY_VERBOSE,
+ );
+ } else {
+ self.write_error(PhpMixed::String(message_str), true, IOInterface::DEBUG);
+ }
+ }
+}