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
2 changes: 1 addition & 1 deletion mrbgems/mruby-regexp/include/re_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ enum re_opcode {
RE_EOTNL, /* assert end of text or before final \n (\Z) */
RE_WBOUND, /* assert word boundary (\b) */
RE_NWBOUND, /* assert non-word boundary (\B) */
RE_BACKREF, /* backreference: operand = group number */
RE_BACKREF, /* backreference: a = group number, offset = 1 if case-insensitive */
RE_LOOKAHEAD, /* positive lookahead: offset = end of sub-pattern */
RE_NEG_LOOKAHEAD, /* negative lookahead: offset = end of sub-pattern */
RE_LOOKBEHIND, /* positive lookbehind: a = byte length, offset = end */
Expand Down
4 changes: 2 additions & 2 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ compile_atom(re_compiler *c)
ch = peek(c);
if (ch >= '1' && ch <= '9') {
next_char(c);
emit(c, RE_BACKREF, (uint8_t)(ch - '0'), 0);
emit(c, RE_BACKREF, (uint8_t)(ch - '0'), (c->flags & RE_FLAG_IGNORECASE) ? 1 : 0);
c->has_backref = TRUE;
}
else if (ch == 'd' || ch == 'D' || ch == 'w' || ch == 'W' || ch == 's' || ch == 'S') {
Expand Down Expand Up @@ -845,7 +845,7 @@ compile_atom(re_compiler *c)
if (group < 1 || group >= (int)c->num_captures) {
compile_error(c, "undefined group name reference");
}
emit(c, RE_BACKREF, (uint8_t)group, 0);
emit(c, RE_BACKREF, (uint8_t)group, (c->flags & RE_FLAG_IGNORECASE) ? 1 : 0);
c->has_backref = TRUE;
}
else {
Expand Down
20 changes: 19 additions & 1 deletion mrbgems/mruby-regexp/src/re_exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ class_match(const re_charclass *cc, uint32_t cp)
return cc->utf8_any;
}

/* Compare two byte spans ignoring ASCII case. Folding stops at ASCII, like
every other ignorecase decision in this engine (see compile_atom()'s /i
handling, which only folds A-Z and a-z into a class bitmap). */
static mrb_bool
memcmp_ci(const char *a, const char *b, int len)
{
for (int i = 0; i < len; i++) {
uint8_t ca = (uint8_t)a[i], cb = (uint8_t)b[i];
if (ca >= 'A' && ca <= 'Z') ca += 32;
if (cb >= 'A' && cb <= 'Z') cb += 32;
if (ca != cb) return FALSE;
}
return TRUE;
}

/*
* Pike VM with optimized thread storage.
*
Expand Down Expand Up @@ -587,7 +602,10 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
if (gs < 0 || ge < 0) return FALSE;
int blen = ge - gs;
if (sp + blen > str_end) return FALSE;
if (memcmp(sp, str + gs, blen) != 0) return FALSE;
if (inst.offset) {
if (!memcmp_ci(sp, str + gs, blen)) return FALSE;
}
else if (memcmp(sp, str + gs, blen) != 0) return FALSE;
sp += blen;
pc++;
}
Expand Down
17 changes: 17 additions & 0 deletions mrbgems/mruby-regexp/test/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@
assert_equal 0, (/(a(?i)b)c/ =~ "aBc")
assert_nil (/(a(?i)b)c/ =~ "aBC") # trailing `c` is case-sensitive again

# A backreference takes the options in effect where it appears, not the
# pattern's own, so an inline toggle reaches it like any other atom.
assert_equal 0, (/(a)(?i)\1/ =~ "aA")
assert_equal 0, (/(a)(?i:\1)/ =~ "aA")
assert_nil (/(?-i:(a)\1)/i =~ "aA")

# m enables dot-matches-newline for its scope.
assert_equal 0, (/(?m:a.b)/ =~ "a\nb")
assert_nil (/a.b/ =~ "a\nb")
Expand Down Expand Up @@ -1224,6 +1230,14 @@ def -(other)
assert_nil /(\w+) \1/.match("hello world")
end

assert("Regexp - backreference under /i") do
# The comparison against the captured text has to fold case too, otherwise
# `\1` stays case-sensitive while the rest of the pattern does not.
assert_equal "aA", /(a)\1/i.match("aA")[0]
assert_equal "Hello hELLO", /(\w+) \1/i.match("Hello hELLO world")[0]
assert_nil /(a)\1/i.match("ab")
end

assert("Regexp - named captures") do
md = /(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/.match("2026-03-21")
assert_equal "2026", md[:year]
Expand Down Expand Up @@ -1334,6 +1348,9 @@ def -(other)
# numeric and relative forms
assert_equal "aa", "aa".match(/(a)\k<1>/)[0]
assert_equal "abba", "abba".match(/(.)(.)\k<-1>\k<-2>/)[0]
# /i folds the comparison against the captured text
assert_equal "aA", "aA".match(/(?<n>a)\k<n>/i)[0]
assert_nil "ab".match(/(?<n>a)\k<n>/i)
# an unknown name is an error
assert_raise(RegexpError) { Regexp.new("\\k<missing>") }
end
Expand Down
Loading