diff options
Diffstat (limited to 'crates/shirabe-symfony-console/src/helper/question_helper.rs')
| -rw-r--r-- | crates/shirabe-symfony-console/src/helper/question_helper.rs | 916 |
1 files changed, 916 insertions, 0 deletions
diff --git a/crates/shirabe-symfony-console/src/helper/question_helper.rs b/crates/shirabe-symfony-console/src/helper/question_helper.rs new file mode 100644 index 00000000..6ee83702 --- /dev/null +++ b/crates/shirabe-symfony-console/src/helper/question_helper.rs @@ -0,0 +1,916 @@ +//! ref: composer/vendor/symfony/console/Helper/QuestionHelper.php + +use crate::cursor::Cursor; +use crate::exception::missing_input_exception::MissingInputException; +use crate::exception::runtime_exception::RuntimeException; +use crate::formatter::output_formatter::OutputFormatter; +use crate::formatter::output_formatter_style::OutputFormatterStyle; +use crate::helper::formatter_helper::FormatBlockMessages; +use crate::helper::helper::Helper; +use crate::helper::helper_interface::HelperInterface; +use crate::helper::helper_set::HelperSet; +use crate::input::input_interface::InputInterface; +use crate::output::console_output::ConsoleOutput; +use crate::output::console_output_interface::ConsoleOutputInterface; +use crate::output::console_section_output::ConsoleSectionOutput; +use crate::output::output_interface; +use crate::output::output_interface::OutputInterface; +use crate::question::ChoiceQuestion; +use crate::question::QuestionInterface; +use crate::terminal::Terminal; +use shirabe_php_shim::PhpMixed; +use shirabe_symfony_string::s; + +/// The QuestionHelper class provides helpers to interact with the user. +#[derive(Debug, Default)] +pub struct QuestionHelper { + pub(crate) inner: Helper, + + /// @var resource|null + input_stream: Option<shirabe_php_shim::PhpResource>, +} + +/// self::$stty +static STTY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); +/// self::$stdinIsInteractive +static STDIN_IS_INTERACTIVE: std::sync::Mutex<Option<bool>> = std::sync::Mutex::new(None); + +/// PHP dispatches `$this->writePrompt()` / `$this->writeError()` virtually, and +/// `SymfonyQuestionHelper` overrides both protected methods. The embedded-super port expresses +/// that late binding as a trait: the template methods are provided here and reach the overridable +/// hooks through `Self`, while `inner()` reaches the base-class state. PHP has no such interface; +/// the invented name follows the `QuestionInterface` precedent. +pub trait QuestionHelperInterface { + fn inner(&self) -> &QuestionHelper; + + fn inner_mut(&mut self) -> &mut QuestionHelper; + + /// Asks a question to the user. + /// + /// @return mixed The user answer + /// + /// @throws RuntimeException If there is no data to read in the input stream + fn ask( + &mut self, + input: &mut dyn InputInterface, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { + let mut output = output; + let error_output = { + let borrowed = output.borrow(); + (*borrowed) + .as_any() + .downcast_ref::<ConsoleOutput>() + .map(|console_output| console_output.get_error_output()) + }; + if let Some(error_output) = error_output { + output = error_output; + } + + if !input.is_interactive() { + return Ok(Ok(self.inner().get_default_answer(question))); + } + + if let Some(streamable) = input.as_streamable() + && let Some(stream) = streamable.get_stream() + { + self.inner_mut().input_stream = Some(stream); + } + + let result: anyhow::Result<Result<PhpMixed, MissingInputException>> = (|| { + if question.get_validator().is_none() { + return self.do_ask(std::rc::Rc::clone(&output), question); + } + + let interviewer = || self.do_ask(std::rc::Rc::clone(&output), question); + + self.validate_attempts(&interviewer, std::rc::Rc::clone(&output), question) + })(); + + let result = result?; + match result { + Ok(value) => Ok(Ok(value)), + Err(exception) => { + input.set_interactive(false); + + let fallback_output = self.inner().get_default_answer(question); + if matches!(fallback_output, PhpMixed::Null) { + return Ok(Err(exception)); + } + + Ok(Ok(fallback_output)) + } + } + } + + /// Asks the question to the user (PHP private; on the trait so it can late-bind the hooks). + /// + /// @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + fn do_ask( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { + self.write_prompt(std::rc::Rc::clone(&output), question); + + let input_stream = self + .inner() + .input_stream + .clone() + .unwrap_or_else(shirabe_php_shim::stdin); + let autocomplete = question.get_autocompleter_callback(); + + let ret: PhpMixed; + + if let Some(autocomplete) = autocomplete + && STTY.load(std::sync::atomic::Ordering::SeqCst) + && Terminal::has_stty_available() + { + let callback = autocomplete; + // The autocompleter callback yields an iterable (Option here); PHP + // treats a null result as an empty list of suggestions. + let callback = move |input: &str| callback(input).unwrap_or_default(); + let autocomplete = match self.inner().autocomplete( + std::rc::Rc::clone(&output), + question, + &input_stream, + &callback, + ) { + Ok(value) => value, + Err(exception) => return Ok(Err(exception)), + }; + ret = PhpMixed::String(if question.is_trimmable() { + shirabe_php_shim::trim(&autocomplete, None) + } else { + autocomplete + }); + } else { + let mut r: PhpMixed = PhpMixed::Bool(false); + if question.is_hidden() { + match self.inner().get_hidden_response( + std::rc::Rc::clone(&output), + &input_stream, + question.is_trimmable(), + )? { + Ok(hidden_response) => { + r = PhpMixed::String(if question.is_trimmable() { + shirabe_php_shim::trim(&hidden_response, None) + } else { + hidden_response + }); + } + Err(e) => { + if !question.is_hidden_fallback() { + return Err(e.into()); + } + } + } + } + + if matches!(r, PhpMixed::Bool(false)) { + let is_blocked = shirabe_php_shim::stream_get_meta_data(&input_stream) + .get("blocked") + .cloned() + .unwrap_or(PhpMixed::Bool(true)); + + if !shirabe_php_shim::boolval(&is_blocked) { + shirabe_php_shim::stream_set_blocking(&input_stream, true); + } + + let read = self.inner().read_input(&input_stream, question); + + if !shirabe_php_shim::boolval(&is_blocked) { + shirabe_php_shim::stream_set_blocking(&input_stream, false); + } + + if matches!(read, PhpMixed::Bool(false)) { + return Ok(Err(MissingInputException::new("Aborted.".to_string()))); + } + r = read; + if question.is_trimmable() { + r = PhpMixed::String(shirabe_php_shim::trim(&r.to_string(), None)); + } + } + ret = r; + } + + let mut ret = ret; + { + let borrowed = output.borrow(); + if let Some(section_output) = + (*borrowed).as_any().downcast_ref::<ConsoleSectionOutput>() + { + section_output.add_content(&ret.to_string()); + } + } + + ret = if shirabe_php_shim::strlen(&ret.to_string()) > 0 { + ret + } else { + question.get_default() + }; + + if let Some(normalizer) = question.get_normalizer() { + return Ok(Ok(normalizer(ret))); + } + + Ok(Ok(ret)) + } + + /// Validates an attempt (PHP private; on the trait so it can late-bind the hooks). + /// + /// @param callable $interviewer A callable that will ask for a question and return the result + /// + /// @return mixed The validated response + /// + /// @throws \Exception In case the max number of attempts has been reached and no valid response has been given + fn validate_attempts( + &self, + interviewer: &dyn Fn() -> anyhow::Result<Result<PhpMixed, MissingInputException>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { + let mut error: Option<shirabe_php_shim::Exception> = None; + let mut attempts = question.get_max_attempts(); + + loop { + // while (null === $attempts || $attempts--) + match attempts { + None => {} + Some(0) => break, + Some(n) => attempts = Some(n - 1), + } + + if let Some(ref error) = error { + self.write_error(std::rc::Rc::clone(&output), error); + } + + let interviewed = match interviewer()? { + Ok(value) => value, + Err(missing) => return Ok(Err(missing)), + }; + + match question.get_validator().unwrap()(Some(interviewed)) { + Ok(value) => return Ok(Ok(value)), + Err(e) => { + // PHP: `catch (RuntimeException $e) { throw $e; } catch (\Exception $error) {}`. + // The validator return type is fixed to InvalidArgumentException here, so the + // RuntimeException rethrow branch is statically unreachable; record the error + // and retry. + error = Some(shirabe_php_shim::Exception::with_code( + e.get_message().to_string(), + e.get_code(), + )); + } + } + } + + // throw $error; + Err(anyhow::Error::msg( + error + .map(|e| e.get_message().to_string()) + .unwrap_or_default(), + )) + } + + /// Outputs the question prompt (PHP protected; the overridable hook). + fn write_prompt( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) { + self.inner().write_prompt(output, question); + } + + /// Outputs an error message (PHP protected; the overridable hook). + fn write_error( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + error: &shirabe_php_shim::Exception, + ) { + self.inner().write_error(output, error); + } +} + +impl QuestionHelperInterface for QuestionHelper { + fn inner(&self) -> &QuestionHelper { + self + } + + fn inner_mut(&mut self) -> &mut QuestionHelper { + self + } +} + +impl QuestionHelper { + pub fn get_name(&self) -> String { + "question".to_string() + } + + /// Prevents usage of stty. + pub fn disable_stty() { + STTY.store(false, std::sync::atomic::Ordering::SeqCst); + } + + fn get_default_answer(&self, question: &impl QuestionInterface) -> PhpMixed { + let default = question.get_default(); + + if matches!(default, PhpMixed::Null) { + return default; + } + + if let Some(validator) = question.get_validator() { + // call_user_func($question->getValidator(), $default) + return validator(Some(default)).unwrap(); + } else if let Some(choice_question) = question.as_choice() { + let choices = choice_question.get_choices(); + + if !choice_question.is_multiselect() { + return choices + .get(&default.to_string()) + .cloned() + .unwrap_or(default); + } + + let default_parts = shirabe_php_shim::explode(",", &default.to_string()); + let mut resolved: indexmap::IndexMap<String, PhpMixed> = indexmap::IndexMap::new(); + for (k, v) in default_parts.iter().enumerate() { + let v = if question.is_trimmable() { + shirabe_php_shim::trim(v, None) + } else { + v.clone() + }; + let value = choices.get(&v).cloned().unwrap_or(PhpMixed::String(v)); + resolved.insert(k.to_string(), value); + } + + return PhpMixed::Array(resolved); + } + + default + } + + /// Outputs the question prompt. + pub(crate) fn write_prompt( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) { + let mut message = question.get_question().to_string(); + + if let Some(choice_question) = question.as_choice() { + let mut lines = vec![question.get_question().to_string()]; + lines.extend(self.format_choice_question_choices(choice_question, "info")); + output + .borrow() + .writeln(&lines, output_interface::OUTPUT_NORMAL); + + message = choice_question.get_prompt().to_string(); + } + + output + .borrow() + .write(&[message], false, output_interface::OUTPUT_NORMAL); + } + + pub(crate) fn format_choice_question_choices( + &self, + question: &ChoiceQuestion, + tag: &str, + ) -> Vec<String> { + let mut messages: Vec<String> = vec![]; + + let choices = question.get_choices(); + let max_width = choices + .keys() + .map(|key| Helper::width(key)) + .max() + .unwrap_or(0); + + for (key, value) in choices { + let padding = + shirabe_php_shim::str_repeat(" ", (max_width - Helper::width(key)) as usize); + + messages.push(format!( + " [<{tag}>{}{padding}</{tag}>] {}", + key.clone(), + value.clone(), + )); + } + + messages + } + + /// Outputs an error message. + pub(crate) fn write_error( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + error: &shirabe_php_shim::Exception, + ) { + let message = if let Some(helper_set) = self.get_helper_set() { + let formatter = helper_set.borrow().get_formatter(); + + formatter.borrow().format_block( + FormatBlockMessages::String(error.get_message().to_string()), + "error", + false, + ) + } else { + format!("<error>{}</error>", error.get_message()) + }; + + output + .borrow() + .writeln(&[message], output_interface::OUTPUT_NORMAL); + } + + /// Autocompletes a question. + fn autocomplete( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + input_stream: &shirabe_php_shim::PhpResource, + autocomplete: &dyn Fn(&str) -> Vec<PhpMixed>, + ) -> Result<String, MissingInputException> { + let cursor = Cursor::new(std::rc::Rc::clone(&output), Some(input_stream.clone())); + + let mut full_choice = String::new(); + let mut ret = String::new(); + + let mut i: i64 = 0; + let mut ofs: i64 = -1; + let mut matches = autocomplete(&ret); + let mut num_matches = matches.len() as i64; + + let stty_mode = shirabe_php_shim::shell_exec("stty -g").unwrap_or_default(); + let is_stdin = shirabe_php_shim::stream_get_meta_data(input_stream) + .get("uri") + .map(|uri| uri.to_string() == "php://stdin") + .unwrap_or(false); + let mut r = vec![input_stream.clone()]; + let w: Vec<shirabe_php_shim::PhpResource> = vec![]; + + // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) + shirabe_php_shim::shell_exec("stty -icanon -echo"); + + // Add highlighted text style + output.borrow().get_formatter().borrow_mut().set_style( + "hl", + Box::new(OutputFormatterStyle::new( + Some("black"), + Some("white"), + vec![], + )), + ); + + // Read a keypress + while !shirabe_php_shim::feof(input_stream) { + while is_stdin + && Some(0) + == shirabe_php_shim::stream_select( + &mut r, + &mut w.clone(), + &mut w.clone(), + 0, + Some(100), + ) + { + // Give signal handlers a chance to run + r = vec![input_stream.clone()]; + } + let mut c = shirabe_php_shim::fread(input_stream, 1); + + // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. + if c.is_none() + || (ret.is_empty() + && c.as_deref() == Some("") + && matches!(question.get_default(), PhpMixed::Null)) + { + shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode)); + return Err(MissingInputException::new("Aborted.".to_string())); + } else if c.as_deref() == Some("\u{7f}") { + // Backspace Character + if 0 == num_matches && 0 != i { + i -= 1; + cursor.move_left(s(&full_choice).slice(-1, None).width(false)); + + full_choice = QuestionHelper::substr(Some(&full_choice), 0, Some(i)); + } + + if 0 == i { + ofs = -1; + matches = autocomplete(&ret); + num_matches = matches.len() as i64; + } else { + num_matches = 0; + } + + // Pop the last character off the end of our string + ret = QuestionHelper::substr(Some(&ret), 0, Some(i)); + } else if c.as_deref() == Some("\u{1b}") { + // Did we read an escape sequence? + let escape = shirabe_php_shim::fread(input_stream, 2).unwrap_or_default(); + let cc = format!("{}{}", c.clone().unwrap_or_default(), escape); + c = Some(cc.clone()); + + // A = Up Arrow. B = Down Arrow + let c2 = cc.as_bytes().get(2).copied(); + if c2 == Some(b'A') || c2 == Some(b'B') { + if c2 == Some(b'A') && -1 == ofs { + ofs = 0; + } + + if 0 == num_matches { + continue; + } + + ofs += if c2 == Some(b'A') { -1 } else { 1 }; + ofs = (num_matches + ofs) % num_matches; + } + } else if shirabe_php_shim::ord(c.as_deref().unwrap_or("")) < 32 { + if c.as_deref() == Some("\t") || c.as_deref() == Some("\n") { + if num_matches > 0 && -1 != ofs { + ret = matches[ofs as usize].to_string(); + // Echo out remaining chars for current match + let remaining_characters = shirabe_php_shim::substr( + &ret, + shirabe_php_shim::strlen(&shirabe_php_shim::trim( + &self.most_recently_entered_value(&full_choice), + None, + )), + None, + ); + output.borrow().write( + std::slice::from_ref(&remaining_characters), + false, + output_interface::OUTPUT_NORMAL, + ); + full_choice.push_str(&remaining_characters); + i = match shirabe_php_shim::mb_detect_encoding(&full_choice, None, true) { + None => shirabe_php_shim::strlen(&full_choice), + Some(encoding) => shirabe_php_shim::mb_strlen(&full_choice, &encoding), + }; + + let ret_for_filter = ret.clone(); + matches = autocomplete(&ret) + .into_iter() + .filter(|m| { + ret_for_filter.is_empty() + || shirabe_php_shim::str_starts_with( + &m.to_string(), + &ret_for_filter, + ) + }) + .collect(); + ofs = -1; + } + + if c.as_deref() == Some("\n") { + output.borrow().write( + &[c.unwrap_or_default()], + false, + output_interface::OUTPUT_NORMAL, + ); + break; + } + + num_matches = 0; + } + + continue; + } else { + let cur = c.clone().unwrap_or_default(); + if "\u{80}" <= cur.as_str() { + let len = match shirabe_php_shim::str_bitand(&cur, "\u{f0}").as_str() { + "\u{c0}" => 1, + "\u{d0}" => 1, + "\u{e0}" => 2, + "\u{f0}" => 3, + _ => 0, + }; + let extra = shirabe_php_shim::fread(input_stream, len).unwrap_or_default(); + c = Some(format!("{}{}", cur, extra)); + } + + let cur = c.clone().unwrap_or_default(); + output.borrow().write( + std::slice::from_ref(&cur), + false, + output_interface::OUTPUT_NORMAL, + ); + ret.push_str(&cur); + full_choice.push_str(&cur); + i += 1; + + let mut temp_ret = ret.clone(); + + if let Some(choice_question) = question.as_choice() + && choice_question.is_multiselect() + { + temp_ret = self.most_recently_entered_value(&full_choice); + } + + num_matches = 0; + ofs = 0; + + for value in autocomplete(&ret) { + // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) + if shirabe_php_shim::str_starts_with(&value.to_string(), &temp_ret) { + if (num_matches as usize) < matches.len() { + matches[num_matches as usize] = value; + } else { + matches.push(value); + } + num_matches += 1; + } + } + } + + cursor.clear_line_after(); + + if num_matches > 0 && -1 != ofs { + cursor.save_position(); + // Write highlighted text, complete the partially entered response + let characters_entered = shirabe_php_shim::strlen(&shirabe_php_shim::trim( + &self.most_recently_entered_value(&full_choice), + None, + )); + output.borrow().write( + &[format!( + "<hl>{}</hl>", + OutputFormatter::escape_trailing_backslash(&shirabe_php_shim::substr( + &matches[ofs as usize].to_string(), + characters_entered, + None, + )) + )], + false, + output_interface::OUTPUT_NORMAL, + ); + cursor.restore_position(); + } + } + + // Reset stty so it behaves normally again + shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode)); + + Ok(full_choice) + } + + fn most_recently_entered_value(&self, entered: &str) -> String { + // Determine the most recent value that the user entered + if !shirabe_php_shim::str_contains(entered, ",") { + return entered.to_string(); + } + + let choices = shirabe_php_shim::explode(",", entered); + let last_choice = shirabe_php_shim::trim(&choices[choices.len() - 1], None); + if !last_choice.is_empty() { + return last_choice; + } + + entered.to_string() + } + + /// Gets a hidden response from user. + /// + /// @param resource $inputStream The handler resource + /// @param bool $trimmable Is the answer trimmable + /// + /// @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + fn get_hidden_response( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + input_stream: &shirabe_php_shim::PhpResource, + trimmable: bool, + ) -> anyhow::Result<Result<String, RuntimeException>> { + if cfg!(windows) { + let mut exe = format!( + "{}/../Resources/bin/hiddeninput.exe", + shirabe_php_shim::dir() + ); + + // handle code running from a phar + let mut tmp_exe: Option<String> = None; + if shirabe_php_shim::substr(&magic_file(), 0, Some(5)) == "phar:" { + let tmp = format!("{}/hiddeninput.exe", shirabe_php_shim::sys_get_temp_dir()); + shirabe_php_shim::copy(&exe, &tmp); + exe = tmp.clone(); + tmp_exe = Some(tmp); + } + + let s_exec = shirabe_php_shim::shell_exec(&format!("\"{}\"", exe)).unwrap_or_default(); + let value = if trimmable { + shirabe_php_shim::rtrim(&s_exec, None) + } else { + s_exec + }; + output + .borrow() + .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL); + + if let Some(tmp) = tmp_exe { + shirabe_php_shim::unlink(&tmp); + } + + return Ok(Ok(value)); + } + + let mut stty_mode = String::new(); + if STTY.load(std::sync::atomic::Ordering::SeqCst) && Terminal::has_stty_available() { + stty_mode = shirabe_php_shim::shell_exec("stty -g").unwrap_or_default(); + shirabe_php_shim::shell_exec("stty -echo"); + } else if self.is_interactive_input(input_stream) { + return Ok(Err(RuntimeException::new( + "Unable to hide the response.".to_string(), + ))); + } + + let value = shirabe_php_shim::fgets(input_stream, Some(4096)); + + if STTY.load(std::sync::atomic::Ordering::SeqCst) && Terminal::has_stty_available() { + shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode)); + } + + let mut value = match value { + Some(value) => value, + None => { + return Err(MissingInputException::new("Aborted.".to_string()).into()); + } + }; + if trimmable { + value = shirabe_php_shim::trim(&value, None); + } + output + .borrow() + .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL); + + Ok(Ok(value)) + } + + fn is_interactive_input(&self, input_stream: &shirabe_php_shim::PhpResource) -> bool { + let uri = shirabe_php_shim::stream_get_meta_data(input_stream) + .get("uri") + .map(|uri| uri.to_string()); + if uri.as_deref() != Some("php://stdin") { + return false; + } + + let mut stdin_is_interactive = STDIN_IS_INTERACTIVE.lock().unwrap(); + if let Some(value) = *stdin_is_interactive { + return value; + } + + let value = shirabe_php_shim::stream_isatty_resource( + &shirabe_php_shim::php_fopen_resource("php://stdin", "r"), + ); + *stdin_is_interactive = Some(value); + value + } + + /// Reads one or more lines of input and returns what is read. + /// + /// @param resource $inputStream The handler resource + /// @param Question $question The question being asked + /// + /// @return string|false The input received, false in case input could not be read + fn read_input( + &self, + input_stream: &shirabe_php_shim::PhpResource, + question: &impl QuestionInterface, + ) -> PhpMixed { + if !question.is_multiline() { + let cp = self.set_io_codepage(); + let ret = shirabe_php_shim::fgets(input_stream, Some(4096)); + + return self.reset_io_codepage( + cp, + ret.map(PhpMixed::String).unwrap_or(PhpMixed::Bool(false)), + ); + } + + let multi_line_stream_reader = self.clone_input_stream(input_stream); + let multi_line_stream_reader = match multi_line_stream_reader { + Some(reader) => reader, + None => return PhpMixed::Bool(false), + }; + + let mut ret = String::new(); + let cp = self.set_io_codepage(); + loop { + let char = shirabe_php_shim::fgetc(&multi_line_stream_reader); + let char = match char { + Some(char) => char, + None => break, + }; + if shirabe_php_shim::PHP_EOL == format!("{}{}", ret, char) { + break; + } + ret.push_str(&char); + } + + self.reset_io_codepage(cp, PhpMixed::String(ret)) + } + + /// Sets console I/O to the host code page. + /// + /// @return int Previous code page in IBM/EBCDIC format + fn set_io_codepage(&self) -> i64 { + if shirabe_php_shim::function_exists("sapi_windows_cp_set") { + let cp = shirabe_php_shim::sapi_windows_cp_get(None); + shirabe_php_shim::sapi_windows_cp_set(shirabe_php_shim::sapi_windows_cp_get(Some( + "oem", + ))); + + return cp; + } + + 0 + } + + /// Sets console I/O to the specified code page and converts the user input. + fn reset_io_codepage(&self, cp: i64, input: PhpMixed) -> PhpMixed { + let mut input = input; + if 0 != cp { + shirabe_php_shim::sapi_windows_cp_set(cp); + + if !matches!(input, PhpMixed::Bool(false)) && input.to_string() != "" { + input = PhpMixed::String(shirabe_php_shim::sapi_windows_cp_conv( + shirabe_php_shim::sapi_windows_cp_get(Some("oem")), + cp, + &input.to_string(), + )); + } + } + + input + } + + /// Clones an input stream in order to act on one instance of the same + /// stream without affecting the other instance. + /// + /// @param resource $inputStream The handler resource + /// + /// @return resource|null The cloned resource, null in case it could not be cloned + fn clone_input_stream( + &self, + input_stream: &shirabe_php_shim::PhpResource, + ) -> Option<shirabe_php_shim::PhpResource> { + let stream_meta_data = shirabe_php_shim::stream_get_meta_data(input_stream); + let seekable = stream_meta_data + .get("seekable") + .cloned() + .unwrap_or(PhpMixed::Bool(false)); + let mode = stream_meta_data + .get("mode") + .map(|m| m.to_string()) + .unwrap_or_else(|| "rb".to_string()); + let uri = stream_meta_data.get("uri").map(|u| u.to_string()); + + let uri = uri?; + + let clone_stream = shirabe_php_shim::fopen(&uri, &mode).ok()?; + + // For seekable and writable streams, add all the same data to the + // cloned stream and then seek to the same offset. + if matches!(seekable, PhpMixed::Bool(true)) && !["r", "rb", "rt"].contains(&mode.as_str()) { + let offset = shirabe_php_shim::ftell(input_stream).unwrap_or(0); + shirabe_php_shim::rewind(input_stream); + shirabe_php_shim::stream_copy_to_stream(input_stream, &clone_stream); + shirabe_php_shim::fseek(input_stream, offset, shirabe_php_shim::SEEK_SET); + shirabe_php_shim::fseek(&clone_stream, offset, shirabe_php_shim::SEEK_SET); + } + + Some(clone_stream) + } + + /// Helper::substr proxy (inherited static helper). + fn substr(string: Option<&str>, from: i64, length: Option<i64>) -> String { + Helper::substr(string.unwrap_or(""), from, length) + } +} + +/// PHP `__FILE__` magic constant. The executing code lives in the Shirabe binary itself, which is +/// the closest analogue for a native executable; it never carries the `phar:` scheme, so the +/// phar-relocation branch in `get_hidden_response` correctly never triggers. +fn magic_file() -> String { + std::env::current_exe() + .expect("current executable path") + .display() + .to_string() +} + +impl HelperInterface for QuestionHelper { + fn set_helper_set(&mut self, helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>) { + self.inner.set_helper_set(helper_set); + } + + fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::RefCell<HelperSet>>> { + self.inner.get_helper_set() + } + + fn get_name(&self) -> String { + self.get_name() + } +} |
