aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/io/console_io.rs418
-rw-r--r--crates/shirabe/src/io/io_interface.rs2
-rw-r--r--crates/shirabe/src/io/null_io.rs2
3 files changed, 241 insertions, 181 deletions
diff --git a/crates/shirabe/src/io/console_io.rs b/crates/shirabe/src/io/console_io.rs
index 6a2edc7..ff1ed0f 100644
--- a/crates/shirabe/src/io/console_io.rs
+++ b/crates/shirabe/src/io/console_io.rs
@@ -1,8 +1,11 @@
//! ref: composer/src/Composer/IO/ConsoleIO.php
+use crate::config::Config;
use crate::io::io_interface;
use indexmap::IndexMap;
+use indexmap::indexmap;
use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_external_packages::psr::log::logger_interface::LoggerInterface;
use shirabe_external_packages::symfony::component::console::helper::helper_set::HelperSet;
use shirabe_external_packages::symfony::component::console::helper::progress_bar::ProgressBar;
use shirabe_external_packages::symfony::component::console::helper::table::Table;
@@ -25,7 +28,7 @@ use crate::util::silencer::Silencer;
/// The Input/Output helper.
#[derive(Debug)]
pub struct ConsoleIO {
- authentications: index::IndexMap<String, indexmap::IndexMap<String, Option<String>>>,
+ authentications: indexmap::IndexMap<String, indexmap::IndexMap<String, Option<String>>>,
pub(crate) input: Box<dyn InputInterface>,
pub(crate) output: Box<dyn OutputInterface>,
@@ -60,9 +63,7 @@ impl ConsoleIO {
);
verbosity_map.insert(io_interface::DEBUG, OutputInterface::VERBOSITY_DEBUG);
Self {
- inner: BaseIO {
- authentications: IndexMap::new(),
- },
+ authentications: indexmap![],
input,
output,
helper_set,
@@ -77,46 +78,6 @@ impl ConsoleIO {
self.start_time = Some(start_time);
}
- pub fn is_interactive(&self) -> bool {
- self.input.is_interactive()
- }
-
- pub fn is_decorated(&self) -> bool {
- self.output.is_decorated()
- }
-
- pub fn is_verbose(&self) -> bool {
- self.output.is_verbose()
- }
-
- pub fn is_very_verbose(&self) -> bool {
- self.output.is_very_verbose()
- }
-
- pub fn is_debug(&self) -> bool {
- self.output.is_debug()
- }
-
- pub fn write(&mut self, messages: PhpMixed, newline: bool, verbosity: i64) {
- let messages = Self::sanitize(messages, true);
-
- self.do_write(messages, newline, false, verbosity, false);
- }
-
- pub fn write_error(&mut self, messages: PhpMixed, newline: bool, verbosity: i64) {
- let messages = Self::sanitize(messages, true);
-
- self.do_write(messages, newline, true, verbosity, false);
- }
-
- pub fn write_raw(&mut self, messages: PhpMixed, newline: bool, verbosity: i64) {
- self.do_write(messages, newline, false, verbosity, true);
- }
-
- pub fn write_error_raw(&mut self, messages: PhpMixed, newline: bool, verbosity: i64) {
- self.do_write(messages, newline, true, verbosity, true);
- }
-
/// @param string[]|string $messages
fn do_write(
&mut self,
@@ -193,26 +154,6 @@ impl ConsoleIO {
);
}
- pub fn overwrite(
- &mut self,
- messages: PhpMixed,
- newline: bool,
- size: Option<i64>,
- verbosity: i64,
- ) {
- self.do_overwrite(messages, newline, size, false, verbosity);
- }
-
- pub fn overwrite_error(
- &mut self,
- messages: PhpMixed,
- newline: bool,
- size: Option<i64>,
- verbosity: i64,
- ) {
- self.do_overwrite(messages, newline, size, true, verbosity);
- }
-
/// @param string[]|string $messages
fn do_overwrite(
&mut self,
@@ -299,7 +240,221 @@ impl ConsoleIO {
ProgressBar::new(self.get_error_output(), max)
}
- pub fn ask(&mut self, question: PhpMixed, default: PhpMixed) -> PhpMixed {
+ pub fn get_table(&self) -> Table {
+ Table::new(&*self.output)
+ }
+
+ fn get_error_output(&self) -> &dyn OutputInterface {
+ if self.output.is_console_output_interface() {
+ // TODO(phase-b): downcast Box<dyn OutputInterface> to ConsoleOutputInterface
+ return todo!(
+ "downcast self.output to ConsoleOutputInterface and call get_error_output()"
+ );
+ }
+
+ &*self.output
+ }
+
+ /// Sanitize string to remove control characters
+ ///
+ /// If $allowNewlines is true, \x0A (\n) and \x0D\x0A (\r\n) are let through. Single \r are still sanitized away to prevent overwriting whole lines.
+ ///
+ /// All other control chars (except NULL bytes) as well as ANSI escape sequences are removed.
+ ///
+ /// Invalid unicode sequences are turned into question marks.
+ ///
+ /// @param string|iterable<string> $messages
+ /// @return string|array<string>
+ /// @phpstan-return ($messages is string ? string : array<string>)
+ pub fn sanitize(messages: PhpMixed, allow_newlines: bool) -> PhpMixed {
+ // Match ANSI escape sequences:
+ // - CSI (Control Sequence Introducer): ESC [ params intermediate final
+ // - OSC (Operating System Command): ESC ] ... ESC \ or BEL
+ // - Other ESC sequences: ESC followed by any character
+ let escape_pattern =
+ r"\x1B\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]|\x1B\].*?(?:\x1B\\|\x07)|\x1B.";
+ let pattern = if allow_newlines {
+ format!(
+ "{{{}|[\\x01-\\x09\\x0B\\x0C\\x0E-\\x1A]|\\r(?!\\n)}}u",
+ escape_pattern
+ )
+ } else {
+ format!("{{{}|[\\x01-\\x1A]}}u", escape_pattern)
+ };
+ if is_string(&messages) {
+ let message = Self::ensure_valid_utf8(messages.as_string().unwrap_or(""));
+ return PhpMixed::String(Preg::replace(&pattern, "", &message));
+ }
+
+ // PHP: $sanitized = []; foreach ($messages as $key => $message) { ... }
+ let mut sanitized: IndexMap<String, PhpMixed> = IndexMap::new();
+ match &messages {
+ PhpMixed::List(l) => {
+ for (key, message) in l.iter().enumerate() {
+ let s = Self::ensure_valid_utf8(message.as_string().unwrap_or(""));
+ sanitized.insert(
+ key.to_string(),
+ PhpMixed::String(Preg::replace(&pattern, "", &s)),
+ );
+ }
+ }
+ PhpMixed::Array(a) => {
+ for (key, message) in a {
+ let s = Self::ensure_valid_utf8(message.as_string().unwrap_or(""));
+ sanitized.insert(
+ key.clone(),
+ PhpMixed::String(Preg::replace(&pattern, "", &s)),
+ );
+ }
+ }
+ _ => {}
+ }
+
+ PhpMixed::Array(
+ sanitized
+ .into_iter()
+ .map(|(k, v)| (k, Box::new(v)))
+ .collect(),
+ )
+ }
+
+ /// Ensures a string is valid UTF-8, replacing invalid byte sequences with '?'
+ fn ensure_valid_utf8(string: &str) -> String {
+ // Quick check: if string is already valid UTF-8, return as-is
+ if function_exists("mb_check_encoding") && mb_check_encoding(string, "UTF-8") {
+ return string.to_string();
+ }
+
+ // Use mb_convert_encoding to replace invalid sequences with '?'
+ // This makes it visible when data quality issues occur
+ if function_exists("mb_convert_encoding") {
+ return mb_convert_encoding(string.as_bytes().to_vec(), "UTF-8", "UTF-8");
+ }
+
+ // Fallback to iconv if mbstring unavailable
+ if function_exists("iconv") {
+ let cleaned =
+ Silencer::call(|| Ok(shirabe_php_shim::iconv("UTF-8", "UTF-8//TRANSLIT", string)));
+ if let Ok(Some(c)) = cleaned {
+ return c;
+ }
+ }
+
+ // Last resort: return as-is (should never happen - Composer requires mbstring OR iconv)
+ string.to_string()
+ }
+
+ /// Helper: PHP `(array) $messages` then collect strings
+ fn to_string_list(messages: &PhpMixed) -> Vec<String> {
+ match messages {
+ PhpMixed::String(s) => vec![s.clone()],
+ PhpMixed::List(l) => l
+ .iter()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect(),
+ PhpMixed::Array(a) => a
+ .values()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect(),
+ _ => vec![],
+ }
+ }
+}
+
+impl LoggerInterface for ConsoleIO {
+ fn emergency(&self, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::emergency(self, message, context)
+ }
+
+ fn alert(&self, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::alert(self, message, context)
+ }
+
+ fn critical(&self, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::critical(self, message, context)
+ }
+
+ fn error(&self, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::error(self, message, context)
+ }
+
+ fn warning(&self, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::warning(self, message, context)
+ }
+
+ fn notice(&self, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::notice(self, message, context)
+ }
+
+ fn info(&self, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::info(self, message, context)
+ }
+
+ fn debug(&self, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::debug(self, message, context)
+ }
+
+ fn log(&self, level: &str, message: &str, context: &[(&str, &str)]) {
+ <Self as BaseIO>::log(self, level, message, context)
+ }
+}
+
+impl IOInterface for ConsoleIO {
+ fn is_interactive(&self) -> bool {
+ self.input.is_interactive()
+ }
+
+ fn is_verbose(&self) -> bool {
+ self.output.is_verbose()
+ }
+
+ fn is_very_verbose(&self) -> bool {
+ self.output.is_very_verbose()
+ }
+
+ fn is_debug(&self) -> bool {
+ self.output.is_debug()
+ }
+
+ fn is_decorated(&self) -> bool {
+ self.output.is_decorated()
+ }
+
+ fn write(&mut self, messages: PhpMixed, newline: bool, verbosity: i64) {
+ let messages = Self::sanitize(messages, true);
+
+ self.do_write(messages, newline, false, verbosity, false);
+ }
+
+ fn write_error(&mut self, messages: PhpMixed, newline: bool, verbosity: i64) {
+ let messages = Self::sanitize(messages, true);
+
+ self.do_write(messages, newline, true, verbosity, false);
+ }
+
+ fn write_raw(&mut self, messages: PhpMixed, newline: bool, verbosity: i64) {
+ self.do_write(messages, newline, false, verbosity, true);
+ }
+
+ fn write_error_raw(&mut self, messages: PhpMixed, newline: bool, verbosity: i64) {
+ self.do_write(messages, newline, true, verbosity, true);
+ }
+
+ fn overwrite(&mut self, messages: PhpMixed, newline: bool, size: Option<i64>, verbosity: i64) {
+ self.do_overwrite(messages, newline, size, false, verbosity);
+ }
+
+ fn overwrite_error(
+ &mut self,
+ messages: PhpMixed,
+ newline: bool,
+ size: Option<i64>,
+ verbosity: i64,
+ ) {
+ self.do_overwrite(messages, newline, size, true, verbosity);
+ }
+
+ fn ask(&mut self, question: PhpMixed, default: PhpMixed) -> PhpMixed {
// PHP: $helper = $this->helperSet->get('question');
let helper = self.helper_set.get("question");
let question = Question::new(
@@ -314,7 +469,7 @@ impl ConsoleIO {
helper.ask(&*self.input, self.get_error_output(), &question)
}
- pub fn ask_confirmation(&mut self, question: PhpMixed, default: bool) -> bool {
+ fn ask_confirmation(&mut self, question: PhpMixed, default: bool) -> bool {
let helper = self.helper_set.get("question");
let default_mixed = PhpMixed::Bool(default);
let question = StrictConfirmationQuestion::new(
@@ -332,7 +487,7 @@ impl ConsoleIO {
.unwrap_or(false)
}
- pub fn ask_and_validate(
+ fn ask_and_validate(
&mut self,
question: PhpMixed,
validator: Box<dyn Fn(PhpMixed) -> PhpMixed>,
@@ -354,7 +509,7 @@ impl ConsoleIO {
helper.ask(&*self.input, self.get_error_output(), &question)
}
- pub fn ask_and_hide_answer(&mut self, question: PhpMixed) -> Option<String> {
+ fn ask_and_hide_answer(&mut self, question: PhpMixed) -> Option<String> {
let helper = self.helper_set.get("question");
let mut question = Question::new(Self::sanitize(question, true), PhpMixed::Null);
question.set_hidden(true);
@@ -365,7 +520,7 @@ impl ConsoleIO {
.map(|s| s.to_string())
}
- pub fn select(
+ fn select(
&mut self,
question: PhpMixed,
choices: PhpMixed,
@@ -449,124 +604,29 @@ impl ConsoleIO {
)
}
- pub fn get_table(&self) -> Table {
- Table::new(&*self.output)
+ fn get_authentications(&self) -> IndexMap<String, IndexMap<String, Option<String>>> {
+ <Self as BaseIO>::get_authentications(self)
}
- fn get_error_output(&self) -> &dyn OutputInterface {
- if self.output.is_console_output_interface() {
- // TODO(phase-b): downcast Box<dyn OutputInterface> to ConsoleOutputInterface
- return todo!(
- "downcast self.output to ConsoleOutputInterface and call get_error_output()"
- );
- }
-
- &*self.output
+ fn has_authentication(&self, repository_name: &str) -> bool {
+ <Self as BaseIO>::has_authentication(self, repository_name)
}
- /// Sanitize string to remove control characters
- ///
- /// If $allowNewlines is true, \x0A (\n) and \x0D\x0A (\r\n) are let through. Single \r are still sanitized away to prevent overwriting whole lines.
- ///
- /// All other control chars (except NULL bytes) as well as ANSI escape sequences are removed.
- ///
- /// Invalid unicode sequences are turned into question marks.
- ///
- /// @param string|iterable<string> $messages
- /// @return string|array<string>
- /// @phpstan-return ($messages is string ? string : array<string>)
- pub fn sanitize(messages: PhpMixed, allow_newlines: bool) -> PhpMixed {
- // Match ANSI escape sequences:
- // - CSI (Control Sequence Introducer): ESC [ params intermediate final
- // - OSC (Operating System Command): ESC ] ... ESC \ or BEL
- // - Other ESC sequences: ESC followed by any character
- let escape_pattern =
- r"\x1B\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]|\x1B\].*?(?:\x1B\\|\x07)|\x1B.";
- let pattern = if allow_newlines {
- format!(
- "{{{}|[\\x01-\\x09\\x0B\\x0C\\x0E-\\x1A]|\\r(?!\\n)}}u",
- escape_pattern
- )
- } else {
- format!("{{{}|[\\x01-\\x1A]}}u", escape_pattern)
- };
- if is_string(&messages) {
- let message = Self::ensure_valid_utf8(messages.as_string().unwrap_or(""));
- return PhpMixed::String(Preg::replace(&pattern, "", &message));
- }
-
- // PHP: $sanitized = []; foreach ($messages as $key => $message) { ... }
- let mut sanitized: IndexMap<String, PhpMixed> = IndexMap::new();
- match &messages {
- PhpMixed::List(l) => {
- for (key, message) in l.iter().enumerate() {
- let s = Self::ensure_valid_utf8(message.as_string().unwrap_or(""));
- sanitized.insert(
- key.to_string(),
- PhpMixed::String(Preg::replace(&pattern, "", &s)),
- );
- }
- }
- PhpMixed::Array(a) => {
- for (key, message) in a {
- let s = Self::ensure_valid_utf8(message.as_string().unwrap_or(""));
- sanitized.insert(
- key.clone(),
- PhpMixed::String(Preg::replace(&pattern, "", &s)),
- );
- }
- }
- _ => {}
- }
-
- PhpMixed::Array(
- sanitized
- .into_iter()
- .map(|(k, v)| (k, Box::new(v)))
- .collect(),
- )
+ fn get_authentication(&self, repository_name: &str) -> IndexMap<String, Option<String>> {
+ <Self as BaseIO>::get_authentication(self, repository_name)
}
- /// Ensures a string is valid UTF-8, replacing invalid byte sequences with '?'
- fn ensure_valid_utf8(string: &str) -> String {
- // Quick check: if string is already valid UTF-8, return as-is
- if function_exists("mb_check_encoding") && mb_check_encoding(string, "UTF-8") {
- return string.to_string();
- }
-
- // Use mb_convert_encoding to replace invalid sequences with '?'
- // This makes it visible when data quality issues occur
- if function_exists("mb_convert_encoding") {
- return mb_convert_encoding(string.as_bytes().to_vec(), "UTF-8", "UTF-8");
- }
-
- // Fallback to iconv if mbstring unavailable
- if function_exists("iconv") {
- let cleaned =
- Silencer::call(|| Ok(shirabe_php_shim::iconv("UTF-8", "UTF-8//TRANSLIT", string)));
- if let Ok(Some(c)) = cleaned {
- return c;
- }
- }
-
- // Last resort: return as-is (should never happen - Composer requires mbstring OR iconv)
- string.to_string()
+ fn set_authentication(
+ &mut self,
+ repository_name: String,
+ username: String,
+ password: Option<String>,
+ ) {
+ <Self as BaseIO>::set_authentication(self, repository_name, username, password)
}
- /// Helper: PHP `(array) $messages` then collect strings
- fn to_string_list(messages: &PhpMixed) -> Vec<String> {
- match messages {
- PhpMixed::String(s) => vec![s.clone()],
- PhpMixed::List(l) => l
- .iter()
- .filter_map(|v| v.as_string().map(|s| s.to_string()))
- .collect(),
- PhpMixed::Array(a) => a
- .values()
- .filter_map(|v| v.as_string().map(|s| s.to_string()))
- .collect(),
- _ => vec![],
- }
+ fn load_configuration(&mut self, config: &mut Config) -> anyhow::Result<()> {
+ <Self as BaseIO>::load_configuration(self, config)
}
}
diff --git a/crates/shirabe/src/io/io_interface.rs b/crates/shirabe/src/io/io_interface.rs
index d66552d..406a3ff 100644
--- a/crates/shirabe/src/io/io_interface.rs
+++ b/crates/shirabe/src/io/io_interface.rs
@@ -71,5 +71,5 @@ pub trait IOInterface: LoggerInterface {
password: Option<String>,
);
- fn load_configuration(&mut self, config: &Config);
+ fn load_configuration(&mut self, config: &mut Config) -> anyhow::Result<()>;
}
diff --git a/crates/shirabe/src/io/null_io.rs b/crates/shirabe/src/io/null_io.rs
index 66f7123..10d56a1 100644
--- a/crates/shirabe/src/io/null_io.rs
+++ b/crates/shirabe/src/io/null_io.rs
@@ -6,7 +6,7 @@ use shirabe_php_shim::PhpMixed;
#[derive(Debug)]
pub struct NullIO {
- authentications: index::IndexMap<String, indexmap::IndexMap<String, Option<String>>>,
+ authentications: indexmap::IndexMap<String, indexmap::IndexMap<String, Option<String>>>,
}
impl IOInterface for NullIO {