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
27 changes: 24 additions & 3 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,16 @@ class_add_shorthand(re_charclass *cc, int ch)
}
}

/* TRUE when every character the class can match is ASCII, so it always
consumes exactly one byte. Non-ASCII codepoint ranges and the utf8_any
catch-all (set by \D, \W, \S, \H and [[:^alpha:]]) both admit multibyte
characters, whose width is not known until match time. */
static mrb_bool
class_is_ascii_only(const re_charclass *cc)
{
return cc->num_ranges == 0 && !cc->utf8_any;
}

/* Set ASCII bits for a POSIX class name (e.g. "alpha") into a 128-bit map.
Returns FALSE for an unknown name. Semantics are ASCII, like this gem's
\w/\d shorthands; non-ASCII codepoints are not classified. */
Expand Down Expand Up @@ -560,11 +570,23 @@ compute_fixed_len(re_compiler *c, uint32_t start, uint32_t end)
re_inst inst = c->code[pc];
switch (inst.op) {
case RE_CHAR:
/* a multibyte literal is a run of one-byte RE_CHAR instructions,
so each one is exactly one byte by construction */
len += 1;
pc++;
break;
case RE_CLASS:
case RE_NCLASS:
/* a class that admits a multibyte character has no single byte
length: one holding both ASCII and non-ASCII members consumes one
byte here and two there. Rewinding by a wrong count lands in the
middle of a character, so refuse to measure it. */
if (!class_is_ascii_only(&c->classes[inst.a])) return -1;
len += 1;
pc++;
break;
case RE_NCLASS:
/* the complement of an ASCII bitmap always admits non-ASCII */
return -1;
case RE_ANY:
case RE_ANY_NL:
/* . matches one character which can be 1-4 bytes in UTF-8.
Expand Down Expand Up @@ -1417,8 +1439,7 @@ first_set_walk(const re_inst *code, uint32_t code_len,
case RE_CLASS: {
const re_charclass *cc = &classes[code[pc].a];
for (int i = 0; i < 16; i++) bm[i] |= cc->bitmap[i];
if (cc->utf8_any) return FALSE; /* non-ASCII possible */
if (cc->num_ranges > 0) return FALSE; /* non-ASCII codepoints possible */
if (!class_is_ascii_only(cc)) return FALSE; /* non-ASCII possible */
return TRUE;
}
case RE_NCLASS: {
Expand Down
31 changes: 31 additions & 0 deletions mrbgems/mruby-regexp/test/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,37 @@ def -(other)
assert_equal "a", md[0]
end

assert("Regexp - lookbehind rejects a class that can match a multibyte character") do
# A class holding non-ASCII members consumes one byte here and two there,
# so no single rewind width is right. Refusing the pattern beats rewinding
# into the middle of a character, where a positive lookbehind reports no
# match and a negative one reports a match.
assert_raise(RegexpError) { Regexp.new("(?<=[Ā])x") }
assert_raise(RegexpError) { Regexp.new("(?<![Ā])b") }
assert_raise(RegexpError) { Regexp.new("(?<=[Ā-ă])x") }
assert_raise(RegexpError) { Regexp.new("(?<=[aĀ])x") }
assert_raise(RegexpError) { Regexp.new("(?<=[Ā]{2})x") }
# a negated class always admits non-ASCII, whatever its members are
assert_raise(RegexpError) { Regexp.new("(?<=[^あ])x") }
assert_raise(RegexpError) { Regexp.new("(?<![^a])b") }
# the uppercase shorthands carry the same catch-all
assert_raise(RegexpError) { Regexp.new("(?<=a\\W)x") }
assert_raise(RegexpError) { Regexp.new("(?<=\\W\\W)x") }
assert_raise(RegexpError) { Regexp.new("(?<=\\D)x") }
assert_raise(RegexpError) { Regexp.new("(?<=\\S)x") }
end

assert("Regexp - lookbehind measures an ASCII-only class") do
assert_equal "x", "ax".match(/(?<=[a-z])x/)[0]
assert_nil "1x".match(/(?<=[a-z])x/)
assert_equal "x", "1x".match(/(?<=\d)x/)[0]
assert_equal "x", " x".match(/(?<=\s)x/)[0]
# a multibyte literal compiles to a run of one-byte instructions, so it
# keeps its exact width and must keep measuring
assert_equal "x", "Āx".match(/(?<=Ā)x/)[0]
assert_nil "bx".match(/(?<=Ā)x/)
end

assert("$1-$9 global variables") do
/(\w+)\s(\w+)/ =~ "hello world"
assert_equal "hello", $1
Expand Down
Loading