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
63 changes: 51 additions & 12 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,20 @@ typedef struct {

static void compile_alt(re_compiler *c); /* forward */

/* Take the message as a String rather than as a C string, for the messages
that quote a group name: the name is a length-counted slice of the pattern
and may hold a NUL, which a C string would cut short. Callers with a fixed
message use compile_error() below. */
static void
compile_error(re_compiler *c, const char *msg)
compile_error_str(re_compiler *c, mrb_value msg)
{
/* Quote c->orig, the pattern as written: when the pattern is preprocessed
c->src points at the buffer preprocess_pattern() returned, so quoting it
would drop the free-spacing, the comments and the (?#...) groups from the
message. c->orig is the caller's buffer, which outlives the compile. It
is not NUL-terminated, so use %l with the explicit length from
c->orig_end. */
mrb_value emsg = mrb_format(c->mrb, "%s: /%l/",
mrb_value emsg = mrb_format(c->mrb, "%v: /%l/",
msg, c->orig, (size_t)(c->orig_end - c->orig));

/* Free compile buffers before raising, since mrb_exc_raise longjmps out
Expand All @@ -76,6 +80,12 @@ compile_error(re_compiler *c, const char *msg)
mrb_exc_new_str(c->mrb, mrb_exc_get_id(c->mrb, MRB_SYM(RegexpError)), emsg));
}

static void
compile_error(re_compiler *c, const char *msg)
{
compile_error_str(c, mrb_str_new_cstr(c->mrb, msg));
}

/* Maximum number of instructions in a compiled pattern. Every jump target
lives in re_inst.offset (uint16_t) and a target may be one past the last
instruction, so the whole program has to be addressable by that field.
Expand Down Expand Up @@ -1209,6 +1219,12 @@ emit_codepoint(re_compiler *c, uint32_t cp)
}
}

/* Largest number a \k<n> / \k<-n> backreference may spell. The bound is far
above RE_MAX_CAPTURES because it is not a capacity: it separates two of
CRuby's messages, `too big number` for a number past it and `invalid backref
number/name` for one within it that names no group. */
#define RE_MAX_BACKREF_NUM 2147483647

/* Compile a single atom (character, class, group, etc.) */
static void
compile_atom(re_compiler *c)
Expand Down Expand Up @@ -1454,6 +1470,32 @@ compile_atom(re_compiler *c)

int group = -1;
if (name_len > 0 && (name[0] == '-' || (name[0] >= '0' && name[0] <= '9'))) {
mrb_bool relative = (name[0] == '-');
uint32_t first = relative ? 1 : 0;

/* CRuby reads the whole name before converting it, so a name that is
not `-`? followed by digits is a malformed name whatever the digits
it does hold would come to: \k<99999999999999999999x> is `invalid
group name`, not `too big number`. A lone `-` is malformed too. */
mrb_bool numeric = (first < name_len);
for (uint32_t i = first; numeric && i < name_len; i++) {
if (name[i] < '0' || name[i] > '9') numeric = FALSE;
}

int n = 0;
for (uint32_t i = first; numeric && i < name_len; i++) {
int digit = name[i] - '0';
/* CRuby's scanner stops at RE_MAX_BACKREF_NUM, and a number past it
is too big rather than a reference to a group that is missing. */
if (n > (RE_MAX_BACKREF_NUM - digit) / 10) compile_error(c, "too big number");
n = n * 10 + digit;
}
/* n == 0 names group 0, the whole match, which \k cannot reference */
if (!numeric || n == 0) {
compile_error_str(c, mrb_format(c->mrb, "invalid group name <%l>",
name, (size_t)name_len));
}

/* CRuby rejects a numbered backreference in a named pattern whatever
its spelling, and it has to be rejected here too: once plain groups
stop consuming numbers, both the absolute bound and the relative
Expand All @@ -1462,14 +1504,10 @@ compile_atom(re_compiler *c)
if (c->dont_capture) {
compile_error(c, "numbered backref/call is not allowed. (use name)");
}
mrb_bool relative = (name[0] == '-');
int n = 0;
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");
}
group = relative ? (int)c->num_captures - n : n;
if (group < 1 || group >= (int)c->num_captures) {
compile_error(c, "invalid backref number/name");
}
}
else {
for (uint16_t i = 0; i < c->num_named; i++) {
Expand All @@ -1479,9 +1517,10 @@ compile_atom(re_compiler *c)
break;
}
}
}
if (group < 1 || group >= (int)c->num_captures) {
compile_error(c, "undefined group name reference");
if (group < 1) {
compile_error_str(c, mrb_format(c->mrb, "undefined name <%l> reference",
name, (size_t)name_len));
}
}
emit(c, RE_BACKREF, (uint8_t)group, (c->flags & RE_FLAG_IGNORECASE) ? 1 : 0);
c->has_backref = TRUE;
Expand Down
125 changes: 122 additions & 3 deletions mrbgems/mruby-regexp/test/regexp_syntax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -754,9 +754,128 @@
assert("Regexp - numeric \\k backreference out of int range") do
# The digit accumulator is an int with no bound, so 4294967297 used to wrap
# to 1 and bind this backreference to group 1 instead of raising.
assert_raise(RegexpError) { Regexp.new("(a)\\k<4294967297>") }
assert_raise(RegexpError) { Regexp.new("(a)\\k<-4294967297>") }
assert_raise(RegexpError) { Regexp.new("(a)(b)\\k<4294967298>") }
msg = "too big number"
assert_raise_with_message(RegexpError, "#{msg}: /(a)\\k<4294967297>/") do
Regexp.new("(a)\\k<4294967297>")
end
assert_raise_with_message(RegexpError, "#{msg}: /(a)\\k<-4294967297>/") do
Regexp.new("(a)\\k<-4294967297>")
end
assert_raise_with_message(RegexpError, "#{msg}: /(a)(b)\\k<4294967298>/") do
Regexp.new("(a)(b)\\k<4294967298>")
end
end

assert("Regexp - \\k group reference errors say which failure it was") do
# A \k reference fails in four ways and CRuby gives each its own message.
# They used to collapse into one, so a pattern that misspelled a name and a
# pattern that named a group it never opened read the same.

# a name that is neither `-`? digits nor a name any group carries
assert_raise_with_message(RegexpError, "invalid group name <1x>: /(a)\\k<1x>/") do
Regexp.new("(a)\\k<1x>")
end
assert_raise_with_message(RegexpError, "invalid group name <-x>: /(a)\\k<-x>/") do
Regexp.new("(a)\\k<-x>")
end
# `-` with no digits behind it
assert_raise_with_message(RegexpError, "invalid group name <->: /(a)\\k<->/") do
Regexp.new("(a)\\k<->")
end
# group 0 is the whole match, which \k cannot name in either spelling.
# The message quotes the name in <> whichever delimiter wrote it.
assert_raise_with_message(RegexpError, "invalid group name <0>: /(a)\\k<0>/") do
Regexp.new("(a)\\k<0>")
end
assert_raise_with_message(RegexpError, "invalid group name <-0>: /(a)\\k<-0>/") do
Regexp.new("(a)\\k<-0>")
end
assert_raise_with_message(RegexpError, "invalid group name <0>: /(a)\\k'0'/") do
Regexp.new("(a)\\k'0'")
end

# the name is read whole before it is converted, so digits followed by
# anything else is a malformed name and never an oversized number
assert_raise_with_message(RegexpError,
"invalid group name <99999999999999999999x>: /(a)\\k<99999999999999999999x>/") do
Regexp.new("(a)\\k<99999999999999999999x>")
end

# a number past the bound, either sign
assert_raise_with_message(RegexpError, "too big number: /(a)\\k<2147483648>/") do
Regexp.new("(a)\\k<2147483648>")
end
assert_raise_with_message(RegexpError, "too big number: /(a)\\k<-2147483648>/") do
Regexp.new("(a)\\k<-2147483648>")
end

# a number within the bound that names no group: a different message from
# the one above, and the bound is where they part
msg = "invalid backref number/name"
assert_raise_with_message(RegexpError, "#{msg}: /(a)\\k<2147483647>/") do
Regexp.new("(a)\\k<2147483647>")
end
assert_raise_with_message(RegexpError, "#{msg}: /(a)\\k<5>/") do
Regexp.new("(a)\\k<5>")
end
assert_raise_with_message(RegexpError, "#{msg}: /(a)\\k'5'/") do
Regexp.new("(a)\\k'5'")
end
assert_raise_with_message(RegexpError, "#{msg}: /(a)\\k<-5>/") do
Regexp.new("(a)\\k<-5>")
end
assert_raise_with_message(RegexpError, "#{msg}: /(a)(b)\\k<-3>/") do
Regexp.new("(a)(b)\\k<-3>")
end

# a name no group carries
assert_raise_with_message(RegexpError,
"undefined name <_nope> reference: /(a)\\k<_nope>/") do
Regexp.new("(a)\\k<_nope>")
end
assert_raise_with_message(RegexpError,
"undefined name <_nope> reference: /(a)\\k'_nope'/") do
Regexp.new("(a)\\k'_nope'")
end
# only `-` leads a number, so `+1` is a name and fails as one
assert_raise_with_message(RegexpError,
"undefined name <+1> reference: /(a)\\k<+1>/") do
Regexp.new("(a)\\k<+1>")
end

# a named pattern refuses a numbered reference, but only once the name is
# read as a number at all: a malformed one and an oversized one are still
# reported for what they are
assert_raise_with_message(RegexpError, "invalid group name <1x>: /(a)(?<b>b)\\k<1x>/") do
Regexp.new("(a)(?<b>b)\\k<1x>")
end
assert_raise_with_message(RegexpError, "invalid group name <0>: /(a)(?<b>b)\\k<0>/") do
Regexp.new("(a)(?<b>b)\\k<0>")
end
assert_raise_with_message(RegexpError,
"too big number: /(a)(?<b>b)\\k<99999999999999999999>/") do
Regexp.new("(a)(?<b>b)\\k<99999999999999999999>")
end
assert_raise_with_message(RegexpError,
"numbered backref/call is not allowed. (use name): /(a)(?<b>b)\\k<5>/") do
Regexp.new("(a)(?<b>b)\\k<5>")
end

# leading zeros are digits like any other, not a malformed name
assert_equal "aa", "aa".match(Regexp.new("(a)\\k<01>"))[0]
assert_equal "aa", "aa".match(Regexp.new("(a)\\k<-01>"))[0]

# The name is a length-counted slice of the pattern, so a name holding a NUL
# is quoted whole. CRuby builds these messages through a C string and stops
# at the NUL, reporting `undefined name <a` for the first of the two.
assert_raise_with_message(RegexpError,
"undefined name <a\0b> reference: /(a)\\k<a\0b>/") do
Regexp.new("(a)\\k<a\0b>")
end
assert_raise_with_message(RegexpError,
"invalid group name <1\0>: /(a)\\k<1\0>/") do
Regexp.new("(a)\\k<1\0>")
end
end

assert("Regexp - named captures survive /x preprocessing") do
Expand Down
Loading