Skip to content
Closed
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
12 changes: 11 additions & 1 deletion mrbgems/mruby-regexp/include/re_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,20 @@ typedef struct {
/* Named capture entry */
typedef struct {
const char *name;
uint16_t name_len;
uint32_t name_len;
uint16_t group;
} re_named_capture;

/* Longest group name re_named_capture::name_len can hold. The compiler
rejects anything longer instead of narrowing to the field, so a stored
length is always the name's true length.

RE_NAME_LEN_FITS() widens before comparing: where the argument's type is
no wider than the field (ptrdiff_t on ILP32, mrb_int on MRB_INT32) the
comparison is otherwise constant, which compilers warn about. */
#define RE_MAX_NAME_LEN UINT32_MAX
#define RE_NAME_LEN_FITS(n) ((uintmax_t)(n) <= RE_MAX_NAME_LEN)

/* Compiled regexp pattern */
typedef struct mrb_regexp_pattern {
re_inst *code; /* bytecode array */
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 @@ -665,7 +665,8 @@ compile_atom(re_compiler *c)
cap_name = c->p;
while (peek(c) != '>' && peek(c) >= 0) next_char(c);
if (peek(c) != '>') compile_error(c, "unterminated named capture");
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 @@ -811,14 +812,15 @@ compile_atom(re_compiler *c)
const char *name = c->p;
while (peek(c) != close && peek(c) >= 0) next_char(c);
if (peek(c) != close) compile_error(c, "unterminated backreference name");
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');
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -1292,7 +1294,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
10 changes: 5 additions & 5 deletions mrbgems/mruby-regexp/src/regexp.c
Original file line number Diff line number Diff line change
Expand Up @@ -592,12 +592,12 @@ 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 @@ -1229,6 +1229,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<name> 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