diff --git a/src/matcher.rs b/src/matcher.rs index adda24a..324e75e 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -9,7 +9,7 @@ use onig::{ SyntaxBehavior, SyntaxOperator, }; use onig_sys::{ - ONIGERR_EMPTY_RANGE_IN_CHAR_CLASS, ONIGERR_RETRY_LIMIT_IN_MATCH_OVER, + ONIGERR_EMPTY_RANGE_IN_CHAR_CLASS, ONIGERR_INVALID_BACKREF, ONIGERR_RETRY_LIMIT_IN_MATCH_OVER, ONIGERR_RETRY_LIMIT_IN_SEARCH_OVER, OnigEncCtype_ONIGENC_CTYPE_WORD, OnigEncodingUTF8, }; use std::io; @@ -248,22 +248,29 @@ impl CompiledPattern { options |= RegexOptions::REGEX_OPTION_IGNORECASE; } - fn compile_with(pattern: &str, syntax: &Syntax, options: RegexOptions) -> UResult { + let mode = config.regex_mode; + fn compile_with( + pattern: &str, + syntax: &Syntax, + options: RegexOptions, + mode: RegexMode, + ) -> UResult { Regex::with_options_and_encoding(pattern, options, syntax).map_err(|err| { // Prefer GNU grep's wording for the errors it has a dedicated // message for; fall back to oniguruma's text otherwise. - match gnu_error_message(err.code()) { + match gnu_error_message(err.code(), mode) { Some(msg) => USimpleError::new(2, msg.to_string()), None => USimpleError::new(2, format!("invalid pattern \"{pattern}\": {err}")), } }) } - let leftmost = compile_with(pattern, &syntax, options)?; + let leftmost = compile_with(pattern, &syntax, options, mode)?; let longest_anchored = compile_with( pattern, &syntax, options | RegexOptions::REGEX_OPTION_FIND_LONGEST, + mode, )?; Ok(Self { leftmost, @@ -336,10 +343,16 @@ fn match_error(err: Error) -> io::Error { /// `Invalid range end`) rather than oniguruma's phrasing, so translating keeps /// us byte-compatible. Returns `None` for errors with no GNU equivalent, where /// the caller falls back to oniguruma's own message. -fn gnu_error_message(code: i32) -> Option<&'static str> { +fn gnu_error_message(code: i32, mode: RegexMode) -> Option<&'static str> { match code { // e.g. `[b-a]`: a range whose end precedes its start. ONIGERR_EMPTY_RANGE_IN_CHAR_CLASS => Some("Invalid range end"), + // e.g. `(.)\2`: a back-reference to a group that does not exist. GNU + // (via PCRE2) and gnulib's regex word this differently. + ONIGERR_INVALID_BACKREF if mode == RegexMode::Perl => { + Some("reference to non-existent subpattern") + } + ONIGERR_INVALID_BACKREF => Some("Invalid back reference"), _ => None, } } diff --git a/tests/test_grep.rs b/tests/test_grep.rs index bfe458b..d3d5d77 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -130,10 +130,11 @@ fn ere_invalid_pattern_is_error() { fn confusing_bracket_class_is_error() { // GNU grep rejects the misspelled `[:name:]` form (meant to be // `[[:name:]]`) with a dedicated diagnostic and exit code 2. + // No piped input: the pattern is rejected at compile time, before stdin is + // read, so feeding stdin would race with the child exiting (broken pipe). for pattern in ["[:space:]", "[:digit:]", "[^:space:]", "x[:space:]y"] { let (_s, mut c) = ucmd(); c.args(&[pattern]) - .pipe_in("x\n") .fails_with_code(2) .stderr_is("grep: character class syntax is [[:space:]], not [:space:]\n"); } @@ -141,7 +142,6 @@ fn confusing_bracket_class_is_error() { // The same diagnostic applies in extended mode. let (_s, mut c) = ucmd(); c.args(&["-E", "[:space:]"]) - .pipe_in("x\n") .fails_with_code(2) .stderr_is("grep: character class syntax is [[:space:]], not [:space:]\n"); } @@ -184,10 +184,10 @@ fn lookalike_brackets_are_not_confusing() { fn reversed_range_uses_gnu_wording() { // A range like `[b-a]` is an error; GNU prints the bare POSIX diagnostic // "Invalid range end" (not oniguruma's phrasing) and exits 2. + // No piped input: the pattern is rejected before stdin is read. for args in [&["[b-a]"][..], &["-E", "[b-a]"][..]] { let (_s, mut c) = ucmd(); c.args(args) - .pipe_in("x\n") .fails_with_code(2) .stderr_is("grep: Invalid range end\n"); } @@ -205,6 +205,33 @@ fn pcre_backtracking_limit_does_not_abort() { .stderr_contains("backtracking limit"); } +#[test] +fn invalid_backreference_uses_gnu_wording() { + // A back-reference to a non-existent group is worded differently by GNU + // depending on the engine: PCRE (-P) vs gnulib regex (BRE/ERE). + // No piped input: these patterns are rejected before stdin is read. + let (_s, mut c) = ucmd(); + c.args(&["-P", r"(.)\2"]) + .fails_with_code(2) + .stderr_is("grep: reference to non-existent subpattern\n"); + + for args in [&["-E", r"(.)\2"][..], &[r"\(.\)\2"][..]] { + let (_s, mut c) = ucmd(); + c.args(args) + .fails_with_code(2) + .stderr_is("grep: Invalid back reference\n"); + } + + // A valid back-reference with -Pw / -Px must still match. + for flag in ["-Pw", "-Px"] { + let (_s, mut c) = ucmd(); + c.args(&[flag, r"(.)\1"]) + .pipe_in("aa\n") + .succeeds() + .stdout_is("aa\n"); + } +} + #[test] fn fixed_string_is_literal() { // Metacharacters are not interpreted.