//! ref: composer/vendor/symfony/console/Input/StringInput.php use crate::exception::InvalidArgumentException; use crate::input::ArgvInput; use crate::input::InputDefinition; use crate::input::InputInterface; use crate::input::StreamableInputInterface; use indexmap::IndexMap; use shirabe_php_shim::{PhpMixed, php_regex, preg_match}; /// StringInput represents an input provided as a string. /// /// Usage: /// /// ```php /// $input = new StringInput('foo --bar="foobar"'); /// ``` #[derive(Debug, Clone)] pub struct StringInput { inner: ArgvInput, } impl StringInput { pub const REGEX_STRING: &'static str = r#"([^\s]+?)(?:\s|(? anyhow::Result { // parent::__construct([]) let inner = ArgvInput::new(Some(vec![]), None)?; let mut string_input = StringInput { inner }; let tokens = string_input.tokenize(input)?; string_input.inner.set_tokens(tokens); Ok(string_input) } /// Tokenizes a string. fn tokenize(&self, input: &str) -> anyhow::Result> { let bytes = input.as_bytes(); let mut tokens: Vec = vec![]; let length = shirabe_php_shim::strlen(input); let mut cursor: i64 = 0; let mut token: Option = None; while cursor < length { if bytes[cursor as usize] == b'\\' { cursor += 1; let next: String = match bytes.get(cursor as usize) { Some(b) => String::from_utf8_lossy(&[*b]).into_owned(), None => String::new(), }; token = Some(format!("{}{}", token.unwrap_or_default(), next)); cursor += 1; continue; } // Regex pattern compatibility: // PHP runs these patterns anchored (`A`) at `$cursor`, so each one must match starting // exactly there. The `regex` crate anchors only at the head of the haystack, so the // search runs over the part of the input that begins at the cursor and each pattern // carries a leading `^` instead of the `A` modifier. let rest = &input[cursor as usize..]; if let Some(m) = preg_match(php_regex!(r"/^\s+/"), rest) { if token.is_some() { tokens.push(token.take().unwrap()); } cursor += shirabe_php_shim::strlen(m.get(0).unwrap_or("")); } else if let Some(m) = preg_match( format!(r#"/^([^="'\s]+?)(=?)({}+)/"#, Self::REGEX_QUOTED_STRING), rest, ) { let inner = shirabe_php_shim::substr(m.get(3).unwrap_or(""), 1, Some(-1)); let replaced = shirabe_php_shim::str_replace_arr(&["\"'", "'\"", "''", "\"\""], "", &inner); token = Some(format!( "{}{}{}{}", token.unwrap_or_default(), m.get(1).unwrap_or(""), m.get(2).unwrap_or(""), shirabe_php_shim::stripcslashes(&replaced) )); cursor += shirabe_php_shim::strlen(m.get(0).unwrap_or("")); } else if let Some(m) = preg_match(format!(r"/^{}/", Self::REGEX_QUOTED_STRING), rest) { token = Some(format!( "{}{}", token.unwrap_or_default(), shirabe_php_shim::stripcslashes(&shirabe_php_shim::substr( m.get(0).unwrap_or(""), 1, Some(-1) )) )); cursor += shirabe_php_shim::strlen(m.get(0).unwrap_or("")); } else if let Some(m) = preg_match(format!(r"/^{}/", Self::REGEX_UNQUOTED_STRING), rest) { token = Some(format!( "{}{}", token.unwrap_or_default(), m.get(1).unwrap_or("") )); cursor += shirabe_php_shim::strlen(m.get(0).unwrap_or("")); } else { // should never happen return Err(InvalidArgumentException::new(format!( "Unable to parse input near \"... {} ...\".", shirabe_php_shim::substr(input, cursor, Some(10)), )) .into()); } } if let Some(token) = token { tokens.push(token); } Ok(tokens) } } impl std::fmt::Display for StringInput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.inner.fmt(f) } } impl InputInterface for StringInput { fn dup(&self) -> std::rc::Rc> { std::rc::Rc::new(std::cell::RefCell::new(self.clone())) } fn get_first_argument(&self) -> Option { self.inner.get_first_argument() } fn has_parameter_option(&self, values: PhpMixed, only_params: bool) -> bool { InputInterface::has_parameter_option(&self.inner, values, only_params) } fn get_parameter_option( &self, values: PhpMixed, default: PhpMixed, only_params: bool, ) -> PhpMixed { InputInterface::get_parameter_option(&self.inner, values, default, only_params) } fn bind(&mut self, definition: &InputDefinition) -> anyhow::Result<()> { InputInterface::bind(&mut self.inner, definition) } fn validate(&mut self) -> anyhow::Result<()> { self.inner.validate() } fn get_arguments(&self) -> IndexMap { InputInterface::get_arguments(&self.inner) } fn get_argument(&self, name: &str) -> anyhow::Result { self.inner.get_argument(name) } fn set_argument(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { self.inner.set_argument(name, value) } fn has_argument(&self, name: &str) -> bool { self.inner.has_argument(name) } fn get_options(&self) -> IndexMap { InputInterface::get_options(&self.inner) } fn get_option(&self, name: &str) -> anyhow::Result { self.inner.get_option(name) } fn set_option(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()> { self.inner.set_option(name, value) } fn has_option(&self, name: &str) -> bool { self.inner.has_option(name) } fn is_interactive(&self) -> bool { self.inner.is_interactive() } fn set_interactive(&mut self, interactive: bool) { self.inner.set_interactive(interactive) } fn __to_string(&self) -> String { self.to_string() } fn as_streamable(&self) -> Option<&dyn StreamableInputInterface> { Some(self) } fn as_streamable_mut(&mut self) -> Option<&mut dyn StreamableInputInterface> { Some(self) } } impl StreamableInputInterface for StringInput { fn set_stream(&mut self, stream: shirabe_php_shim::PhpResource) { self.inner.set_stream(stream) } fn get_stream(&self) -> Option { self.inner.get_stream() } }