diff --git a/regex/regex.cc b/regex/regex.cc index 6bcefe7..ecfe562 100644 --- a/regex/regex.cc +++ b/regex/regex.cc @@ -176,6 +176,26 @@ Regex::SplitNResult Regex::Split(absl::string_view text, size_t limit) const { return SplitNResult(regex_.splitn(internal::AsSlice(text), limit)); } +std::string Regex::Replace(absl::string_view text, + absl::string_view rewrite) const { + return internal::AsString( + regex_.replace(internal::AsSlice(text), internal::AsSlice(rewrite))); +} + +std::string Regex::ReplaceAll(absl::string_view text, + absl::string_view rewrite) const { + return internal::AsString( + regex_.replace_all(internal::AsSlice(text), internal::AsSlice(rewrite))); +} + +std::string Regex::Replacen(absl::string_view text, size_t limit, + absl::string_view rewrite) const { + return internal::AsString( + regex_ + .replacen(internal::AsSlice(text), limit, internal::AsSlice(rewrite)) + .into_result()); +} + std::map Regex::NamedCapturingGroups() const { std::map result; int i = 0; @@ -249,4 +269,44 @@ absl::StatusOr RegexSet::Compile( return RegexSet(std::move(result).value()); } +bool Replace(std::string* str, const Regex& regex, absl::string_view rewrite) { + if (str == nullptr) return false; + rust::ReplaceResult result = regex.regex_.replacen( + internal::AsSlice(*str), 1, internal::AsSlice(rewrite)); + if (result.count() == 0) { + return false; + } + *str = internal::AsString(std::move(result).into_result()); + return true; +} + +bool Replace(std::string* str, absl::string_view pattern, + absl::string_view rewrite) { + if (str == nullptr) return false; + absl::StatusOr compiled_regex = Regex::Compile(pattern); + if (!compiled_regex.ok()) return false; + return Replace(str, *compiled_regex, rewrite); +} + +int GlobalReplace(std::string* str, const Regex& regex, + absl::string_view rewrite) { + if (str == nullptr) return 0; + rust::ReplaceResult result = regex.regex_.replacen( + internal::AsSlice(*str), 0, internal::AsSlice(rewrite)); + size_t count = result.count(); + if (count == 0) { + return 0; + } + *str = internal::AsString(std::move(result).into_result()); + return static_cast(count); +} + +int GlobalReplace(std::string* str, absl::string_view pattern, + absl::string_view rewrite) { + if (str == nullptr) return 0; + absl::StatusOr compiled_regex = Regex::Compile(pattern); + if (!compiled_regex.ok()) return 0; + return GlobalReplace(str, *compiled_regex, rewrite); +} + } // namespace security::regex diff --git a/regex/regex.h b/regex/regex.h index 58f4971..4f6a747 100644 --- a/regex/regex.h +++ b/regex/regex.h @@ -126,6 +126,15 @@ // std::string s = c->Expand("$2 $1"); // "world hello" // } // +// You can also use Replace and GlobalReplace (similar to RE2): +// +// std::string s = "yabba dabba doo"; +// Replace(&s, "b+", "d"); // s is now "yada dabba doo" +// GlobalReplace(&s, "b+", "d"); // s is now "yada dada doo" +// +// NOTE: The `rewrite` string uses Rust's `regex` crate syntax (e.g., `$1`, +// `${name}`) for capture groups, rather than RE2's `\1` syntax. +// // ----------------------------------------------------------------------- // NUMERIC PARSING: // @@ -401,6 +410,26 @@ class Regex { // returns at most `limit` pieces. SplitNResult Split(absl::string_view text, size_t limit) const; + // Replaces the first match of the regex in `text` with `rewrite`. + // NOTE: The `rewrite` string uses `$1` syntax for capture groups, NOT `\1`. + std::string Replace(absl::string_view text, absl::string_view rewrite) const; + + // Replaces all non-overlapping matches of the regex in `text` with `rewrite`. + // NOTE: The `rewrite` string uses `$1` syntax for capture groups, NOT `\1`. + std::string ReplaceAll(absl::string_view text, + absl::string_view rewrite) const; + + // Replaces at most `limit` non-overlapping matches of the regex in `text` + // with `rewrite`. If `limit` is 0, replaces all matches. + // NOTE: The `rewrite` string uses `$1` syntax for capture groups, NOT `\1`. + std::string Replacen(absl::string_view text, size_t limit, + absl::string_view rewrite) const; + + friend bool Replace(std::string* str, const Regex& regex, + absl::string_view rewrite); + friend int GlobalReplace(std::string* str, const Regex& regex, + absl::string_view rewrite); + private: explicit Regex(rust::Regex inner); @@ -616,21 +645,21 @@ Arg Octal(T* ptr) { } } -// Matches `r` against `text` and stores captures in the given Args. -inline bool MatchN(absl::string_view text, const Regex& r, +// Matches `regex` against `text` and stores captures in the given Args. +inline bool MatchN(absl::string_view text, const Regex& regex, const Arg* const args[], int n) { if (n == 0) { - return r.IsMatch(text); + return regex.IsMatch(text); } - std::optional c = r.FindCaptures(text); - if (!c.has_value()) { + std::optional captures = regex.FindCaptures(text); + if (!captures.has_value()) { return false; } - if (static_cast(n) > c->Len() - 1) { + if (static_cast(n) > captures->Len() - 1) { return false; } for (int i = 0; i < n; ++i) { - if (!args[i]->Parse(c->Get(i + 1))) { + if (!args[i]->Parse(captures->Get(i + 1))) { return false; } } @@ -638,9 +667,9 @@ inline bool MatchN(absl::string_view text, const Regex& r, } // Matches the pattern to any substring in the text using the array interface. -inline bool PartialMatchN(absl::string_view text, const Regex& r, +inline bool PartialMatchN(absl::string_view text, const Regex& regex, const Arg* const args[], int n) { - return MatchN(text, r, args, n); + return MatchN(text, regex, args, n); } // Matches the pattern to any substring in the text (similar to @@ -649,62 +678,85 @@ inline bool PartialMatchN(absl::string_view text, const Regex& r, template bool PartialMatch(absl::string_view text, absl::string_view pattern, Args&&... args) { - absl::StatusOr r = Regex::Compile(pattern); - if (!r.ok()) { + absl::StatusOr compiled_regex = Regex::Compile(pattern); + if (!compiled_regex.ok()) { return false; } - return PartialMatch(text, *r, std::forward(args)...); + return PartialMatch(text, *compiled_regex, std::forward(args)...); } // Like PartialMatch, but takes a pre-compiled Regex. template -bool PartialMatch(absl::string_view text, const Regex& r, Args&&... args) { +bool PartialMatch(absl::string_view text, const Regex& regex, Args&&... args) { if constexpr (sizeof...(args) == 0) { - return MatchN(text, r, nullptr, 0); + return MatchN(text, regex, nullptr, 0); } else { Arg temp_args[] = {MakeArg(std::forward(args))...}; // We need an array of pointers to Arg. - return [](absl::string_view text, const Regex& r, + return [](absl::string_view text, const Regex& regex, Arg* args_array, std::index_sequence) { const Arg* const ptrs[] = {&args_array[Is]...}; - return MatchN(text, r, ptrs, sizeof...(Is)); - }(text, r, temp_args, std::index_sequence_for{}); + return MatchN(text, regex, ptrs, sizeof...(Is)); + }(text, regex, temp_args, std::index_sequence_for{}); } } +// Matches the pattern to the entire text using the array interface. +inline bool FullMatchN(absl::string_view text, const Regex& regex, + const Arg* const args[], int n) { + std::optional captures = regex.FindCaptures(text); + if (captures && captures->GetMatch().Start() == 0 && + captures->GetMatch().End() == text.size()) { + if (static_cast(n) > captures->Len() - 1) return false; + for (int i = 0; i < n; ++i) { + if (!args[i]->Parse(captures->Get(i + 1))) return false; + } + return true; + } + return false; +} + // Matches the pattern to the entire text (similar to RE2::FullMatch). // Returns true on a successful full match, false otherwise. -// -// This is a convenience function that only works with patterns passed as -// strings. If you want full match on a pre-compiled regex, use PartialMatch on -// a Regex with anchors (e.g., "\A(?:...)\z"). -// -// The reason we don't allow passing a pre-compiled regex is that the underlying -// Rust crate only supports partial search. Adding anchors under the hood would -// require an expensive recompilation step, which defeats the point of passing a -// pre-compiled regex in the first place. template bool FullMatch(absl::string_view text, absl::string_view pattern, Args&&... args) { // Use \A and \z to anchor to beginning and end of string even in multiline - // mode, with an non-capturing group to make sure the anchors work properly + // mode, with a non-capturing group to make sure the anchors work properly // with alternation (e.g. turning 'abc|xyz' into '\Aabc|xyz\z' is wrong). std::string anchored = absl::StrCat("\\A(?:", pattern, ")\\z"); - return PartialMatch(text, anchored, std::forward(args)...); + absl::StatusOr compiled_regex = Regex::Compile(anchored); + if (!compiled_regex.ok()) return false; + return FullMatch(text, *compiled_regex, std::forward(args)...); +} + +// Like FullMatch, but takes a pre-compiled Regex. +template +bool FullMatch(absl::string_view text, const Regex& regex, Args&&... args) { + if constexpr (sizeof...(args) == 0) { + return FullMatchN(text, regex, nullptr, 0); + } else { + Arg temp_args[] = {MakeArg(std::forward(args))...}; + return [](absl::string_view text, const Regex& regex, + Arg* args_array, std::index_sequence) { + const Arg* const ptrs[] = {&args_array[Is]...}; + return FullMatchN(text, regex, ptrs, sizeof...(Is)); + }(text, regex, temp_args, std::index_sequence_for{}); + } } // Matches the pattern to a prefix of the input string and advances the input // string view past the match using the array interface. -inline bool ConsumeN(absl::string_view* input, const Regex& r, +inline bool ConsumeN(absl::string_view* input, const Regex& regex, const Arg* const args[], int n) { - std::optional c = r.FindCaptures(*input); + std::optional captures = regex.FindCaptures(*input); // For Consume, we require the match to be at the beginning of the string. - if (c && c->GetMatch().Start() == 0) { - if (static_cast(n) > c->Len() - 1) return false; + if (captures && captures->GetMatch().Start() == 0) { + if (static_cast(n) > captures->Len() - 1) return false; for (int i = 0; i < n; ++i) { - if (!args[i]->Parse(c->Get(i + 1))) return false; + if (!args[i]->Parse(captures->Get(i + 1))) return false; } - input->remove_prefix(c->GetMatch().End()); + input->remove_prefix(captures->GetMatch().End()); return true; } return false; @@ -713,15 +765,15 @@ inline bool ConsumeN(absl::string_view* input, const Regex& r, // Searches for the first match anywhere in the input string, populates // arguments, and advances the input string view past the match using the array // interface. -inline bool FindAndConsumeN(absl::string_view* input, const Regex& r, +inline bool FindAndConsumeN(absl::string_view* input, const Regex& regex, const Arg* const args[], int n) { - std::optional c = r.FindCaptures(*input); - if (c) { - if (static_cast(n) > c->Len() - 1) return false; + std::optional captures = regex.FindCaptures(*input); + if (captures) { + if (static_cast(n) > captures->Len() - 1) return false; for (int i = 0; i < n; ++i) { - if (!args[i]->Parse(c->Get(i + 1))) return false; + if (!args[i]->Parse(captures->Get(i + 1))) return false; } - input->remove_prefix(c->GetMatch().End()); + input->remove_prefix(captures->GetMatch().End()); return true; } return false; @@ -738,23 +790,23 @@ bool Consume(absl::string_view* input, absl::string_view pattern, // Note that if multiple alternations could match at the beginning, the // first matching alternation will be picked (leftmost-first matching). std::string anchored = absl::StrCat("\\A(?:", pattern, ")"); - absl::StatusOr r = Regex::Compile(anchored); - if (!r.ok()) return false; - return Consume(input, *r, std::forward(args)...); + absl::StatusOr compiled_regex = Regex::Compile(anchored); + if (!compiled_regex.ok()) return false; + return Consume(input, *compiled_regex, std::forward(args)...); } // Like Consume, but takes a pre-compiled Regex. template -bool Consume(absl::string_view* input, const Regex& r, Args&&... args) { +bool Consume(absl::string_view* input, const Regex& regex, Args&&... args) { Arg temp_args[] = {MakeArg(std::forward(args))...}; if constexpr (sizeof...(args) == 0) { - return ConsumeN(input, r, nullptr, 0); + return ConsumeN(input, regex, nullptr, 0); } else { - return [](absl::string_view* input, const Regex& r, + return [](absl::string_view* input, const Regex& regex, Arg* args_array, std::index_sequence) { const Arg* const ptrs[] = {&args_array[Is]...}; - return ConsumeN(input, r, ptrs, sizeof...(Is)); - }(input, r, temp_args, std::index_sequence_for{}); + return ConsumeN(input, regex, ptrs, sizeof...(Is)); + }(input, regex, temp_args, std::index_sequence_for{}); } } @@ -764,26 +816,50 @@ bool Consume(absl::string_view* input, const Regex& r, Args&&... args) { template bool FindAndConsume(absl::string_view* input, absl::string_view pattern, Args&&... args) { - absl::StatusOr r = Regex::Compile(pattern); - if (!r.ok()) return false; - return FindAndConsume(input, *r, std::forward(args)...); + absl::StatusOr compiled_regex = Regex::Compile(pattern); + if (!compiled_regex.ok()) return false; + return FindAndConsume(input, *compiled_regex, std::forward(args)...); } // Like FindAndConsume, but takes a pre-compiled Regex. template -bool FindAndConsume(absl::string_view* input, const Regex& r, Args&&... args) { +bool FindAndConsume(absl::string_view* input, const Regex& regex, + Args&&... args) { Arg temp_args[] = {MakeArg(std::forward(args))...}; if constexpr (sizeof...(args) == 0) { - return FindAndConsumeN(input, r, nullptr, 0); + return FindAndConsumeN(input, regex, nullptr, 0); } else { - return [](absl::string_view* input, const Regex& r, + return [](absl::string_view* input, const Regex& regex, Arg* args_array, std::index_sequence) { const Arg* const ptrs[] = {&args_array[Is]...}; - return FindAndConsumeN(input, r, ptrs, sizeof...(Is)); - }(input, r, temp_args, std::index_sequence_for{}); + return FindAndConsumeN(input, regex, ptrs, sizeof...(Is)); + }(input, regex, temp_args, std::index_sequence_for{}); } } +// Replaces the first match of the pattern in `str` with `rewrite` (similar to +// RE2::Replace). +// Returns true if the pattern matches and a replacement occurs, false +// otherwise. If no match occurs, `str` is not modified. +// NOTE: The `rewrite` string uses `$1` syntax for capture groups, NOT `\1`. +bool Replace(std::string* str, const Regex& regex, absl::string_view rewrite); + +// Like Replace, but compiles `pattern` on the fly. +bool Replace(std::string* str, absl::string_view pattern, + absl::string_view rewrite); + +// Replaces successive non-overlapping occurrences of the pattern in `str` with +// `rewrite` (similar to RE2::GlobalReplace). +// Returns the number of replacements made. +// If no match occurs, `str` is not modified. +// NOTE: The `rewrite` string uses `$1` syntax for capture groups, NOT `\1`. +int GlobalReplace(std::string* str, const Regex& regex, + absl::string_view rewrite); + +// Like GlobalReplace, but compiles `pattern` on the fly. +int GlobalReplace(std::string* str, absl::string_view pattern, + absl::string_view rewrite); + namespace internal { // Implementation of ParseNumeric. It must be after Match is fully defined. diff --git a/regex/rust/Cargo.toml b/regex/rust/Cargo.toml index 8171de5..5c5bc0c 100644 --- a/regex/rust/Cargo.toml +++ b/regex/rust/Cargo.toml @@ -11,6 +11,7 @@ doctest = false [dependencies] num-traits = "0.2" regex = "1" +regex-automata = "0.4" regex-syntax = "0.8" [dev-dependencies] diff --git a/regex/rust/lib.rs b/regex/rust/lib.rs index 019ffc7..aa6641f 100644 --- a/regex/rust/lib.rs +++ b/regex/rust/lib.rs @@ -10,14 +10,9 @@ mod vec_u8; macro_rules! error { ($($arg:tt)*) => { eprintln!($($arg)*); }; } use num_traits::Num; -use regex::bytes::{ - CaptureMatches as InnerCaptureMatches, CaptureNames as InnerCaptureNames, - Captures as InnerCaptures, Match as InnerMatch, Matches as InnerMatches, Regex as InnerRegex, - RegexBuilder as InnerRegexBuilder, RegexSet as InnerRegexSet, - RegexSetBuilder as InnerRegexSetBuilder, SetMatches as InnerSetMatches, Split as InnerSplit, - SplitN as InnerSplitN, SubCaptureMatches as InnerSubCaptureMatches, -}; +use regex_automata::{meta, util::syntax, MatchKind}; use std::option::Option; +use std::sync::Arc; pub use vec_u8::VecU8; // Wrapper structs for structs in the regex package that we need to expose. We use `Option` for the @@ -31,66 +26,43 @@ pub use vec_u8::VecU8; // this is not acceptable in production code for some projects, so we return a sensible default // value and log an error instead. -/// Opaque wrapper for `Match`. 'h is the lifetime of the haystack. -#[derive(Default, Debug)] +/// An opaque representation of a match found in a haystack. 'h is the lifetime of the haystack. +#[derive(Clone, Default, Debug)] pub struct Match<'h> { - inner: Option>, + slice: &'h [u8], + start: usize, } impl<'h> Match<'h> { + pub fn new(haystack: &'h [u8], start: usize, end: usize) -> Self { + Self { slice: &haystack[start..end], start } + } + pub fn start(&self) -> usize { - self.inner.as_ref().map_or_else( - || { - error!("Use of moved-from Match"); - 0 - }, - |i| i.start(), - ) + self.start } + pub fn end(&self) -> usize { - self.inner.as_ref().map_or_else( - || { - error!("Use of moved-from Match"); - 0 - }, - |i| i.end(), - ) + self.start + self.slice.len() } + pub fn is_empty(&self) -> bool { - self.inner.as_ref().map_or_else( - || { - error!("Use of moved-from Match"); - true - }, - |i| i.is_empty(), - ) + self.slice.is_empty() } + pub fn len(&self) -> usize { - self.inner.as_ref().map_or_else( - || { - error!("Use of moved-from Match"); - 0 - }, - |i| i.len(), - ) + self.slice.len() } // NOTE(b/469976097): Decide if we want to support `range()` here. It can be implemented in the // C++ side with `start()` and `end()` anyway. pub fn as_str(&self) -> &'h [u8] { - self.inner.as_ref().map_or_else( - || { - error!("Use of moved-from Match"); - const EMPTY: &[u8] = &[]; - EMPTY - }, - |i| i.as_bytes(), - ) + self.slice } pub fn as_bytes(&self) -> &'h [u8] { - self.inner.as_ref().unwrap().as_bytes() + self.slice } pub fn parse_as_i8(&self, radix: i32) -> Result { @@ -141,7 +113,7 @@ where // An ASCII string is always valid UTF-8. let string = - str::from_utf8(slice).map_err::(|_| "Invalid Utf8".to_string().into())?; + std::str::from_utf8(slice).map_err::(|_| "Invalid Utf8".to_string().into())?; // RE2 doesn't allow leading spaces for integers. if string.starts_with(|c: char| c.is_whitespace()) { @@ -241,81 +213,91 @@ where return Err("Non-ASCII match is not a valid float".to_string().into()); } // An ASCII string is always valid UTF-8. - let s = str::from_utf8(slice).map_err::(|_| "Invalid Utf8".to_string().into())?; + let s = + std::str::from_utf8(slice).map_err::(|_| "Invalid Utf8".to_string().into())?; // RE2 allows leading spaces for floats. s.trim_start().parse::().map_err(|e| { format!("Error parsing {} as a {}: {}", s, std::any::type_name::(), e).into() }) } -impl<'h> From> for Match<'h> { - fn from(m: InnerMatch<'h>) -> Self { - Match { inner: Some(m) } - } -} - /// Opaque wrapper for `Matches`, an iterator over matches in a haystack. /// 'r is the lifetime of the compiled regex, 'h is the lifetime of the haystack. #[derive(Default, Debug)] pub struct Matches<'r, 'h> { - inner: Option>, + haystack: &'h [u8], + inner: Option>, } impl<'r, 'h> Matches<'r, 'h> { // NOTE(b/483382648): Make `Matches` implement `Iterator`. #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Option> { + let haystack = self.haystack; self.inner.as_mut().map_or_else( || { error!("Use of moved-from Matches"); None }, - |i| i.next().map(Match::from), + |i| i.next().map(|m| Match::new(haystack, m.start(), m.end())), ) } } /// Opaque wrapper for `Captures`. 'h is the lifetime of the haystack. -#[derive(Default, Debug)] +#[derive(Clone, Default, Debug)] pub struct Captures<'h> { - inner: Option>, + haystack: &'h [u8], + inner: Option, } impl<'h> Captures<'h> { + pub fn new(haystack: &'h [u8], caps: regex_automata::util::captures::Captures) -> Self { + Self { haystack, inner: Some(caps) } + } + pub fn get(&self, i: usize) -> Option> { + let haystack = self.haystack; self.inner.as_ref().map_or_else( || { error!("Use of moved-from Captures"); None }, - |inner| inner.get(i).map(Match::from), + |inner| inner.get_group(i).map(|span| Match::new(haystack, span.start, span.end)), ) } + pub fn get_match(&self) -> Match<'h> { + let haystack = self.haystack; self.inner.as_ref().map_or_else( || { error!("Use of moved-from Captures"); Default::default() }, - |i| i.get_match().into(), + |i| i.get_match().map(|m| Match::new(haystack, m.start(), m.end())).unwrap_or_default(), ) } + pub fn name(&self, name: &str) -> Option> { + let haystack = self.haystack; self.inner.as_ref().map_or_else( || { error!("Use of moved-from Captures"); None }, - |inner| inner.name(name).map(Match::from), + |inner| { + inner.get_group_by_name(name).map(|span| Match::new(haystack, span.start, span.end)) + }, ) } + pub fn expand(&self, replacement: &[u8]) -> VecU8 { let Some(inner) = &self.inner else { error!("Use of moved-from Captures"); return VecU8::from(""); }; let mut dst = Vec::::new(); - inner.expand(replacement, &mut dst); + inner.interpolate_bytes_into(self.haystack, replacement, &mut dst); VecU8::from(dst) } @@ -327,9 +309,10 @@ impl<'h> Captures<'h> { error!("Use of moved-from Captures"); Default::default() }, - |i| SubCaptureMatches { inner: Some(i.iter()) }, + |i| SubCaptureMatches { caps: Some(self), index: 0, len: i.group_len() }, ) } + // `regex::Captures` doesn't have an `is_empty` method, so this wrapper doesn't either. #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { @@ -338,35 +321,35 @@ impl<'h> Captures<'h> { error!("Use of moved-from Captures"); 0 }, - |i| i.len(), + |i| i.group_len(), ) } } -impl<'h> From> for Captures<'h> { - fn from(m: InnerCaptures<'h>) -> Self { - Captures { inner: Some(m) } - } -} - -/// Opaque wrapper for `SubCaptureMatches`. 'c is the lifetime of the `Captures` value, and 'h is +/// An opaque iterator over the capture groups in a single match. 'c is the lifetime of the `Captures` value, and 'h is /// the lifetime of the haystack. #[derive(Default, Debug)] pub struct SubCaptureMatches<'c, 'h> { - inner: Option>, + caps: Option<&'c Captures<'h>>, + index: usize, + len: usize, } impl<'c, 'h> SubCaptureMatches<'c, 'h> { // NOTE(b/483382648): Make `SubCaptureMatches` implement `Iterator`. #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Option>> { - self.inner.as_mut().map_or_else( - || { - error!("Use of moved-from SubCaptureMatches"); - None - }, - |i| i.next().map(|maybe_match| maybe_match.map(Match::from)), - ) + if self.caps.is_none() { + error!("Use of moved-from SubCaptureMatches"); + return None; + } + if self.index >= self.len { + return None; + } + let caps = self.caps.unwrap(); + let res = caps.get(self.index); + self.index += 1; + Some(res) } } @@ -374,19 +357,21 @@ impl<'c, 'h> SubCaptureMatches<'c, 'h> { /// lifetime of the haystack. #[derive(Default, Debug)] pub struct CaptureMatches<'r, 'h> { - inner: Option>, + haystack: &'h [u8], + inner: Option>, } impl<'r, 'h> CaptureMatches<'r, 'h> { // NOTE(b/483382648): Make `CaptureMatches` implement `Iterator`. #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Option> { + let haystack = self.haystack; self.inner.as_mut().map_or_else( || { error!("Use of moved-from CaptureMatches"); None }, - |i| i.next().map(Captures::from), + |i| i.next().map(|caps| Captures::new(haystack, caps)), ) } } @@ -395,19 +380,21 @@ impl<'r, 'h> CaptureMatches<'r, 'h> { /// lifetime of the haystack. #[derive(Default, Debug)] pub struct Split<'r, 'h> { - inner: Option>, + haystack: &'h [u8], + inner: Option>, } impl<'r, 'h> Split<'r, 'h> { // NOTE(b/483382648): Make `Split` implement `Iterator`. #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Option<&'h [u8]> { + let haystack = self.haystack; self.inner.as_mut().map_or_else( || { error!("Use of moved-from Split"); None }, - |i| i.next(), + |i| i.next().map(|span| &haystack[span.start..span.end]), ) } } @@ -416,19 +403,21 @@ impl<'r, 'h> Split<'r, 'h> { /// lifetime of the haystack. #[derive(Default, Debug)] pub struct SplitN<'r, 'h> { - inner: Option>, + haystack: &'h [u8], + inner: Option>, } impl<'r, 'h> SplitN<'r, 'h> { // NOTE(b/483382648): Make `SplitN` implement `Iterator`. #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Option<&'h [u8]> { + let haystack = self.haystack; self.inner.as_mut().map_or_else( || { error!("Use of moved-from SplitN"); None }, - |i| i.next(), + |i| i.next().map(|span| &haystack[span.start..span.end]), ) } } @@ -437,7 +426,7 @@ impl<'r, 'h> SplitN<'r, 'h> { /// 'r is the lifetime of the compiled regular expression. #[derive(Default, Debug)] pub struct CaptureNames<'r> { - inner: Option>, + inner: Option>, } impl<'r> CaptureNames<'r> { @@ -456,38 +445,50 @@ impl<'r> CaptureNames<'r> { } } +/// Opaque wrapper for the result of a regex replacement operation, including +/// the number of replacements made and the resulting bytes. +#[derive(Clone, Default, Debug, PartialEq)] +pub struct ReplaceResult { + count: usize, + result: VecU8, +} + +impl ReplaceResult { + pub fn new(count: usize, result: VecU8) -> Self { + Self { count, result } + } + + pub fn count(&self) -> usize { + self.count + } + + pub fn result(&self) -> &VecU8 { + &self.result + } + + pub fn into_result(self) -> VecU8 { + self.result + } +} + /// Opaque wrapper for Regex object. We keep the inner regex in an Option to make this object /// implement Default, and thus be movable. #[derive(Clone, Default, Debug)] pub struct Regex { - inner: Option, + inner: Option, + pattern: Arc, } impl Regex { // Disable the Clippy warning in order to follow the Regex API. #[allow(clippy::new_ret_no_self)] pub fn new(val: &[u8]) -> Result { - let result = (|| -> Result { - let input = std::str::from_utf8(val) - .map_err::(|_| VecU8::from("Invalid Utf8".to_string()))?; - Ok(Regex { - inner: Some( - InnerRegex::new(input) - .map_err::(|_| VecU8::from("Invalid Utf8".to_string()))?, - ), - }) - })(); - result.into() + let builder = RegexBuilder::new(val); + builder.build() } pub fn as_str(&self) -> &str { - self.inner.as_ref().map_or_else( - || { - error!("Use of moved-from Regex"); - "" - }, - |i| i.as_str(), - ) + &self.pattern } pub fn is_match(&self, haystack: &[u8]) -> bool { @@ -506,7 +507,7 @@ impl Regex { error!("Use of moved-from Regex"); None }, - |i| i.find(haystack).map(Match::from), + |i| i.find(haystack).map(|m| Match::new(haystack, m.start(), m.end())), ) } @@ -516,7 +517,7 @@ impl Regex { error!("Use of moved-from Regex"); Default::default() }, - |i| Matches { inner: Some(i.find_iter(haystack)) }, + |i| Matches { haystack, inner: Some(i.find_iter(haystack)) }, ) } @@ -526,7 +527,15 @@ impl Regex { error!("Use of moved-from Regex"); None }, - |i| i.captures(haystack).map(Captures::from), + |i| { + let mut caps = i.create_captures(); + i.captures(haystack, &mut caps); + if caps.is_match() { + Some(Captures::new(haystack, caps)) + } else { + None + } + }, ) } @@ -536,7 +545,7 @@ impl Regex { error!("Use of moved-from Regex"); Default::default() }, - |i| CaptureMatches { inner: Some(i.captures_iter(haystack)) }, + |i| CaptureMatches { haystack, inner: Some(i.captures_iter(haystack)) }, ) } @@ -556,7 +565,9 @@ impl Regex { error!("Use of moved-from Regex"); Default::default() }, - |i| CaptureNames { inner: Some(i.capture_names()) }, + |i| CaptureNames { + inner: Some(i.group_info().pattern_names(regex_automata::PatternID::ZERO)), + }, ) } @@ -566,30 +577,37 @@ impl Regex { // NOTE(b/469976097): The original `replace*` methods support passing a function to perform // replacements. Decide if we want to support this. pub fn replace(&self, haystack: &[u8], rep: &[u8]) -> VecU8 { - let Some(inner) = &self.inner else { - error!("Use of moved-from Regex"); - return VecU8::from(""); - }; - let result = inner.replace(haystack, rep); - VecU8::from(result.into_owned()) + self.replacen(haystack, 1, rep).into_result() } pub fn replace_all(&self, haystack: &[u8], rep: &[u8]) -> VecU8 { - let Some(inner) = &self.inner else { - error!("Use of moved-from Regex"); - return VecU8::from(""); - }; - let result = inner.replace_all(haystack, rep); - VecU8::from(result.into_owned()) + self.replacen(haystack, usize::MAX, rep).into_result() } - pub fn replacen(&self, haystack: &[u8], limit: usize, rep: &[u8]) -> VecU8 { - let Some(inner) = &self.inner else { + pub fn replacen(&self, haystack: &[u8], limit: usize, rep: &[u8]) -> ReplaceResult { + let Some(re) = &self.inner else { error!("Use of moved-from Regex"); - return VecU8::from(""); + return ReplaceResult::default(); }; - let result = inner.replacen(haystack, limit, rep).to_vec(); - VecU8::from(result) + let limit = if limit == 0 { usize::MAX } else { limit }; + let mut it = re.captures_iter(haystack); + let mut new = Vec::with_capacity(haystack.len()); + let mut last_match = 0; + let mut count = 0; + while count < limit { + if let Some(caps) = it.next() { + if let Some(m) = caps.get_match() { + new.extend_from_slice(&haystack[last_match..m.start()]); + caps.interpolate_bytes_into(haystack, rep, &mut new); + last_match = m.end(); + count += 1; + } + } else { + break; + } + } + new.extend_from_slice(&haystack[last_match..]); + ReplaceResult { count, result: VecU8::from(new) } } pub fn split<'r, 'h>(&'r self, haystack: &'h [u8]) -> Split<'r, 'h> { @@ -598,7 +616,7 @@ impl Regex { error!("Use of moved-from Regex"); Default::default() }, - |i| Split { inner: Some(i.split(haystack)) }, + |i| Split { haystack, inner: Some(i.split(haystack)) }, ) } @@ -608,119 +626,136 @@ impl Regex { error!("Use of moved-from Regex"); Default::default() }, - |i| SplitN { inner: Some(i.splitn(haystack, limit)) }, + |i| SplitN { haystack, inner: Some(i.splitn(haystack, limit)) }, ) } } -/// An opaque wrapper for RegexBuilder. +// Default size limits matching the standard defaults of the upstream `regex` and `regex-automata` +// crates (from github.com/rust-lang/regex): +// - `DEFAULT_NFA_SIZE_LIMIT` (10 MiB): Matches `regex::RegexBuilder::size_limit` and +// `regex_automata::meta::Config::nfa_size_limit`, preventing memory explosion during Thompson NFA +// construction for complex patterns. +// - `DEFAULT_HYBRID_CACHE_CAPACITY` (2 MiB): Matches `regex::RegexBuilder::dfa_size_limit` and +// `regex_automata::meta::Config::hybrid_cache_capacity`, setting the capacity for the lazy Hybrid +// DFA transition cache. +const DEFAULT_NFA_SIZE_LIMIT: usize = 10 * (1 << 20); +const DEFAULT_HYBRID_CACHE_CAPACITY: usize = 2 * (1 << 20); + +/// An opaque builder for configuring and compiling a `Regex`. /// -/// In this case, we DO use the fact that `inner` can be none. We need to create a `str` from the -/// given pattern which, coming from C++, may contain invalid UTF-8. When it happens, we set `inner` -/// to `None`, turn all the setters into no-ops, and return a special error for this case from -/// `build()`. Other than that, it should behave exactly as the inner RegexBuilder. -#[derive(Default, Debug)] +/// In this case, we DO use the fact that `pattern` can be none. We need to create a `str` from the +/// given pattern which, coming from C++, may contain invalid UTF-8. When it happens, we set +/// `pattern` to `None`, turn all the setters into no-ops, and return a special error for this case +/// from `build()`. +#[derive(Clone, Debug)] pub struct RegexBuilder { - inner: Option, + pattern: Option, + metac: meta::Config, + syntaxc: syntax::Config, +} + +impl Default for RegexBuilder { + fn default() -> Self { + Self { + pattern: None, + metac: meta::Config::new() + // Standard leftmost-first match semantics (same as RE2 / PCRE). + .match_kind(MatchKind::LeftmostFirst) + // Allow empty matches on any byte offset (supports arbitrary byte haystacks). + .utf8_empty(false) + // Cap NFA memory to prevent resource exhaustion on pathological patterns. + .nfa_size_limit(Some(DEFAULT_NFA_SIZE_LIMIT)) + // Lazy Hybrid DFA cache capacity (2 MiB). + .hybrid_cache_capacity(DEFAULT_HYBRID_CACHE_CAPACITY) + // Disable fully ahead-of-time (AOT) dense DFA compilation to keep regex + // compilation fast and lightweight; regex-automata uses the lazy Hybrid DFA + // and PikeVM instead. + .dfa(false), + // Parse in byte-oriented mode to support raw byte slices. + syntaxc: syntax::Config::new().utf8(false), + } + } } impl RegexBuilder { pub fn new(pattern: &[u8]) -> Self { let s = std::str::from_utf8(pattern); - Self { - inner: if let Ok(pattern) = s { Some(InnerRegexBuilder::new(&pattern)) } else { None }, + RegexBuilder { + pattern: s.ok().map(|str_slice| str_slice.to_string()), + ..Default::default() } } pub fn build(&self) -> Result { - if let Some(inner) = &self.inner { - Ok(Regex { - inner: Some(inner.build().map_err::(|err| err.to_string().into())?), - }) + if let Some(pattern) = &self.pattern { + let metac = self.metac.clone().match_kind(MatchKind::LeftmostFirst).utf8_empty(false); + let syntaxc = self.syntaxc.utf8(false); + let meta = meta::Builder::new() + .configure(metac) + .syntax(syntaxc) + .build(pattern) + .map_err::(|err| err.to_string().into())?; + Ok(Regex { inner: Some(meta), pattern: Arc::from(pattern.as_str()) }) } else { - // The only reason to not have an `inner` value is if we couldn't create a pattern - // string from the input. Err("Invalid UTF-8 in pattern".to_string().into()) } } pub fn unicode(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.unicode(yes); - } + self.syntaxc = self.syntaxc.unicode(yes); } pub fn case_insensitive(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.case_insensitive(yes); - } + self.syntaxc = self.syntaxc.case_insensitive(yes); } pub fn multi_line(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.multi_line(yes); - } + self.syntaxc = self.syntaxc.multi_line(yes); } pub fn dot_matches_new_line(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.dot_matches_new_line(yes); - } + self.syntaxc = self.syntaxc.dot_matches_new_line(yes); } pub fn crlf(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.crlf(yes); - } + self.syntaxc = self.syntaxc.crlf(yes); } pub fn line_terminator(&mut self, byte: u8) { - if let Some(inner) = &mut self.inner { - inner.line_terminator(byte); - } + self.metac = self.metac.clone().line_terminator(byte); + self.syntaxc = self.syntaxc.line_terminator(byte); } pub fn swap_greed(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.swap_greed(yes); - } + self.syntaxc = self.syntaxc.swap_greed(yes); } pub fn ignore_whitespace(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.ignore_whitespace(yes); - } + self.syntaxc = self.syntaxc.ignore_whitespace(yes); } pub fn octal(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.octal(yes); - } + self.syntaxc = self.syntaxc.octal(yes); } pub fn size_limit(&mut self, bytes: usize) { - if let Some(inner) = &mut self.inner { - inner.size_limit(bytes); - } + self.metac = self.metac.clone().nfa_size_limit(Some(bytes)); } pub fn dfa_size_limit(&mut self, bytes: usize) { - if let Some(inner) = &mut self.inner { - inner.dfa_size_limit(bytes); - } + self.metac = self.metac.clone().hybrid_cache_capacity(bytes); } pub fn nest_limit(&mut self, limit: u32) { - if let Some(inner) = &mut self.inner { - inner.nest_limit(limit); - } + self.syntaxc = self.syntaxc.nest_limit(limit); } } /// Opaque wrapper for `SetMatches`. #[derive(Default, Debug)] pub struct SetMatches { - inner: Option, + inner: Option, } impl SetMatches { @@ -730,7 +765,13 @@ impl SetMatches { error!("Use of moved-from SetMatches"); false }, - |i| i.matched(regex_index), + |i| { + if let Ok(pid) = regex_automata::PatternID::new(regex_index) { + i.contains(pid) + } else { + false + } + }, ) } @@ -743,7 +784,7 @@ impl SetMatches { error!("Use of moved-from SetMatches"); 0 }, - |i| i.len(), + |i| i.capacity(), ) } @@ -757,7 +798,7 @@ impl SetMatches { /// Opaque wrapper for RegexSet. #[derive(Default, Debug)] pub struct RegexSet { - inner: Option, + inner: Option, } impl RegexSet { @@ -765,21 +806,8 @@ impl RegexSet { /// it returns an error. #[allow(clippy::new_ret_no_self)] // We need to return a Result because compilation can fail. pub fn new(patterns: &[&[u8]]) -> Result { - let result = (|| -> Result { - let mut pattern_strings = Vec::with_capacity(patterns.len()); - for p in patterns { - pattern_strings.push( - std::str::from_utf8(p).map_err::(|_| "Invalid pattern".into())?, - ); - } - Ok(RegexSet { - inner: Some( - InnerRegexSet::new(pattern_strings) - .map_err::(|err| err.to_string().into())?, - ), - }) - })(); - result.into() + let builder = RegexSetBuilder::new(patterns); + builder.build() } /// Returns true iff at least one of the regexes matches the haystack. @@ -801,7 +829,12 @@ impl RegexSet { error!("Use of moved-from RegexSet"); Default::default() }, - |i| SetMatches { inner: Some(i.matches(haystack)) }, + |i| { + let mut pset = regex_automata::PatternSet::new(i.pattern_len()); + let input = regex_automata::Input::new(haystack); + i.which_overlapping_matches(&input, &mut pset); + SetMatches { inner: Some(pset) } + }, ) } @@ -812,7 +845,7 @@ impl RegexSet { error!("Use of moved-from RegexSet"); 0 }, - |i| i.len(), + |i| i.pattern_len(), ) } @@ -823,15 +856,40 @@ impl RegexSet { error!("Use of moved-from RegexSet"); false }, - |i| i.is_empty(), + |i| i.pattern_len() == 0, ) } } -/// An opaque wrapper for RegexSetBuilder. -#[derive(Default, Debug)] +/// An opaque builder for configuring and compiling a `RegexSet`. +#[derive(Clone, Debug)] pub struct RegexSetBuilder { - inner: Option, + patterns: Option>, + metac: meta::Config, + syntaxc: syntax::Config, +} + +impl Default for RegexSetBuilder { + fn default() -> Self { + Self { + patterns: None, + metac: meta::Config::new() + // Report all matching pattern IDs in the set rather than stopping at the first. + .match_kind(MatchKind::All) + // Allow empty matches on any byte offset (supports arbitrary byte haystacks). + .utf8_empty(false) + // Disable capture group tracking since RegexSet only checks set membership. + .which_captures(regex_automata::nfa::thompson::WhichCaptures::None) + // Cap NFA memory to prevent resource exhaustion on pathological patterns. + .nfa_size_limit(Some(DEFAULT_NFA_SIZE_LIMIT)) + // Lazy Hybrid DFA cache capacity (2 MiB). + .hybrid_cache_capacity(DEFAULT_HYBRID_CACHE_CAPACITY) + // Disable fully ahead-of-time (AOT) dense DFA compilation. + .dfa(false), + // Parse in byte-oriented mode to support raw byte slices. + syntaxc: syntax::Config::new().utf8(false), + } + } } impl RegexSetBuilder { @@ -840,95 +898,82 @@ impl RegexSetBuilder { let mut ok = true; for p in patterns { if let Ok(s) = std::str::from_utf8(p) { - exprs.push(s); + exprs.push(s.to_string()); } else { ok = false; break; } } - Self { inner: if ok { Some(InnerRegexSetBuilder::new(exprs)) } else { None } } + RegexSetBuilder { patterns: if ok { Some(exprs) } else { None }, ..Default::default() } } pub fn build(&self) -> Result { - if let Some(inner) = &self.inner { - Ok(RegexSet { - inner: Some(inner.build().map_err::(|err| err.to_string().into())?), - }) + if let Some(patterns) = &self.patterns { + let metac = self + .metac + .clone() + .match_kind(MatchKind::All) + .utf8_empty(false) + .which_captures(regex_automata::nfa::thompson::WhichCaptures::None); + let syntaxc = self.syntaxc.utf8(false); + let meta = meta::Builder::new() + .configure(metac) + .syntax(syntaxc) + .build_many(patterns) + .map_err::(|err| err.to_string().into())?; + Ok(RegexSet { inner: Some(meta) }) } else { Err("Invalid UTF-8 in pattern".to_string().into()) } } pub fn unicode(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.unicode(yes); - } + self.syntaxc = self.syntaxc.unicode(yes); } pub fn case_insensitive(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.case_insensitive(yes); - } + self.syntaxc = self.syntaxc.case_insensitive(yes); } pub fn multi_line(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.multi_line(yes); - } + self.syntaxc = self.syntaxc.multi_line(yes); } pub fn dot_matches_new_line(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.dot_matches_new_line(yes); - } + self.syntaxc = self.syntaxc.dot_matches_new_line(yes); } pub fn crlf(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.crlf(yes); - } + self.syntaxc = self.syntaxc.crlf(yes); } pub fn line_terminator(&mut self, byte: u8) { - if let Some(inner) = &mut self.inner { - inner.line_terminator(byte); - } + self.metac = self.metac.clone().line_terminator(byte); + self.syntaxc = self.syntaxc.line_terminator(byte); } pub fn swap_greed(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.swap_greed(yes); - } + self.syntaxc = self.syntaxc.swap_greed(yes); } pub fn ignore_whitespace(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.ignore_whitespace(yes); - } + self.syntaxc = self.syntaxc.ignore_whitespace(yes); } pub fn octal(&mut self, yes: bool) { - if let Some(inner) = &mut self.inner { - inner.octal(yes); - } + self.syntaxc = self.syntaxc.octal(yes); } pub fn size_limit(&mut self, bytes: usize) { - if let Some(inner) = &mut self.inner { - inner.size_limit(bytes); - } + self.metac = self.metac.clone().nfa_size_limit(Some(bytes)); } pub fn dfa_size_limit(&mut self, bytes: usize) { - if let Some(inner) = &mut self.inner { - inner.dfa_size_limit(bytes); - } + self.metac = self.metac.clone().hybrid_cache_capacity(bytes); } pub fn nest_limit(&mut self, limit: u32) { - if let Some(inner) = &mut self.inner { - inner.nest_limit(limit); - } + self.syntaxc = self.syntaxc.nest_limit(limit); } } @@ -1165,4 +1210,93 @@ mod tests { check_bad_float!(f64, "1.0.0", "invalid float"); } + + #[gtest] + fn test_regex_builder_line_terminator() { + let mut builder = RegexBuilder::new(b"."); + builder.line_terminator(b'z'); + let regex = builder.build().unwrap(); + expect_that!(regex.is_match(b"\n"), eq(true)); + expect_that!(regex.is_match(b"z"), eq(false)); + + let mut builder_multiline = RegexBuilder::new(b"^abc$"); + builder_multiline.multi_line(true); + builder_multiline.line_terminator(b'z'); + let regex_multiline = builder_multiline.build().unwrap(); + expect_that!(regex_multiline.is_match(b"zabc"), eq(true)); + expect_that!(regex_multiline.is_match(b"abcz"), eq(true)); + expect_that!(regex_multiline.is_match(b"\nabc"), eq(false)); + } + + #[gtest] + fn test_regex_set_builder_line_terminator() { + let mut builder = RegexSetBuilder::new(&[b"."]); + builder.line_terminator(b'z'); + let set = builder.build().unwrap(); + expect_that!(set.is_match(b"\n"), eq(true)); + expect_that!(set.is_match(b"z"), eq(false)); + + let mut builder_multiline = RegexSetBuilder::new(&[b"^abc$"]); + builder_multiline.multi_line(true); + builder_multiline.line_terminator(b'z'); + let set_multiline = builder_multiline.build().unwrap(); + expect_that!(set_multiline.is_match(b"zabc"), eq(true)); + expect_that!(set_multiline.is_match(b"abcz"), eq(true)); + expect_that!(set_multiline.is_match(b"\nabc"), eq(false)); + } + + #[gtest] + fn test_regex_builder_options() { + // unicode + let mut builder = RegexBuilder::new(b"\\w+"); + builder.unicode(false); + let r = builder.build().unwrap(); + expect_that!(r.is_match("é".as_bytes()), eq(false)); + expect_that!(r.is_match(b"abc"), eq(true)); + + // crlf + let mut builder = RegexBuilder::new(b"^abc$"); + builder.multi_line(true); + builder.crlf(true); + let r = builder.build().unwrap(); + expect_that!(r.is_match(b"abc\r\n"), eq(true)); + + // swap_greed + let mut builder = RegexBuilder::new(b"a*"); + builder.swap_greed(true); + let r = builder.build().unwrap(); + expect_that!(r.find(b"aaa").unwrap().len(), eq(0)); + + // octal + let mut builder = RegexBuilder::new(b"\\101"); + builder.octal(true); + let r = builder.build().unwrap(); + expect_that!(r.is_match(b"A"), eq(true)); + + // nest_limit + let mut builder = RegexBuilder::new(b"ab"); + builder.nest_limit(0); + expect_that!(builder.build().is_err(), eq(true)); + } + + #[gtest] + fn test_regex_set_builder_options() { + // crlf + let mut builder = RegexSetBuilder::new(&[b"^abc$"]); + builder.multi_line(true); + builder.crlf(true); + let set = builder.build().unwrap(); + expect_that!(set.is_match(b"abc\r\n"), eq(true)); + + // octal + let mut builder = RegexSetBuilder::new(&[b"\\101"]); + builder.octal(true); + let set = builder.build().unwrap(); + expect_that!(set.is_match(b"A"), eq(true)); + + // nest_limit + let mut builder = RegexSetBuilder::new(&[b"ab"]); + builder.nest_limit(0); + expect_that!(builder.build().is_err(), eq(true)); + } }