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
7 changes: 7 additions & 0 deletions mrbgems/mruby-regexp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ pattern analysis.
supported.
- **No `\x{...}` hex escape**: the hex escape is `\xHH`, so it reaches
`0xff` at most. Write `\u{...}` for a codepoint above that.
- **No encodings**: a pattern is a byte string read as UTF-8, and there is no
encoding to consult about a byte that starts no whole character. Such a byte
is that byte, inside a character class as much as outside one: `[\xB5]` and
`\xB5` both hold the byte `0xB5`, and neither matches `µ`, which is `C2 B5`.
CRuby settles the same question with the pattern's encoding and raises
`RegexpError` for either spelling. A range whose ends are a byte and a
character (`[\x80-µ]`) names neither and raises `RegexpError`.
- **ASCII case folding by default**: The `i` flag handles ASCII letters
only unless the build defines `MRB_REGEXP_UNICODE_CASE`, which adds the
Unicode foldings that pair one codepoint with one other. Without the
Expand Down
16 changes: 13 additions & 3 deletions mrbgems/mruby-regexp/include/re_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,21 @@ typedef struct {

/* Character class bitmap (ASCII range) */
#define RE_CLASS_BITMAP_SIZE 16 /* 128 bits = 16 bytes for ASCII */

/* A class member that is a byte rather than a codepoint, held in the same
range list with this bit set. A pattern byte that starts no whole character
is a byte, and the two spaces collide over U+0080 to U+00FF: the byte 0xB5
and the character U+00B5 both arrive as the number 0xB5, so the number alone
cannot say which was written. The tag sits above every codepoint, so a
tagged range can never overlap an untagged one. */
#define RE_CLASS_BYTE 0x80000000u

typedef struct {
uint8_t bitmap[RE_CLASS_BITMAP_SIZE]; /* bitmap for 0-127 */
/* Non-ASCII codepoint ranges. Stored as flat (lo, hi) pairs:
ranges[2k] = lo, ranges[2k+1] = hi (inclusive). NULL when the
class has no non-ASCII members (the common case). */
/* Non-ASCII codepoint ranges, and byte ranges tagged with RE_CLASS_BYTE.
Stored as flat (lo, hi) pairs: ranges[2k] = lo, ranges[2k+1] = hi
(inclusive). NULL when the class has no non-ASCII members (the common
case). */
uint32_t *ranges;
uint32_t num_ranges;
uint32_t range_capa;
Expand Down
72 changes: 54 additions & 18 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -532,22 +532,33 @@ unicode_escape_first(re_compiler *c, mrb_bool *more)
return cp;
}

/* Add one codepoint to the class: the ASCII bitmap and the codepoint range
list each hold one side of 128, and class_match() picks the side to read
from the codepoint alone. */
/* Add one member to the class: the ASCII bitmap and the range list each hold
one side of 128, and class_match() picks the side to read from the value
alone. Above 128 the value is a codepoint or a byte, which the tag records
because the number cannot: see RE_CLASS_BYTE. */
static void
class_add_member(re_compiler *c, re_charclass *cc, uint32_t cp)
class_add_member(re_compiler *c, re_charclass *cc, uint32_t cp, mrb_bool is_byte)
{
if (cp < 128) class_set_bit(cc, (uint8_t)cp);
else class_add_codepoint(c, cc, cp);
else class_add_codepoint(c, cc, (is_byte ? RE_CLASS_BYTE : 0) | cp);
}

/* Read one character class atom: either an ASCII byte (0-127), a
`\escape`, or a full multi-byte UTF-8 codepoint. Returns the
codepoint and advances c->p. */
`\escape`, or a full multi-byte UTF-8 codepoint. Returns the value and
advances c->p. *is_byte says which of the two the value is: TRUE for a
byte at or above 0x80 that starts no whole character, FALSE for ASCII, for
a decoded codepoint and for `\u`, which names a codepoint outright.

The question is the one the literal path already answers: emit_char_folded()
decodes and stands aside when the decode consumed one byte, so `\xB5` and a
raw 0xB5 both compile to the byte outside [...]. Reading the same byte as
U+00B5 inside [...] made the two halves of one pattern disagree about what
the pattern holds. A byte and a codepoint of the same number are different
members, which is what the tag on the stored value records. */
static uint32_t
read_class_atom(re_compiler *c, re_charclass *cc)
read_class_atom(re_compiler *c, re_charclass *cc, mrb_bool *is_byte)
{
*is_byte = FALSE;
if (peek(c) == '\\') {
next_char(c);
if (peek(c) == 'u') {
Expand All @@ -559,7 +570,7 @@ read_class_atom(re_compiler *c, re_charclass *cc)
the last join the class here; the last is returned, so it can open a
range as any other atom would: `[\u{61 62}-z]` is `a` plus `b-z`. */
while (unicode_escape_next(c, &more, &nx)) {
class_add_member(c, cc, cp);
class_add_member(c, cc, cp, FALSE);
cp = nx;
}
return cp;
Expand All @@ -569,17 +580,25 @@ read_class_atom(re_compiler *c, re_charclass *cc)
returns one byte, which left the continuation byte as a class atom of
its own. A trailing backslash (peek < 0) still reaches parse_escape(),
which reports it. */
if (peek(c) < 0xC0) return (uint32_t)parse_escape(c);
if (peek(c) < 0xC0) {
uint32_t esc = (uint32_t)parse_escape(c);
/* \xNN and octal \NNN name a byte, and the literal path emits one. */
if (esc >= 0x80) *is_byte = TRUE;
return esc;
}
}
uint8_t b = (uint8_t)*c->p;
if (b < 0xC0) {
/* ASCII or stray continuation byte. */
/* ASCII, or a continuation byte that starts nothing. */
if (b >= 0x80) *is_byte = TRUE;
return (uint32_t)next_char(c);
}
/* Multi-byte UTF-8 leader: decode the full codepoint. */
/* Multi-byte UTF-8 leader: decode the full codepoint. An invalid leader
decodes as itself over one byte, so it is a byte like the rest. */
int len = 0;
uint32_t cp = mrb_re_utf8_decode(c->p, c->src_end, &len);
c->p += len;
if (len == 1) *is_byte = TRUE;
return cp;
}

Expand Down Expand Up @@ -641,24 +660,35 @@ compile_charclass(re_compiler *c)
}
}

uint32_t cp = read_class_atom(c, cc);
mrb_bool cp_byte;
uint32_t cp = read_class_atom(c, cc, &cp_byte);

/* check for range a-z (or U+xxxx-U+yyyy) */
if (peek(c) == '-' && c->p + 1 < c->src_end && c->p[1] != ']') {
next_char(c); /* skip '-' */
uint32_t hi = read_class_atom(c, cc);
mrb_bool hi_byte;
uint32_t hi = read_class_atom(c, cc, &hi_byte);
/* An endpoint at or above 128 is a byte or a character, and a span from
one to the other names neither: [\x80-µ] would run from a byte to a
codepoint. ASCII belongs to both, so it pairs with either. */
if (cp >= 128 && hi >= 128 && cp_byte != hi_byte) {
compile_error(c, "character class range mixes a byte and a character");
}
/* A range that straddles the ASCII boundary is split in two: the
bitmap takes the half below 128 and the codepoint list the rest.
Neither half can hold the other, and class_match() picks the side
to read from the codepoint alone, so a span left whole in the
codepoint list is unreachable below 128. */
if (cp <= hi) {
if (cp < 128) class_set_range(cc, (uint8_t)cp, (uint8_t)(hi < 128 ? hi : 127));
if (hi >= 128) class_add_range(c, cc, cp < 128 ? 128 : cp, hi);
if (hi >= 128) {
uint32_t tag = hi_byte ? RE_CLASS_BYTE : 0;
class_add_range(c, cc, tag | (cp < 128 ? 128 : cp), tag | hi);
}
}
}
else {
class_add_member(c, cc, cp);
class_add_member(c, cc, cp, cp_byte);
}
}
next_char(c); /* skip ']' */
Expand All @@ -671,7 +701,10 @@ compile_charclass(re_compiler *c)
written to reject.

Closing means: x belongs to the class whenever some written member folds
the same way x does. */
the same way x does. A byte member has no case: it stands for no character,
so nothing folds to it and it folds to nothing. Every walk below steps over
the tagged ranges, which is also what keeps /i from refusing a class of
continuation bytes on a build without the folding tables. */
if (c->flags & RE_FLAG_IGNORECASE) {
#ifdef MRB_REGEXP_UNICODE_CASE
/* That takes two rounds rather than one walk in each direction, because a
Expand All @@ -689,6 +722,7 @@ compile_charclass(re_compiler *c)
added. */
uint32_t nranges = cc->num_ranges;
for (uint32_t i = 0; i < nranges; i++) {
if (cc->ranges[2 * i] & RE_CLASS_BYTE) continue;
mrb_re_case_fold_range(cc->ranges[2 * i], cc->ranges[2 * i + 1],
class_fold_add, &sink);
}
Expand All @@ -702,6 +736,7 @@ compile_charclass(re_compiler *c)
an upper case letter. */
nranges = cc->num_ranges;
for (uint32_t i = 0; i < nranges; i++) {
if (cc->ranges[2 * i] & RE_CLASS_BYTE) continue;
mrb_re_case_unfold_range(cc->ranges[2 * i], cc->ranges[2 * i + 1],
class_fold_add, &sink);
}
Expand All @@ -720,6 +755,7 @@ compile_charclass(re_compiler *c)
across the boundary in each direction. */
for (uint32_t i = 0; i < cc->num_ranges; i++) {
uint32_t lo = cc->ranges[2 * i], hi = cc->ranges[2 * i + 1];
if (lo & RE_CLASS_BYTE) continue;
if (mrb_re_needs_case_data(lo, hi)) {
compile_error(c, "/i needs MRB_REGEXP_UNICODE_CASE for this character class");
}
Expand Down Expand Up @@ -961,7 +997,7 @@ emit_cp_folded(re_compiler *c, uint32_t cp)
uint16_t id = add_class(c);
class_add_codepoint(c, &c->classes[id], cp);
for (int i = 0; i < n; i++) {
class_add_member(c, &c->classes[id], alt[i]);
class_add_member(c, &c->classes[id], alt[i], FALSE);
}
emit(c, RE_CLASS, (uint8_t)id, 0);
return TRUE;
Expand Down
31 changes: 23 additions & 8 deletions mrbgems/mruby-regexp/src/re_exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,24 @@ skip_to_prefix(const mrb_regexp_pattern *pat, const char *sp, const char *str_en
#define FIRST_BYTE_OK(pat, ch) \
((ch) >= 128 || ((pat)->first_bytes[(ch) >> 3] & (1 << ((ch) & 7))))

/* Check if a codepoint matches a character class. ASCII (cp < 128) hits
the bitmap; non-ASCII falls back to the inclusive (lo, hi) range list,
then to the utf8_any catch-all (used by negated shorthand like \D). */
/* Check if the current input character matches a character class. ASCII
(cp < 128) hits the bitmap; non-ASCII falls back to the inclusive (lo, hi)
range list, then to the utf8_any catch-all (used by negated shorthand like
\D).

`raw` says the input is a byte rather than a character: a byte-indexed
subject, or one whose byte at this position starts no whole character. It
picks which half of the range list to read, since a byte member and a
codepoint member of the same number are different members and arrive here as
the same number. utf8_any is the answer for either, being about the byte
being non-ASCII at all. */
static mrb_bool
class_match(const re_charclass *cc, uint32_t cp)
class_match(const re_charclass *cc, uint32_t cp, mrb_bool raw)
{
if (cp < 128) {
return (cc->bitmap[cp >> 3] >> (cp & 7)) & 1;
}
if (raw) cp |= RE_CLASS_BYTE;
for (uint32_t i = 0; i < cc->num_ranges; i++) {
if (cp >= cc->ranges[2*i] && cp <= cc->ranges[2*i + 1]) return TRUE;
}
Expand Down Expand Up @@ -473,6 +482,10 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
int dlen = 0;
curr_cp = mrb_re_decode_char(sp, str_end, &dlen, s.binary);
}
/* A non-ASCII byte that stands alone is a byte, not the character its
number spells: every byte of a byte-indexed subject, and a byte that
starts no whole character in a decoded one. */
mrb_bool curr_raw = (advance == 1 && ch >= 0x80);

for (int i = 0; i < curr.count; i++) {
re_thread *th = &curr.threads[i];
Expand Down Expand Up @@ -512,14 +525,14 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
break;

case RE_CLASS:
if (class_match(&pat->classes[inst.a], curr_cp)) {
if (class_match(&pat->classes[inst.a], curr_cp, curr_raw)) {
int cp = match_only ? 0 : pool_copy(&s, th->cap_slot);
add_thread(&s, &next, th->pc + 1, cp, sp + advance, s.gen);
}
break;

case RE_NCLASS:
if (!class_match(&pat->classes[inst.a], curr_cp)) {
if (!class_match(&pat->classes[inst.a], curr_cp, curr_raw)) {
int cp = match_only ? 0 : pool_copy(&s, th->cap_slot);
add_thread(&s, &next, th->pc + 1, cp, sp + advance, s.gen);
}
Expand Down Expand Up @@ -602,7 +615,8 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
{
int dlen = 0;
uint32_t cp_ = mrb_re_decode_char(sp, str_end, &dlen, binary);
if (!class_match(&pat->classes[inst.a], cp_)) return FALSE;
mrb_bool raw = (dlen == 1 && (uint8_t)*sp >= 0x80);
if (!class_match(&pat->classes[inst.a], cp_, raw)) return FALSE;
sp += mrb_re_charlen(sp, str_end, binary);
}
pc++;
Expand All @@ -613,7 +627,8 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,
{
int dlen = 0;
uint32_t cp_ = mrb_re_decode_char(sp, str_end, &dlen, binary);
if (class_match(&pat->classes[inst.a], cp_)) return FALSE;
mrb_bool raw = (dlen == 1 && (uint8_t)*sp >= 0x80);
if (class_match(&pat->classes[inst.a], cp_, raw)) return FALSE;
sp += mrb_re_charlen(sp, str_end, binary);
}
pc++;
Expand Down
69 changes: 64 additions & 5 deletions mrbgems/mruby-regexp/test/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -840,8 +840,10 @@
# A character the class does hold is still found through the same branch.
assert_equal 4, ("ĵ" + "µ").match(/.?[µ]/)[0].bytesize
assert_equal 5, ("あ" + "µ").match(/.?[µ]/)[0].bytesize
# And so is the byte itself where no lead byte reaches it.
assert_equal 2, ("x" + "\xb5").match(/.?[µ]/)[0].bytesize
# And so is a byte where no lead byte reaches it, through a class that holds
# the byte. [µ] holds the character, whose trailing byte alone is not it.
assert_equal 2, ("x" + "\xb5").match(Regexp.new(".?[\xb5]"))[0].bytesize
assert_nil ("x" + "\xb5").match(/.?[µ]/)
end

assert("Regexp - a match does not end inside a character") do
Expand Down Expand Up @@ -2426,9 +2428,9 @@ def -(other)

assert("Regexp - overlong UTF-8 is not the character it spells") do
# C0 BC is the two-byte overlong spelling of "<" and E0 84 80 the three-byte
# spelling of "Ā". A class compares the decoded codepoint and a literal
# compares bytes, so a decoder that hands out a codepoint for these makes the
# two disagree about the same subject: assert them together.
# spelling of "Ā". A decoder that hands out a codepoint for these would let a
# class hold a character the subject does not spell, so assert the class and
# the literal together against the same subject.
assert_nil ("\xC0\xBC" =~ /[<]/)
assert_nil ("\xC0\xBC" =~ /</)
assert_equal 0, ("\xC0\xBC" =~ /[^<]/)
Expand Down Expand Up @@ -2456,6 +2458,63 @@ def -(other)
assert_equal 0, ("\u{10FFFF}" =~ Regexp.new("[\u{10FFFF}]"))
end

assert("Regexp - a pattern byte that starts no character is a byte in a class") do
# A class used to read a lone continuation byte as the codepoint of its
# number, so "[\xB5]" held U+00B5 while "\xB5" held the byte: one pattern
# meant two things depending on which side of the brackets it was written.
# CRuby settles it with the pattern's encoding and raises RegexpError for
# either spelling; this gem has no encoding to consult, so it reads the byte
# as the byte on both sides.
mu = "\xC2\xB5" # U+00B5 MICRO SIGN, two bytes
assert_nil (mu =~ Regexp.new("[\xB5]"))
assert_nil (mu =~ Regexp.new("\xB5"))
assert_equal mu.bytes, mu.gsub(Regexp.new("[\xB5]"), "!").bytes
assert_equal mu.bytes, mu.gsub(Regexp.new("\xB5"), "!").bytes
assert_equal 0, ("\xB5" =~ Regexp.new("[\xB5]")) # the byte alone is it
assert_equal 1, (mu.b =~ Regexp.new("[\xB5]")) # so is a byte subject
assert_equal 0, (mu =~ Regexp.new("[^\xB5]"))
# An escape names a byte too, which is what the literal path emits for it.
assert_nil (mu =~ Regexp.new("[\\xB5]"))
assert_equal 0, ("\xB5" =~ Regexp.new("[\\xB5]"))
# `\u` names a codepoint outright, so it is how the character gets spelled
# where the byte of the same number will not do.
assert_equal 0, (mu =~ Regexp.new("[\\u{B5}]"))
assert_nil ("\xB5" =~ Regexp.new("[\\u{B5}]"))
# An invalid leader is a byte on both sides for the same reason, which is
# what "overlong UTF-8 is not the character it spells" pins for the class.
assert_equal 0, ("\xC0" =~ Regexp.new("[\xC0]"))
assert_nil ("À" =~ Regexp.new("[\xC0]")) # C3 80
# A byte range is how a continuation byte gets spelled, and it stays a range
# of bytes: it holds no character of its own.
data = "\xC2\xB5A\xCE\xBC"
assert_equal 2, data.b.scan(Regexp.new("[\x80-\xBF]")).size
assert_equal 0, data.scan(Regexp.new("[\x80-\xBF]")).size
assert_equal 0, ("\u{00BF}" =~ Regexp.new("[^\x80-\xBF]"))
# A range from a byte to a character names neither, however it is spelled.
assert_raise(RegexpError) { Regexp.new("[\x80-µ]") }
assert_raise(RegexpError) { Regexp.new("[µ-\x80]") }
assert_raise(RegexpError) { Regexp.new("[\\u{B5}-\\xBF]") }
# ASCII belongs to both, so it pairs with either.
assert_equal 0, ("\xFF" =~ Regexp.new("[\x00-\xFF]"))
assert_equal 0, ("µ" =~ Regexp.new("[\x00-\u{FF}]"))
end

assert("Regexp - /i over a class of bytes asks for no case data") do
# Folding is for characters, and a byte that starts none has no case: a
# class of continuation bytes used to reach the fold tables through the
# codepoint its number spells, which refused the pattern on a build without
# them and folded it into two Greek letters on a build with them.
assert_kind_of Regexp, Regexp.new("[\xB5]", Regexp::IGNORECASE)
assert_kind_of Regexp, Regexp.new("[\x80-\xBF]", Regexp::IGNORECASE)
assert_kind_of Regexp, Regexp.new("[\xC0\xBC]", Regexp::IGNORECASE)
assert_nil ("μ" =~ Regexp.new("[\xB5]", Regexp::IGNORECASE))
assert_nil ("Μ" =~ Regexp.new("[\xB5]", Regexp::IGNORECASE))
assert_equal 0, ("μ" =~ Regexp.new("[^\xB5]", Regexp::IGNORECASE))
assert_equal 2, "\xC2\xB5A\xCE\xBC".b.scan(Regexp.new("[\x80-\xBF]", Regexp::IGNORECASE)).size
# The characters in the same class still fold.
assert_equal 0, ("K" =~ Regexp.new("[\x80-\xBF k]", Regexp::IGNORECASE))
end

assert("Regexp - pattern too large for its jump targets is refused") do
# Jump targets live in a 16-bit field, so a program that outgrows the field
# used to wrap them and jump to an unrelated instruction: the pattern then
Expand Down
Loading