diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-12 01:01:35 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-12 01:01:43 +0900 |
| commit | 1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch) | |
| tree | 9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs | |
| parent | ddf0a624145b618c05c2ee47414545b99b685f37 (diff) | |
| download | php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.gz php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.zst php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.zip | |
feat(symfony-console): port Symfony Console and make the workspace compile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs')
| -rw-r--r-- | crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs | 932 |
1 files changed, 920 insertions, 12 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs index c17cb6b..7ddb620 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs @@ -1,25 +1,933 @@ -use crate::symfony::console::helper::HelperInterface; -use crate::symfony::console::input::InputInterface; -use crate::symfony::console::output::OutputInterface; -use crate::symfony::console::question::Question; +use crate::symfony::console::cursor::Cursor; +use crate::symfony::console::exception::missing_input_exception::MissingInputException; +use crate::symfony::console::exception::runtime_exception::RuntimeException; +use crate::symfony::console::formatter::output_formatter::OutputFormatter; +use crate::symfony::console::formatter::output_formatter_style::OutputFormatterStyle; +use crate::symfony::console::helper::helper::Helper; +use crate::symfony::console::helper::helper_interface::HelperInterface; +use crate::symfony::console::helper::helper_set::HelperSet; +use crate::symfony::console::input::input_interface::InputInterface; +use crate::symfony::console::output::console_output::ConsoleOutput; +use crate::symfony::console::output::console_output_interface::ConsoleOutputInterface; +use crate::symfony::console::output::console_section_output::ConsoleSectionOutput; +use crate::symfony::console::output::output_interface; +use crate::symfony::console::output::output_interface::OutputInterface; +use crate::symfony::console::question::choice_question::ChoiceQuestion; +use crate::symfony::console::question::question::Question; +use crate::symfony::console::terminal::Terminal; +use crate::symfony::string::s; +use shirabe_php_shim::AsAny; use shirabe_php_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; -#[derive(Debug)] -pub struct QuestionHelper; +/// 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); impl 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 pub fn ask( + &mut self, + input: &mut dyn InputInterface, + output: Rc<RefCell<dyn OutputInterface>>, + question: &Question, + ) -> 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.get_default_answer(question))); + } + + // TODO(phase-b): `$input instanceof StreamableInputInterface` cannot be + // expressed as a trait-object-to-trait-object downcast, and no concrete + // streamable input type is wired up yet. The stream is left unset so the + // helper falls back to STDIN. Revisit once StreamableInputInterface has a + // concrete implementor and the PhpResource/PhpMixed split is resolved. + let _ = &mut self.input_stream; + + let result: anyhow::Result<Result<PhpMixed, MissingInputException>> = (|| { + if question.get_validator().is_none() { + return self.do_ask(Rc::clone(&output), question); + } + + let interviewer = || self.do_ask(Rc::clone(&output), question); + + self.validate_attempts(&interviewer, Rc::clone(&output), question) + })(); + + let result = result?; + match result { + Ok(value) => Ok(Ok(value)), + Err(exception) => { + input.set_interactive(false); + + let fallback_output = self.get_default_answer(question); + if matches!(fallback_output, PhpMixed::Null) { + return Ok(Err(exception)); + } + + Ok(Ok(fallback_output)) + } + } + } + + 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); + } + + /// Asks the question to the user. + /// + /// @return mixed + /// + /// @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + fn do_ask( + &self, + output: Rc<RefCell<dyn OutputInterface>>, + question: &Question, + ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { + self.write_prompt(Rc::clone(&output), question); + + let input_stream = self + .input_stream + .clone() + .unwrap_or_else(shirabe_php_shim::stdin); + let autocomplete = question.get_autocompleter_callback(); + + let ret: PhpMixed; + if autocomplete.is_none() + || !STTY.load(std::sync::atomic::Ordering::SeqCst) + || !Terminal::has_stty_available() + { + let mut r: PhpMixed = PhpMixed::Bool(false); + if question.is_hidden() { + match self.get_hidden_response( + 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") + .map(|v| (**v).clone()) + .unwrap_or(PhpMixed::Bool(true)); + + if !shirabe_php_shim::boolval(&is_blocked) { + shirabe_php_shim::stream_set_blocking(&input_stream, true); + } + + let read = self.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(RuntimeException( + shirabe_php_shim::RuntimeException { + message: "Aborted.".to_string(), + code: 0, + }, + )))); + } + r = read; + if question.is_trimmable() { + r = PhpMixed::String(shirabe_php_shim::trim(&r.to_string(), None)); + } + } + ret = r; + } else { + let callback = autocomplete.unwrap(); + // 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 = + self.autocomplete(Rc::clone(&output), question, &input_stream, &callback); + ret = PhpMixed::String(if question.is_trimmable() { + shirabe_php_shim::trim(&autocomplete, None) + } else { + autocomplete + }); + } + + 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)) + } + + /// @return mixed + fn get_default_answer(&self, question: &Question) -> 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_question(question) { + let choices = choice_question.get_choices(); + + if !choice_question.is_multiselect() { + return choices + .get(&default.to_string()) + .map(|v| (**v).clone()) + .unwrap_or(default); + } + + let default_parts = shirabe_php_shim::explode(",", &default.to_string()); + let mut resolved: indexmap::IndexMap<String, Box<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) + .map(|value| (**value).clone()) + .unwrap_or(PhpMixed::String(v)); + resolved.insert(k.to_string(), Box::new(value)); + } + + return PhpMixed::Array(resolved); + } + + default + } + + /// Outputs the question prompt. + pub(crate) fn write_prompt( + &self, + output: Rc<RefCell<dyn OutputInterface>>, + question: &Question, + ) { + let mut message = question.get_question().to_string(); + + if let Some(choice_question) = question_as_choice_question(question) { + 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); + } + + /// @return string[] + 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(shirabe_php_shim::sprintf( + &format!(" [<{tag}>%s{padding}</{tag}>] %s"), + &[PhpMixed::String(key.clone()), (**value).clone()], + )); + } + + messages + } + + /// Outputs an error message. + pub(crate) fn write_error( + &self, + output: Rc<RefCell<dyn OutputInterface>>, + error: &shirabe_php_shim::Exception, + ) { + let message; + if let Some(helper_set) = self.get_helper_set() { + if helper_set.borrow().has("formatter") { + // PHP: `$this->getHelperSet()->get('formatter')->formatBlock(...)`. + // HelperSet::get yields `dyn HelperInterface`, which does not have + // `AsAny` as a supertrait, so it cannot be downcast to + // FormatterHelper. Resolved once HelperInterface gains AsAny (see + // report). + let _formatter = helper_set.borrow().get("formatter").unwrap(); + todo!("downcast dyn HelperInterface to FormatterHelper for format_block"); + } else { + message = format!("<error>{}</error>", error.message); + } + } else { + message = format!("<error>{}</error>", error.message); + } + + output + .borrow() + .writeln(&[message], output_interface::OUTPUT_NORMAL); + } + + /// Autocompletes a question. + /// + /// @param resource $inputStream + fn autocomplete( + &self, + output: Rc<RefCell<dyn OutputInterface>>, + question: &Question, + input_stream: &shirabe_php_shim::PhpResource, + autocomplete: &dyn Fn(&str) -> Vec<PhpMixed>, + ) -> String { + // TODO(phase-b): Cursor takes Option<PhpMixed>, but the input stream is a + // PhpResource and PhpMixed has no resource variant. Defaulting to STDIN + // until the PhpResource/PhpMixed stream representation is unified. + let cursor = Cursor::new(Rc::clone(&output), None); + + 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 !feof_resource(input_stream) { + while is_stdin + && 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 = fread_resource(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)); + // throw new MissingInputException('Aborted.'); + // autocomplete() returns string in PHP; this throw aborts the + // whole read. Faithful exception propagation is resolved later. + todo!("MissingInputException('Aborted.') thrown inside autocomplete"); + } 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 = fread_resource(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( + &[remaining_characters.clone()], + 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(); + num_matches = matches.len() as i64; + ofs = -1; + } + + if c.as_deref() == Some("\n") { + output.borrow().write( + &[c.clone().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 = fread_resource(input_stream, len).unwrap_or_default(); + c = Some(format!("{}{}", cur, extra)); + } + + let cur = c.clone().unwrap_or_default(); + output + .borrow() + .write(&[cur.clone()], 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_question(question) { + if 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)); + + 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, - _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - _question: &Question, + output: Rc<RefCell<dyn OutputInterface>>, + input_stream: &shirabe_php_shim::PhpResource, + trimmable: bool, + ) -> anyhow::Result<Result<String, RuntimeException>> { + if shirabe_php_shim::DIRECTORY_SEPARATOR == "\\" { + 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(shirabe_php_shim::RuntimeException { + message: "Unable to hide the response.".to_string(), + code: 0, + }))); + } + + let value = fgets_resource(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(RuntimeException( + shirabe_php_shim::RuntimeException { + message: "Aborted.".to_string(), + code: 0, + }, + )) + .into()); + } + }; + if trimmable { + value = shirabe_php_shim::trim(&value, None); + } + output + .borrow() + .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL); + + Ok(Ok(value)) + } + + /// Validates an attempt. + /// + /// @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: Rc<RefCell<dyn OutputInterface>>, + question: &Question, + ) -> 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(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 { + message: e.0.message.clone(), + code: e.0.code, + }); + } + } + } + + // throw $error; + Err(anyhow::Error::msg( + error.map(|e| e.message).unwrap_or_default(), + )) + } + + 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: &Question, ) -> PhpMixed { - todo!() + if !question.is_multiline() { + let cp = self.set_io_codepage(); + let ret = fgets_resource(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. + /// + /// @param string|false $input + /// + /// @return string|false + 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") + .map(|v| (**v).clone()) + .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::php_fopen_resource(&uri, &mode); + + // 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); + rewind_resource(input_stream); + stream_copy_to_stream_resource(input_stream, &clone_stream); + fseek_resource(input_stream, offset); + fseek_resource(&clone_stream, offset); + } + + 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) + } +} + +/// Downcasts a Question to a ChoiceQuestion if applicable. +fn question_as_choice_question(question: &Question) -> Option<&ChoiceQuestion> { + question.as_any().downcast_ref::<ChoiceQuestion>() +} + +// PHP operates on raw stream resources, but the shim models `feof`/`fread`/ +// `fgets`/`fseek`/`rewind`/`stream_copy_to_stream` over `PhpMixed`, which has no +// resource variant. These thin resource-typed wrappers stand in until the shim +// grows `*_resource` counterparts (see report). Each defers to the real +// implementation once it exists. +// PHP `__FILE__` magic constant. The shim's `file()` is PHP's file() function, +// not the magic constant, and there is no `__FILE__` shim yet (see report). +fn magic_file() -> String { + todo!("magic_file: shim needs a __FILE__ magic-constant equivalent") +} + +fn feof_resource(_stream: &shirabe_php_shim::PhpResource) -> bool { + todo!("feof_resource: shim needs a PhpResource-based feof") +} + +fn fread_resource(_stream: &shirabe_php_shim::PhpResource, _length: i64) -> Option<String> { + todo!("fread_resource: shim needs a PhpResource-based fread") +} + +fn fgets_resource(_stream: &shirabe_php_shim::PhpResource, _length: Option<i64>) -> Option<String> { + todo!("fgets_resource: shim needs a PhpResource-based fgets") +} + +fn fseek_resource(_stream: &shirabe_php_shim::PhpResource, _offset: i64) -> i64 { + todo!("fseek_resource: shim needs a PhpResource-based fseek") +} + +fn rewind_resource(_stream: &shirabe_php_shim::PhpResource) -> bool { + todo!("rewind_resource: shim needs a PhpResource-based rewind") +} + +fn stream_copy_to_stream_resource( + _source: &shirabe_php_shim::PhpResource, + _dest: &shirabe_php_shim::PhpResource, +) -> Option<i64> { + todo!("stream_copy_to_stream_resource: shim needs a PhpResource-based stream_copy_to_stream") } impl HelperInterface for QuestionHelper { - fn as_any(&self) -> &dyn std::any::Any { - self + fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) { + self.inner.set_helper_set(helper_set); + } + + fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> { + self.inner.get_helper_set() + } + + fn get_name(&self) -> String { + self.get_name() } } |
