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
5 changes: 5 additions & 0 deletions mrbgems/mruby-regexp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ pattern analysis.
only.
- **Step limit on backtracking**: Patterns that require the
backtracking engine are subject to a step limit.
- **No inline extended mode**: `(?x)` and `(?x:...)` raise a
`RegexpError`, because extended mode is applied to the whole pattern
before it is parsed. A `-x` is accepted and ignored, so inside a
pattern that is itself extended it does not bring back the
whitespace that pass removed.

## Configuration

Expand Down
20 changes: 17 additions & 3 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,9 @@ compute_fixed_len(re_compiler *c, uint32_t start, uint32_t end)
(':' or ')'). `base` is the option set in effect on entry; the resulting
set is returned. Ruby's inline letters are i (IGNORECASE), m (DOTALL),
x (EXTENDED). Extended mode is applied by a whole-pattern preprocessing
pass, so it cannot be scoped inline and is rejected here. */
pass that runs before the parser, so it cannot be scoped inline:
enabling it is rejected here, and see the 'x' branch below for why
disabling it is not. */
static uint32_t
parse_inline_flags(re_compiler *c, uint32_t base)
{
Expand All @@ -605,8 +607,20 @@ parse_inline_flags(re_compiler *c, uint32_t base)
if (oc == 'i') bit = RE_FLAG_IGNORECASE;
else if (oc == 'm') bit = RE_FLAG_DOTALL;
else if (oc == 'x') {
compile_error(c, "inline extended mode (?x) is not supported");
return base; /* unreached: compile_error longjmps */
if (!negate) {
compile_error(c, "inline extended mode (?x) is not supported");
return base; /* unreached: compile_error longjmps */
}
/* A '-x' is accepted and dropped. Regexp#to_s names every flag that
is off, so its result carries one whenever the pattern is not
extended, and rejecting it would make interpolation and
Regexp.new(re.to_s) raise for such a Regexp. Dropping it is exact
there, since the flag is already off. Inside a pattern that is
itself extended it is not: the preprocessing pass has removed the
whitespace by now and the scope cannot get it back. */
seen = TRUE;
next_char(c);
continue;
}
else if (oc == '-' && !negate) { negate = TRUE; next_char(c); continue; }
else break;
Expand Down
49 changes: 42 additions & 7 deletions mrbgems/mruby-regexp/src/regexp.c
Original file line number Diff line number Diff line change
Expand Up @@ -510,19 +510,52 @@ regexp_casefold_p(mrb_state *mrb, mrb_value self)
return mrb_bool_value((get_iflags(mrb, self) & RE_FLAG_IGNORECASE) != 0);
}

/* The flag letters of the displayed forms, in the order Ruby writes them.
Regexp#to_s and Regexp#inspect both walk this table, so the two cannot
drift apart. RE_FLAG_DOTALL is the other half of Ruby's `m` and is
always set together with RE_FLAG_MULTILINE, so testing one of the pair
is enough. */
static const struct {
uint32_t bit;
char letter;
} re_flag_letters[] = {
{ RE_FLAG_MULTILINE, 'm' },
{ RE_FLAG_IGNORECASE, 'i' },
{ RE_FLAG_EXTENDED, 'x' },
};

#define RE_FLAG_LETTER_COUNT (sizeof(re_flag_letters) / sizeof(re_flag_letters[0]))

/*
* Regexp#to_s - CRuby-compatible (?flags:source) format
* Regexp#to_s - (?on-off:source) format
*
* The flags that are off are named after a '-', and that run is left out
* only when none of them are. Spelling them out is what keeps the result
* meaningful once it is interpolated into another pattern: written as
* "(?i:a)", the embedded source in /#{/a/i}b/m would pick up the
* enclosing pattern's flags instead of carrying only its own.
*/
static mrb_value
regexp_to_s(mrb_state *mrb, mrb_value self)
{
mrb_value src = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@source"));
uint32_t flags = get_iflags(mrb, self);
char off[RE_FLAG_LETTER_COUNT];
mrb_int noff = 0;

mrb_value result = mrb_str_new_lit(mrb, "(?");
if (flags & RE_FLAG_IGNORECASE) mrb_str_cat_lit(mrb, result, "i");
if (flags & RE_FLAG_MULTILINE) mrb_str_cat_lit(mrb, result, "m");
if (flags & RE_FLAG_EXTENDED) mrb_str_cat_lit(mrb, result, "x");
for (size_t i = 0; i < RE_FLAG_LETTER_COUNT; i++) {
if (flags & re_flag_letters[i].bit) {
mrb_str_cat(mrb, result, &re_flag_letters[i].letter, 1);
}
else {
off[noff++] = re_flag_letters[i].letter;
}
}
if (noff > 0) {
mrb_str_cat_lit(mrb, result, "-");
mrb_str_cat(mrb, result, off, noff);
}
mrb_str_cat_lit(mrb, result, ":");
mrb_str_cat_str(mrb, result, src);
mrb_str_cat_lit(mrb, result, ")");
Expand All @@ -538,9 +571,11 @@ regexp_inspect(mrb_state *mrb, mrb_value self)
mrb_value result = mrb_str_new_lit(mrb, "/");
mrb_str_cat_str(mrb, result, src);
mrb_str_cat_lit(mrb, result, "/");
if (flags & RE_FLAG_IGNORECASE) mrb_str_cat_lit(mrb, result, "i");
if (flags & RE_FLAG_MULTILINE) mrb_str_cat_lit(mrb, result, "m");
if (flags & RE_FLAG_EXTENDED) mrb_str_cat_lit(mrb, result, "x");
for (size_t i = 0; i < RE_FLAG_LETTER_COUNT; i++) {
if (flags & re_flag_letters[i].bit) {
mrb_str_cat(mrb, result, &re_flag_letters[i].letter, 1);
}
}
return result;
}

Expand Down
44 changes: 38 additions & 6 deletions mrbgems/mruby-regexp/test/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,21 @@
assert_equal 0, (/(?m:a.b)/ =~ "a\nb")
assert_nil (/a.b/ =~ "a\nb")

# x (extended) cannot be scoped inline with the current architecture.
# x (extended) cannot be scoped inline with the current architecture, so
# turning it on is rejected.
assert_raise(RegexpError) { Regexp.new("(?x)a b") }
assert_raise(RegexpError) { Regexp.new("(?x:a b)") }

# Turning it off is accepted, because Regexp#to_s writes a '-x' for every
# pattern that is not extended and that form has to recompile.
assert_equal 0, (/(?-x:a b)/ =~ "a b")
assert_equal 0, (/(?i-mx:a)b/ =~ "Ab")
assert_true Regexp.new("(?-mix:a b)").match?("a b")

# The '-x' is dropped rather than honoured, so in a pattern that is
# itself extended the whitespace stays stripped. CRuby matches "a b"
# here.
assert_true Regexp.new("(?-x:a b)", Regexp::EXTENDED).match?("ab")
end

assert("Regexp - comment groups (?#...)") do
Expand Down Expand Up @@ -742,13 +755,32 @@
assert("Regexp#inspect") do
re = Regexp.new("abc", Regexp::IGNORECASE)
assert_equal "/abc/i", re.inspect
# several flags are written in the m, i, x order, whatever order they
# were given in
assert_equal "/abc/mi", Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE).inspect
assert_equal "/abc/mix", Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE | Regexp::EXTENDED).inspect
end

assert("Regexp#to_s") do
assert_equal "(?:abc)", Regexp.new("abc").to_s
assert_equal "(?i:abc)", Regexp.new("abc", Regexp::IGNORECASE).to_s
assert_equal "(?m:abc)", Regexp.new("abc", Regexp::MULTILINE).to_s
assert_equal "(?im:abc)", Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE).to_s
assert_equal "(?-mix:abc)", Regexp.new("abc").to_s
assert_equal "(?i-mx:abc)", Regexp.new("abc", Regexp::IGNORECASE).to_s
assert_equal "(?m-ix:abc)", Regexp.new("abc", Regexp::MULTILINE).to_s
assert_equal "(?mi-x:abc)", Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE).to_s
# the '-' run is dropped only when no flag is off
assert_equal "(?mix:abc)", Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE | Regexp::EXTENDED).to_s

# the form recompiles, and the flags it names do not leak either way
assert_true Regexp.new(Regexp.new("abc", Regexp::IGNORECASE).to_s).match?("ABC")
assert_false Regexp.new(Regexp.new("abc").to_s + "d", Regexp::IGNORECASE).match?("ABCd")
end

assert("Regexp#to_s - interpolation") do
inner = Regexp.new("abc", Regexp::IGNORECASE)
# the inner Regexp keeps its own flags where the outer has none
assert_true(/#{inner}d/.match?("ABCd"))
assert_false(/#{inner}d/.match?("ABCD"))
# and does not pick up the outer ones
assert_false(/#{Regexp.new("abc")}d/i.match?("ABCd"))
end

assert("Regexp#== and Regexp#eql?") do
Expand Down Expand Up @@ -853,7 +885,7 @@
assert_equal "/abc/x", Regexp.new("abc", Regexp::EXTENDED).inspect

# to_s shows x flag
assert_equal "(?x:abc)", Regexp.new("abc", Regexp::EXTENDED).to_s
assert_equal "(?x-mi:abc)", Regexp.new("abc", Regexp::EXTENDED).to_s

# errors quote the pattern as written, not the stripped text
assert_raise_with_message(RegexpError, "unterminated character class: /a # c\n[/") do
Expand Down
Loading