diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-17 14:59:32 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-17 14:59:32 +0900 |
| commit | 79bcd3a9ce71954ce7b257e5f3ca1c147c0d974a (patch) | |
| tree | 3e6b47ae4e5f351bdbf25e8dd5220714dea3746a /crates | |
| parent | 3e21569688cf0c8a1918c73ff96cb1b3aeffe0b3 (diff) | |
| download | php-shirabe-79bcd3a9ce71954ce7b257e5f3ca1c147c0d974a.tar.gz php-shirabe-79bcd3a9ce71954ce7b257e5f3ca1c147c0d974a.tar.zst php-shirabe-79bcd3a9ce71954ce7b257e5f3ca1c147c0d974a.zip | |
fix(compile): implement IOInterface and LoggerInterface for ConsoleIO
- Move standalone pub fn methods into impl IOInterface for ConsoleIO
- Add impl LoggerInterface for ConsoleIO delegating to BaseIO
- Fix load_configuration signature to take &mut Config and return Result
- Fix index::IndexMap import paths to indexmap::IndexMap in ConsoleIO and NullIO
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/shirabe/src/io/console_io.rs | 418 | ||||
| -rw-r--r-- | crates/shirabe/src/io/io_interface.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/src/io/null_io.rs | 2 |
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 { |
