Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions mrbgems/mruby-regexp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ simulation) with backtracking fallback.
- `(?#...)` comment group
- `(?<name>...)`, `(?'name'...)` named capture group
- `|` alternation
- `\1`-`\9` backreferences
- `\N` backreference: a digit run whose decimal value is at most 9 or at
most the number of groups opened before it; a run past both is an octal
escape (see below)
- `\k<name>`, `\k'name'` named backreferences
- `(?=...)` positive lookahead
- `(?!...)` negative lookahead
Expand All @@ -32,7 +34,10 @@ simulation) with backtracking fallback.
### Character Escapes

- `\n`, `\t`, `\r`, `\f`, `\v`, `\a`, `\e` control characters
- `\NNN` octal, one to three digits
- `\NNN` octal, one to three digits, when the digits spell no
backreference: `\101` is `A`, `\12` is a newline before twelve groups
and a backreference after them, `\0NN` is always octal; `\8` and `\9`
that spell no backreference are the digits themselves
- `\xHH` hex, one or two digits; `\x` with no digit raises `RegexpError`
- `\uXXXX` Unicode codepoint, exactly four hex digits
- `\u{...}` Unicode codepoints, one to six hex digits each, several of
Expand Down
99 changes: 83 additions & 16 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ typedef struct {
mrb_bool has_backref;
mrb_bool needs_backtrack;
mrb_bool dont_capture; /* pattern declares a named group: plain (...) does not capture */
uint16_t num_groups; /* groups opened so far, counting the plain ones a
named pattern demotes: what decides whether
`\NN` is a backreference or an octal escape */
uint32_t atomic_depth; /* how many (?>...) groups enclose the parse point */
uint32_t atom_start; /* where the atom a quantifier binds to begins;
compile_quantified sets it to the position
Expand Down Expand Up @@ -548,11 +551,11 @@ parse_escape(re_compiler *c)
case 'e': return 0x1b;
case 'b': return '\b'; /* backspace; only reachable inside [...] since the
top-level dispatcher emits RE_WBOUND for `\b` */
/* Octal escape `\NNN` (1-3 digits, value 0-255). The outer dispatcher
consumes `\1`-`\9` as backref, so the only octal-leading digit that
reaches here from the top level is `\0` -- but parse_escape also fires
from read_class_atom inside `[...]`, where backref parsing does not
apply, so the full 0-7 range needs handling. */
/* Octal escape `\NNN` (1-3 digits, value 0-255). From the top level a
digit other than `0` reaches here only once the dispatcher has ruled out
a backreference; inside `[...]` read_class_atom sends every digit here,
since a class has no backreferences. Three digits can spell up to
0777, and CRuby refuses what is past a byte rather than fold it. */
case '0': case '1': case '2': case '3':
case '4': case '5': case '6': case '7': {
int val = ch - '0';
Expand All @@ -564,7 +567,8 @@ parse_escape(re_compiler *c)
next_char(c);
n++;
}
return val & 0xff;
if (val > 0xff) compile_error(c, "invalid escape code");
return val;
}
/* Hex escape `\xHH` (1-2 hex digits, value 0-255). The `\x{HHHH}` form
for codepoints above 0xff is not implemented, and it is not read as
Expand Down Expand Up @@ -1448,7 +1452,11 @@ compile_atom(re_compiler *c)
/* Onigmo's ONIG_OPTION_DONT_CAPTURE_GROUP, which CRuby turns on for a
pattern that declares a named group: a plain (...) then groups
without capturing, so the numbered side counts only the named
groups. The named group itself keeps its number. */
groups. The named group itself keeps its number. The count of groups
opened is taken before that demotion: CRuby demotes plain groups
only once the parse is done, so while it reads the pattern every one
of them is still a group that a `\NN` may refer to. */
if (capturing && c->num_groups < UINT16_MAX) c->num_groups++;
if (c->dont_capture && cap_name == NULL) capturing = FALSE;

uint16_t group = 0;
Expand Down Expand Up @@ -1505,12 +1513,36 @@ compile_atom(re_compiler *c)
next_char(c);
ch = peek(c);
if (ch >= '1' && ch <= '9') {
if (c->dont_capture) {
compile_error(c, "numbered backref/call is not allowed. (use name)");
/* A digit run after the backslash is read as one decimal number first,
and is a backreference when that number is at most 9 or at most the
number of groups opened so far, as in CRuby (Onigmo's fetch_token):
`\1` and `\12` after twelve groups refer back, `\12` before them is
octal 012, a newline. What is not a backreference is an octal escape
of up to three digits (`\101` is `A`, `\1234` is `S4`), or the digit
itself when it starts with 8 or 9 (`\81` is `81`). Only the
comparison with the group count needs the number, so accumulation
stops once it is past every count a pattern can reach. */
uint32_t num = 0;
const char *q = c->p;
while (q < c->src_end && *q >= '0' && *q <= '9') {
if (num <= UINT16_MAX) num = num * 10 + (uint32_t)(*q - '0');
q++;
}
if (num <= 9 || num <= c->num_groups) {
if (c->dont_capture) {
compile_error(c, "numbered backref/call is not allowed. (use name)");
}
/* Not dont_capture, so every group counted captures and num is
within RE_MAX_CAPTURES. */
c->p = q;
emit(c, RE_BACKREF, (uint8_t)num, (c->flags & RE_FLAG_IGNORECASE) ? 1 : 0);
c->has_backref = TRUE;
}
else {
/* parse_escape() reads `\1`-`\7` as octal and `\8`, `\9` as the
digit itself, its default for a byte with no escape meaning. */
emit_char(c, (uint8_t)parse_escape(c));
}
next_char(c);
emit(c, RE_BACKREF, (uint8_t)(ch - '0'), (c->flags & RE_FLAG_IGNORECASE) ? 1 : 0);
c->has_backref = TRUE;
}
else if (ch == 'd' || ch == 'D' || ch == 'w' || ch == 'W' || ch == 's' || ch == 'S') {
next_char(c);
Expand Down Expand Up @@ -2054,20 +2086,33 @@ scope_get(const uint8_t *scope, mrb_int depth)
* Escaped characters (\ followed by anything) are preserved.
* skip_uninterpreted() decides which bytes those are.
*
* Removing whitespace must not join what it kept apart. An escape spelled
* with digits (`\1`, `\01`, `\x1`) takes the digits that follow it, so
* `\x1 2` copied as `\x12` would be one byte where CRuby, whose tokenizer
* stops at the space, reads two. So when whitespace went out between such
* an escape and a hex digit, an empty group `(?:)` goes in: it emits no
* instruction and keeps the digit an atom of its own. A removed comment,
* `#...` to the end of its line or `(?#...)`, does join the two: CRuby
* strips those before it tokenizes, and `\1(?#c)0` is `\10` there.
*
* The buffer comes from the GC arena, which holds it until the caller's frame
* is gone: the parser reads it from beginning to end, and every raise in
* between leaves this function nothing to be reached through. The scope
* stack lives behind the rewritten pattern in the same allocation: a group
* opener is one byte of source, so len bits is room for every group.
* opener is one byte of source, so len bits is room for every group. The
* pattern part is twice the source: each separator adds four bytes and
* takes an escape, a blank and a digit, at least four bytes, to occur.
*/
static char*
preprocess_pattern(mrb_state *mrb, const char *src, mrb_int len,
mrb_bool extended, mrb_int *out_len)
{
char *buf = (char*)mrb_temp_alloc(mrb, (size_t)len + ((size_t)len + 7) / 8);
uint8_t *scope = (uint8_t*)buf + len;
char *buf = (char*)mrb_temp_alloc(mrb, (size_t)len * 2 + ((size_t)len + 7) / 8);
uint8_t *scope = (uint8_t*)buf + len * 2;
mrb_int depth = 0;
mrb_int o = 0;
mrb_int esc_end = -1; /* where the last digit escape ended in buf */
mrb_bool blank_out = FALSE; /* whitespace was removed since */
mrb_bool in_class = FALSE;
const char *end = src + len;

Expand All @@ -2078,7 +2123,17 @@ preprocess_pattern(mrb_state *mrb, const char *src, mrb_int len,
not apply. */
const char *skip = skip_uninterpreted(src, end, &in_class);
if (skip) {
/* An escape spelled with digits: `\N`, `\x`, or `\u` outside the
`\u{...}` list form, whose brace closes it. What is copied here is
the backslash and the byte after it; the digits beyond are literals
to this pass, and the loop below keeps track of them. */
mrb_bool digits = (ch == '\\' && skip - src == 2 &&
(ISDIGIT(src[1]) || src[1] == 'x' || src[1] == 'u'));
while (src < skip) buf[o++] = *src++;
if (digits) {
esc_end = o;
blank_out = FALSE;
}
continue;
}
if (ch == ')') {
Expand Down Expand Up @@ -2135,15 +2190,27 @@ preprocess_pattern(mrb_state *mrb, const char *src, mrb_int len,
}
if (extended) {
if (ch == '#') {
/* skip to end of line */
/* Skip to the end of the line, newline included: the comment is
one removed span, not a comment and then a blank. */
while (src < end && *src != '\n') src++;
if (src < end) src++;
continue;
}
if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\f' || ch == '\v') {
src++;
blank_out = TRUE;
continue;
}
}
if (o == esc_end && hex_value((unsigned char)ch) >= 0) {
if (blank_out) {
memcpy(buf + o, "(?:)", 4); /* the digit is an atom of its own */
o += 4;
}
else {
esc_end = o + 1; /* the digit extends the escape */
}
}
buf[o++] = *src++;
}
*out_len = o;
Expand Down
77 changes: 77 additions & 0 deletions mrbgems/mruby-regexp/test/regexp_syntax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,21 @@
re = Regexp.new('a\\ b', Regexp::EXTENDED)
assert_true re.match?("a b")

# whitespace keeps an escape spelled with digits apart from a digit that
# follows it, as CRuby's tokenizer does: \x1 2 is two bytes, not \x12
assert_equal 0, (Regexp.new('\x1 2', Regexp::EXTENDED) =~ "\x012")
assert_nil Regexp.new('\x1 2', Regexp::EXTENDED) =~ "\x12"
assert_equal 0, (Regexp.new('\x1 a', Regexp::EXTENDED) =~ "\x01a")
assert_equal 0, (Regexp.new('\01 2', Regexp::EXTENDED) =~ "\x012")
assert_equal 0, (Regexp.new('(a)\1 0', Regexp::EXTENDED) =~ "aa0")
# what keeps them apart is no atom: a quantifier after the digit repeats
# the digit, and a lookbehind still measures a fixed width
assert_equal "aa000", Regexp.new('(a)\1 0+', Regexp::EXTENDED).match("aa000")[0]
assert_equal 2, (Regexp.new('(?<=\x1 2)x', Regexp::EXTENDED) =~ "\x012x")
assert_raise_with_message(RegexpError, "unmatched '(': /\\x1 2(/") do
Regexp.new('\x1 2(', Regexp::EXTENDED)
end

# a comment group is removed ahead of the line-comment pass, so its ')'
# survives the '#' inside it
re = Regexp.new("a (?#note) b", Regexp::EXTENDED)
Expand Down Expand Up @@ -1387,6 +1402,16 @@
assert_equal 0, (/[\x41]/ =~ "A")
assert_equal 0, (/[\101]/ =~ "A")
assert_equal 0, (/\x7/ =~ "\a")

# three octal digits can spell more than a byte, which is refused rather
# than folded to one, inside a class and out
assert_kind_of Regexp, Regexp.new('\377')
assert_raise_with_message(RegexpError, "invalid escape code: /\\400/") do
Regexp.new('\400')
end
assert_raise_with_message(RegexpError, "invalid escape code: /[\\400]/") do
Regexp.new('[\400]')
end
end

assert("Regexp - a hex escape needs at least one digit") do
Expand Down Expand Up @@ -1418,6 +1443,58 @@
assert_equal 0, (Regexp.new("[\\x4]") =~ "\x04")
end

assert("Regexp - a digit escape is a backreference or an octal escape by the group count") do
# Outside a class the digits after the backslash are read as one decimal
# number: a backreference when it is at most 9 or at most the number of
# groups opened before it, as CRuby reads it, and an octal escape of up
# to three digits otherwise. \0 is always octal.
assert_equal 0, (/\101/ =~ "A")
assert_equal 0, (/\12/ =~ "\n")
assert_equal 0, (/\100/ =~ "@")
assert_equal 0, (/\1234/ =~ "S4")
assert_equal 0, (/\18/ =~ "\x018")
assert_equal 0, (/\101/i =~ "a")
assert_equal "AA", /\101{2}/.match("AA")[0]
assert_equal 0, (/\303\244/ =~ "ä")

# 8 and 9 are no octal digits, so what is not a backreference is the
# digit itself
assert_equal 0, (/\81/ =~ "81")
assert_equal 0, (/\99/ =~ "99")

# the count is taken where the escape stands, so the same \10 refers back
# after ten groups and is octal 010 before them
ten = "(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)"
assert_equal 0, (Regexp.new("#{ten}\\10") =~ "abcdefghijj")
assert_nil Regexp.new("#{ten}\\10") =~ "abcdefghij\b"
assert_equal 0, (Regexp.new("#{ten}\\10{2}") =~ "abcdefghijjj")
assert_equal 0, (Regexp.new("#{ten}\\11") =~ "abcdefghij\t")
assert_equal 0, (Regexp.new("#{ten}(k)\\11") =~ "abcdefghijkk")
assert_equal 0, (Regexp.new("\\10#{ten}") =~ "\babcdefghij")
assert_equal 0, (/(a)(b)(c)(d)(e)(f)(g)(h)(i)\10/ =~ "abcdefghi\b")

# A named pattern counts its plain groups too, since CRuby demotes them
# only once the parse is done: what the count makes a backreference is
# then refused by number, and what it makes an octal escape is read.
msg = "numbered backref/call is not allowed. (use name)"
assert_equal 0, (/(?<n>a)\101/ =~ "aA")
assert_equal 0, (/(?<n>a)\10/ =~ "a\b")
assert_equal 0, (/(?<n>a)(?<m>b)(c)(d)(e)(f)(g)(h)(i)\10/ =~ "abcdefghi\b")
assert_raise_with_message(RegexpError, "#{msg}: /(?<n>a)\\9/") do
Regexp.new("(?<n>a)\\9")
end
assert_raise_with_message(RegexpError, "#{msg}: /(?<n>a)(?<m>b)(c)(d)(e)(f)(g)(h)(i)(j)\\10/") do
Regexp.new("(?<n>a)(?<m>b)(c)(d)(e)(f)(g)(h)(i)(j)\\10")
end

# Under /x whitespace ends the number and a comment does not, as in
# CRuby, whose tokenizer stops at whitespace but never sees a comment.
assert_equal 0, (Regexp.new("\\10 1", Regexp::EXTENDED) =~ "\b1")
assert_equal 0, (Regexp.new("(a)\\1 0", Regexp::EXTENDED) =~ "aa0")
assert_equal 0, (Regexp.new("(a)\\1#c\n0", Regexp::EXTENDED) =~ "a\b")
assert_equal 0, (/(a)\1(?#c)0/ =~ "a\b")
end

assert("Regexp - \\h and \\H hex-digit shorthands") do
assert_equal 0, (/\h/ =~ "f")
assert_nil (/\h/ =~ "g")
Expand Down
Loading