Skip to content

Commit 452449e

Browse files
authored
Merge pull request #95 from Koan-Bot/koan.atoomic/validate-regex-search
2 parents 4580f71 + 5f7809d commit 452449e

File tree

2 files changed

+97
-0
lines changed

2 files changed

+97
-0
lines changed

src/lib/GrepCpan/Grep.pm

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,26 @@ sub _get_git_grep_flavor($s) {
232232
return q{-P};
233233
}
234234

235+
# Validate a search string as a PCRE pattern.
236+
# Returns undef if valid, or the error message if invalid.
237+
sub _validate_pcre_pattern($s) {
238+
return undef unless defined $s;
239+
return undef if _get_git_grep_flavor($s) eq q{--fixed-string};
240+
241+
local $@;
242+
eval {
243+
no warnings 'regexp'; # user patterns may have unescaped braces
244+
qr/$s/;
245+
};
246+
if ($@) {
247+
my $err = $@;
248+
$err =~ s{ at .+ line \d+.*}{}s; # strip Perl location info
249+
$err =~ s{^\s+|\s+$}{}g;
250+
return $err;
251+
}
252+
return undef;
253+
}
254+
235255
# idea use git rev-parse HEAD to include it in the cache name
236256

237257
sub do_search ( $self, %opts ) {
@@ -250,6 +270,28 @@ sub do_search ( $self, %opts ) {
250270

251271
$search = _sanitize_search($search);
252272

273+
# Validate regex before running git grep — invalid PCRE would silently
274+
# return empty results, confusing users.
275+
if ( my $regex_error = _validate_pcre_pattern($search) ) {
276+
my $elapsed = sprintf( "%.3f",
277+
Time::HiRes::tv_interval( $t0, [Time::HiRes::gettimeofday] ) );
278+
return {
279+
is_incomplete => 0,
280+
search_in_progress => 0,
281+
match => { files => 0, distros => 0 },
282+
adjusted_request => {
283+
q => {
284+
error => "Invalid regular expression: $regex_error",
285+
value => $search,
286+
},
287+
},
288+
results => [],
289+
time_elapsed => $elapsed,
290+
is_a_known_distro => 0,
291+
version => $self->current_version(),
292+
};
293+
}
294+
253295
my $results = $self->_do_search(%opts);
254296

255297
my $cache = $results->{cache};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use strict;
2+
use warnings;
3+
4+
use Test2::Bundle::Extended;
5+
use Test2::Tools::Explain;
6+
use Test2::Plugin::NoWarnings;
7+
8+
use GrepCpan::Grep;
9+
10+
# Valid patterns — _validate_pcre_pattern returns undef
11+
my @valid = (
12+
q{\d+},
13+
q{[a-z]+},
14+
q{\bfoo\b},
15+
q{foo(bar)?},
16+
q{\(\{\d+(,\d+)?\}},
17+
q{open\(},
18+
q{^start},
19+
q{end$},
20+
q{a|b|c},
21+
);
22+
23+
# Fixed-string searches — always valid (skipped by validator)
24+
my @fixed = (
25+
undef,
26+
q{hello world},
27+
q{simple},
28+
);
29+
30+
# Invalid PCRE patterns — returns an error string
31+
my @invalid = (
32+
q{open(}, # unmatched paren
33+
q{[unclosed}, # unmatched bracket
34+
q{*greedy}, # quantifier without target
35+
q{+also bad}, # quantifier without target
36+
q{(?P<broken}, # incomplete named group
37+
);
38+
39+
for my $pattern (@valid) {
40+
is GrepCpan::Grep::_validate_pcre_pattern($pattern), undef,
41+
"valid PCRE: $pattern";
42+
}
43+
44+
for my $pattern (@fixed) {
45+
is GrepCpan::Grep::_validate_pcre_pattern($pattern), undef,
46+
"fixed-string (skipped): " . ( $pattern // 'undef' );
47+
}
48+
49+
for my $pattern (@invalid) {
50+
my $err = GrepCpan::Grep::_validate_pcre_pattern($pattern);
51+
ok defined $err, "invalid PCRE detected: $pattern";
52+
ok length($err) > 0, "error message is not empty for: $pattern";
53+
}
54+
55+
done_testing;

0 commit comments

Comments
 (0)