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
10 changes: 9 additions & 1 deletion mrbgems/mruby-regexp/include/re_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,18 @@ typedef struct {
mrb_bool utf8_any; /* match any non-ASCII byte if true */
} re_charclass;

/* Longest capture name that fits in re_named_capture::name_len. The bound
sits beside the field it describes so a later change of width is one line.
The widening inside the macro is not cosmetic: on a target whose argument
type is no wider than the field, the naive comparison is against a constant
that type cannot exceed, and the compiler reports it. */
#define RE_MAX_NAME_LEN UINT32_MAX
#define RE_NAME_LEN_FITS(n) ((uintmax_t)(n) <= RE_MAX_NAME_LEN)

/* Named capture entry */
typedef struct {
const char *name;
uint16_t name_len;
uint32_t name_len;
uint16_t group;
} re_named_capture;

Expand Down
12 changes: 7 additions & 5 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ compile_atom(re_compiler *c)
uint32_t saved_flags = c->flags;

const char *cap_name = NULL;
uint16_t cap_name_len = 0;
uint32_t cap_name_len = 0;

if (peek(c) == '?' && c->p + 1 < c->src_end) {
if (c->p[1] == ':') {
Expand Down Expand Up @@ -666,7 +666,8 @@ compile_atom(re_compiler *c)
while (peek(c) != '>' && peek(c) >= 0) next_char(c);
if (peek(c) != '>') compile_error(c, "unterminated named capture");
if (c->p == cap_name) compile_error(c, "group name is empty");
cap_name_len = (uint16_t)(c->p - cap_name);
if (!RE_NAME_LEN_FITS(c->p - cap_name)) compile_error(c, "group name too long");
cap_name_len = (uint32_t)(c->p - cap_name);
next_char(c); /* skip > */
}
else if (c->p[1] == 'i' || c->p[1] == 'm' || c->p[1] == 'x' || c->p[1] == '-') {
Expand Down Expand Up @@ -813,14 +814,15 @@ compile_atom(re_compiler *c)
while (peek(c) != close && peek(c) >= 0) next_char(c);
if (peek(c) != close) compile_error(c, "unterminated backreference name");
if (c->p == name) compile_error(c, "group name is empty");
uint16_t name_len = (uint16_t)(c->p - name);
if (!RE_NAME_LEN_FITS(c->p - name)) compile_error(c, "group name too long");
uint32_t name_len = (uint32_t)(c->p - name);
next_char(c); /* skip the closing > or ' */

int group = -1;
if (name_len > 0 && (name[0] == '-' || (name[0] >= '0' && name[0] <= '9'))) {
mrb_bool relative = (name[0] == '-');
int n = 0;
for (uint16_t i = (relative ? 1 : 0); i < name_len; i++) {
for (uint32_t i = (relative ? 1 : 0); i < name_len; i++) {
if (name[i] < '0' || name[i] > '9') compile_error(c, "invalid backreference");
n = n * 10 + (name[i] - '0');
if (n > (int)c->num_captures - 1) compile_error(c, "undefined group name reference");
Expand Down Expand Up @@ -1295,7 +1297,7 @@ mrb_re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags)
pat->named_arena = (char*)mrb_malloc(mrb, total);
size_t off = 0;
for (uint16_t i = 0; i < c.num_named; i++) {
uint16_t n = c.named_captures[i].name_len;
uint32_t n = c.named_captures[i].name_len;
memcpy(pat->named_arena + off, c.named_captures[i].name, n);
pat->named_captures[i].name = pat->named_arena + off;
off += n;
Expand Down
11 changes: 6 additions & 5 deletions mrbgems/mruby-regexp/src/regexp.c
Original file line number Diff line number Diff line change
Expand Up @@ -622,12 +622,13 @@ matchdata_aref(mrb_state *mrb, mrb_value self)
if (!mrb_nil_p(md->regexp)) {
pat = DATA_GET_PTR(mrb, md->regexp, &regexp_type, mrb_regexp_pattern);
}
/* A stored name never exceeds UINT16_MAX, so a longer request can name no
group. Rejecting it here keeps the cast in the loop lossless; without it
the length test truncates while the memcmp() next to it does not. */
if (pat && name_len <= UINT16_MAX) {
/* A stored name never exceeds RE_MAX_NAME_LEN, so a longer request can
name no group. Rejecting it here keeps the cast in the loop lossless;
without it the length test truncates while the memcmp() next to it does
not. */
if (pat && RE_NAME_LEN_FITS(name_len)) {
for (uint16_t i = 0; i < pat->num_named; i++) {
if (pat->named_captures[i].name_len == (uint16_t)name_len &&
if (pat->named_captures[i].name_len == (uint32_t)name_len &&
memcmp(pat->named_captures[i].name, name, name_len) == 0) {
idx = pat->named_captures[i].group;
goto found;
Expand Down
28 changes: 28 additions & 0 deletions mrbgems/mruby-regexp/test/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,34 @@ def -(other)
assert_equal "x", md[:abc]
end

assert("Regexp - group name longer than a uint16 length") do
# The name length used to live in a uint16_t and was truncated with a cast,
# so (uint16_t)65538 == 2 made this group answer to "ab" instead of to the
# name it was given.
long = "ab" + "A" * 65536
re = Regexp.new("(?<#{long}>x)")
assert_equal [long], re.named_captures.keys
assert_equal "x", re.match("x")[long]
assert_raise(IndexError) { re.match("x")["ab"] }

# two names that shared a truncation stay distinct, and the two APIs that
# resolve a name agree on which group it names
re = Regexp.new("(?<ab>x)(?<#{long}>y)")
md = re.match("xy")
assert_equal "x", md["ab"]
assert_equal "y", md[long]
assert_equal({ "ab" => "x", long => "y" }, md.named_captures)

# \k binds to the group the name was written on
re = Regexp.new("(?<ab>x)(?<#{long}>y)\\k<#{long}>")
assert_nil re.match("xyx")
assert_equal "xyy", re.match("xyy")[0]

# a name of exactly 65536 bytes is not the empty name
z = "Z" * 65536
assert_equal [z], Regexp.new("(?<#{z}>x)").named_captures.keys
end

assert("Regexp - named backreference \\k") do
assert_equal "aa", "aa".match(/(?<n>\w)\k<n>/)[0]
assert_equal "abba", "abba".match(/(?<a>.)(?<b>.)\k<b>\k<a>/)[0]
Expand Down
Loading