aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-symfony-console/src/helper/question_helper.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-symfony-console/src/helper/question_helper.rs')
-rw-r--r--crates/shirabe-symfony-console/src/helper/question_helper.rs916
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()
+ }
+}