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
56 changes: 41 additions & 15 deletions mrbgems/mruby-regexp/mrblib/string_regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,17 @@ def sub(*args, &block)
# get_pat. Only the quoting is taken from it: get_pat_quoted also accepts
# anything answering `to_str`, where `__check_pattern` keeps to a real
# String, as `match` already does.
pattern = Regexp.new(Regexp.escape(pattern)) if String === pattern
literal = String === pattern
# CRuby searches for a literal byte by byte and never reads the subject as
# UTF-8 on the way, so quoting one into a Regexp here must not put the
# subject through a check CRuby does not make: `"a\x80b".sub("b", "!")`
# answers there, where the same call with `/b/` is refused.
pattern = Regexp.new(Regexp.escape(pattern)) if literal
# A replacement argument wins over the block, as in CRuby.
if args.length == 2
return Regexp.__sub_str(pattern, self, replacement.to_s)
return Regexp.__sub_str(pattern, self, replacement.to_s, literal)
end
md = Regexp.__search(pattern, self)
md = Regexp.__search(pattern, self, 0, literal)
return self.dup unless md
md.pre_match + block.call(md[0]).to_s + md.post_match
end
Expand All @@ -141,19 +146,23 @@ def sub!(*args, &block)
# Resolved here rather than left to `sub` because the match below decides
# the return value, and a String pattern is a literal on both paths.
pattern = Regexp.__check_pattern(args[0])
pattern = Regexp.new(Regexp.escape(pattern)) if String === pattern
literal = String === pattern
pattern = Regexp.new(Regexp.escape(pattern)) if literal
raise FrozenError, "can't modify frozen String" if frozen?
# Whether a substitution happened is a question about the match, not about
# the result: `"aaa".sub!(/a/, "a")` returns self even though the string is
# unchanged. A full search and not `match?`, so a failed match clears $~.
return nil unless Regexp.__search(pattern, self)
return nil unless Regexp.__search(pattern, self, 0, literal)
# `sub` matches again and publishes its own $~ over this one, leaving the
# caller the match `sub` would have left, a block's own matches included.
# The resolved pattern takes the place of the original argument so that a
# String is not quoted and compiled a second time. Overwriting `self`
# afterwards is safe: a MatchData snapshots its subject, so $~ keeps
# describing the string as it was matched.
str = args.length == 2 ? self.sub(pattern, args[1], &block) : self.sub(pattern, &block)
# String is not quoted and compiled a second time; a literal goes down as
# the String it was instead, since that is what tells `sub` to leave the
# subject unread, and quoting it twice is the price of saying so.
# Overwriting `self` afterwards is safe: a MatchData snapshots its subject,
# so $~ keeps describing the string as it was matched.
down = literal ? args[0] : pattern
str = args.length == 2 ? self.sub(down, args[1], &block) : self.sub(down, &block)
self.replace(str)
end

Expand All @@ -169,12 +178,18 @@ def gsub(*args, &block)
# After the to_enum return above, so that `"abc".gsub(:b)` yields an
# Enumerator and raises on the first iteration, as CRuby does.
pattern = Regexp.__check_pattern(pattern)
pattern = Regexp.new(Regexp.escape(pattern)) if String === pattern
# A String pattern is a literal, as in `sub`, and reaches the subject the
# way CRuby reaches it: byte by byte, with no reading of it as UTF-8.
literal = String === pattern
pattern = Regexp.new(Regexp.escape(pattern)) if literal
# A replacement argument wins over the block, as in CRuby.
if args.length == 2
return Regexp.__gsub_str(pattern, self, replacement.to_s)
return Regexp.__gsub_str(pattern, self, replacement.to_s, literal)
end
# block case: keep in Ruby to avoid VM callback from C
# `__byte_search` leaves the encoding check to its caller, so run it here,
# once for the subject the loop below holds fixed.
Regexp.__check_encoding(self) unless literal
parts = []
pos = 0
len = self.bytesize
Expand Down Expand Up @@ -227,12 +242,15 @@ def gsub!(*args, &block)
end
return to_enum(:gsub!, *args) if args.length == 1 && !block
pattern = Regexp.__check_pattern(args[0])
pattern = Regexp.new(Regexp.escape(pattern)) if String === pattern
literal = String === pattern
pattern = Regexp.new(Regexp.escape(pattern)) if literal
# As in `sub!`: the match decides the return value, and a failed search
# clears $~. What it publishes on success is replaced right away by the
# last match of the `gsub` below, which is the one CRuby leaves behind.
return nil unless Regexp.__search(pattern, self)
str = args.length == 2 ? self.gsub(pattern, args[1], &block) : self.gsub(pattern, &block)
# A literal goes down as the String it was, for the reason `sub!` gives.
return nil unless Regexp.__search(pattern, self, 0, literal)
down = literal ? args[0] : pattern
str = args.length == 2 ? self.gsub(down, args[1], &block) : self.gsub(down, &block)
self.replace(str)
end

Expand Down Expand Up @@ -278,6 +296,8 @@ def split(pattern = nil, *args)
# branch of the check is unreachable here and nothing needs quoting.
pattern = Regexp.__check_pattern(pattern)

# Once for the whole loop, as in `gsub`.
Regexp.__check_encoding(self)
result = []
field_start = 0
search_pos = 0
Expand Down Expand Up @@ -505,9 +525,13 @@ def index(*args)
# not at the match end, which is what keeps overlapping matches in view:
# `"aaa".rindex(/aa/)` is 1, where resuming at the end would answer 0.
def __regexp_rsearch(pattern, limit, bytes)
# The walk searches once per match position, so the encoding check goes
# here rather than in each of those searches, which is what the flag on
# `__search` below says.
Regexp.__check_encoding(self)
found = nil
pos = 0
while (md = Regexp.__search(pattern, self, pos))
while (md = Regexp.__search(pattern, self, pos, true))
break if (bytes ? md.__byte_begin(0) : md.begin(0)) > limit
found = md
pos = md.begin(0) + 1
Expand Down Expand Up @@ -570,6 +594,8 @@ def byteindex(*args)
# not check for one either, and on a build without MRB_UTF8_STRING there
# is nothing to check.
return Regexp.__search(args[0], nil) if pos < 0 || pos > len
# One search, but `__byte_search` still leaves the check to its caller.
Regexp.__check_encoding(self)
md = Regexp.__byte_search(args[0], self, pos)
md && md.__byte_begin(0)
end
Expand Down
86 changes: 76 additions & 10 deletions mrbgems/mruby-regexp/src/regexp.c
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,46 @@ re_binary_string_p(mrb_value str)
return RSTR_BINARY_P(RSTRING(str));
}

/* CRuby refuses a search whose subject holds a byte that spells no character,
and mruby answers for it. Refuse it here too, so that a program moved from
one to the other is told about the subject rather than handed a result the
other would not have produced.

A binary string is exempt because it is indexed by byte throughout, so its
bytes make no claim that could be broken. A quoted String pattern is exempt
for a narrower reason: CRuby searches for a literal byte by byte and reads
the subject as UTF-8 nowhere along the way, so `"a\x80b".sub("b", "!")`
answers there while the same call with `/b/` is refused.

The check walks the whole subject, so it runs once per search a method
makes, not once per match it finds: the entry points below check what
arrives from Ruby, and the two that an mrblib loop drives (`__byte_search`,
and `__search` with the flag set) leave it to the loop, which checks the
subject it holds fixed before the first turn. */
static void
re_check_encoding(mrb_state *mrb, mrb_value str)
{
if (!mrb_str_valid_encoding_p(mrb, str)) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "invalid byte sequence in UTF-8");
}
}

/*
* Regexp.__check_encoding(str)
*
* Internal: the check itself, for the mrblib loops of `gsub`, `split`,
* `byteindex`, `rindex`, `byterindex` and `rpartition` to run once at the
* entry to the method.
*/
static mrb_value
regexp_s_check_encoding(mrb_state *mrb, mrb_value klass)
{
mrb_value str;
mrb_get_args(mrb, "S", &str);
re_check_encoding(mrb, str);
return mrb_nil_value();
}

static mrb_value
regexp_binary_string_p(mrb_state *mrb, mrb_value self)
{
Expand Down Expand Up @@ -362,6 +402,7 @@ regexp_match(mrb_state *mrb, mrb_value self)
return mrb_nil_value();
}

re_check_encoding(mrb, str);
md = exec_match(mrb, self, str, pos);
if (!mrb_nil_p(md) && !mrb_nil_p(block)) {
return mrb_yield(mrb, block, md);
Expand All @@ -383,22 +424,30 @@ check_regexp_arg(mrb_state *mrb, mrb_value re)
}

/*
* Regexp.__search(re, str, pos = 0)
* Regexp.__search(re, str, pos = 0, checked = false)
*
* Internal: `Regexp#match` with the pattern as an argument and no block form.
* The String overrides in mrblib search through this so that the search never
* dispatches on the pattern, where a singleton method would replace it; see
* the note at the top of mrblib/string_regexp.rb. A nil subject clears the
* match globals and answers nil, as `Regexp#match` does, which is what the
* overrides use to report a miss.
*
* `checked` says the caller has settled the encoding question for the subject
* and this search must not ask it again. The backward search sets it because
* it ran `Regexp.__check_encoding` itself, keeping one check for a walk that
* searches once per match position; `sub!` and `gsub!` set it because their
* pattern is a quoted String, which CRuby searches for without reading the
* subject as UTF-8 at all.
*/
static mrb_value
regexp_s_search(mrb_state *mrb, mrb_value klass)
{
mrb_value re, str;
mrb_int pos = 0;
mrb_bool checked = FALSE;

mrb_get_args(mrb, "oo|i", &re, &str, &pos);
mrb_get_args(mrb, "oo|ib", &re, &str, &pos, &checked);
check_regexp_arg(mrb, re);
if (mrb_nil_p(str)) {
clear_match_globals(mrb);
Expand All @@ -410,6 +459,7 @@ regexp_s_search(mrb_state *mrb, mrb_value klass)
clear_match_globals(mrb);
return mrb_nil_value();
}
if (!checked) re_check_encoding(mrb, str);
return exec_match(mrb, re, str, pos);
}

Expand All @@ -419,7 +469,8 @@ regexp_s_search(mrb_state *mrb, mrb_value klass)
* Internal: the byte-offset search the mrblib loops of `gsub`, `split` and
* `byteindex` drive themselves. No position normalization, because the
* callers already work in byte space, and no operand conversion, because
* they always pass a String.
* they always pass a String. The subject is checked by the caller, once for
* the whole loop.
*/
static mrb_value
regexp_s_byte_search(mrb_state *mrb, mrb_value klass)
Expand All @@ -445,6 +496,7 @@ exec_match_p(mrb_state *mrb, mrb_value re, mrb_value str, mrb_int pos)

mrb_regexp_pattern *pat = DATA_GET_PTR(mrb, re, &regexp_type, mrb_regexp_pattern);
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
re_check_encoding(mrb, str);

int ncap = mrb_re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos, NULL, 0,
re_binary_string_p(str));
Expand Down Expand Up @@ -493,6 +545,7 @@ regexp_match_op(mrb_state *mrb, mrb_value self)
return mrb_nil_value();
}
str = match_operand(mrb, str);
re_check_encoding(mrb, str);

mrb_value md = exec_match(mrb, self, str, 0);
if (mrb_nil_p(md)) return mrb_nil_value();
Expand All @@ -516,6 +569,7 @@ regexp_case_match(mrb_state *mrb, mrb_value self)

pat = DATA_GET_PTR(mrb, self, &regexp_type, mrb_regexp_pattern);
if (!pat) return mrb_false_value();
re_check_encoding(mrb, str);

md = exec_match(mrb, self, str, 0);
return mrb_bool_value(!mrb_nil_p(md));
Expand Down Expand Up @@ -1071,17 +1125,23 @@ has_backslash(const char *s, mrb_int len)
}

/*
* Regexp.__gsub_str(re, str, replacement) - gsub core without block
* Regexp.__gsub_str(re, str, replacement, checked = false) - gsub core without block
*
* `checked` says the caller has settled the encoding question for the subject,
* which for `gsub` means the pattern is a quoted String and the search is a
* literal one that CRuby runs without reading the subject as UTF-8.
*/
static mrb_value
regexp_s_gsub_str(mrb_state *mrb, mrb_value klass)
{
mrb_value re, str, replacement;
mrb_get_args(mrb, "oSS", &re, &str, &replacement);
mrb_bool checked = FALSE;
mrb_get_args(mrb, "oSS|b", &re, &str, &replacement, &checked);
check_regexp_arg(mrb, re);

mrb_regexp_pattern *pat = DATA_GET_PTR(mrb, re, &regexp_type, mrb_regexp_pattern);
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
if (!checked) re_check_encoding(mrb, str);

const char *s = RSTRING_PTR(str);
mrb_int slen = RSTRING_LEN(str);
Expand Down Expand Up @@ -1161,17 +1221,21 @@ regexp_s_gsub_str(mrb_state *mrb, mrb_value klass)
}

/*
* Regexp.__sub_str(re, str, replacement) - sub core without block
* Regexp.__sub_str(re, str, replacement, checked = false) - sub core without block
*
* `checked` carries the same meaning as in `__gsub_str`.
*/
static mrb_value
regexp_s_sub_str(mrb_state *mrb, mrb_value klass)
{
mrb_value re, str, replacement;
mrb_get_args(mrb, "oSS", &re, &str, &replacement);
mrb_bool checked = FALSE;
mrb_get_args(mrb, "oSS|b", &re, &str, &replacement, &checked);
check_regexp_arg(mrb, re);

mrb_regexp_pattern *pat = DATA_GET_PTR(mrb, re, &regexp_type, mrb_regexp_pattern);
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
if (!checked) re_check_encoding(mrb, str);

const char *s = RSTRING_PTR(str);
mrb_int slen = RSTRING_LEN(str);
Expand Down Expand Up @@ -1226,6 +1290,7 @@ regexp_s_scan(mrb_state *mrb, mrb_value klass)

mrb_regexp_pattern *pat = DATA_GET_PTR(mrb, re, &regexp_type, mrb_regexp_pattern);
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
re_check_encoding(mrb, str);

const char *s = RSTRING_PTR(str);
mrb_int slen = RSTRING_LEN(str);
Expand Down Expand Up @@ -1341,7 +1406,8 @@ mrb_mruby_regexp_gem_init(mrb_state *mrb)
mrb_define_class_method(mrb, re, "quote", regexp_escape, MRB_ARGS_REQ(1));
mrb_define_class_method(mrb, re, "__binary_string?", regexp_binary_string_p, MRB_ARGS_REQ(1));
mrb_define_class_method(mrb, re, "__check_pattern", regexp_check_pattern, MRB_ARGS_REQ(1));
mrb_define_class_method(mrb, re, "__search", regexp_s_search, MRB_ARGS_ARG(2, 1));
mrb_define_class_method(mrb, re, "__check_encoding", regexp_s_check_encoding, MRB_ARGS_REQ(1));
mrb_define_class_method(mrb, re, "__search", regexp_s_search, MRB_ARGS_ARG(2, 2));
mrb_define_class_method(mrb, re, "__byte_search", regexp_s_byte_search, MRB_ARGS_ARG(2, 1));
mrb_define_class_method(mrb, re, "__search_p", regexp_s_search_p, MRB_ARGS_ARG(2, 1));

Expand All @@ -1358,8 +1424,8 @@ mrb_mruby_regexp_gem_init(mrb_state *mrb)
mrb_define_method(mrb, re, "hash", regexp_hash, MRB_ARGS_NONE());
mrb_define_method(mrb, re, "options", regexp_options, MRB_ARGS_NONE());
mrb_define_method(mrb, re, "casefold?", regexp_casefold_p, MRB_ARGS_NONE());
mrb_define_class_method(mrb, re, "__gsub_str", regexp_s_gsub_str, MRB_ARGS_REQ(3));
mrb_define_class_method(mrb, re, "__sub_str", regexp_s_sub_str, MRB_ARGS_REQ(3));
mrb_define_class_method(mrb, re, "__gsub_str", regexp_s_gsub_str, MRB_ARGS_ARG(3, 1));
mrb_define_class_method(mrb, re, "__sub_str", regexp_s_sub_str, MRB_ARGS_ARG(3, 1));
mrb_define_class_method(mrb, re, "__scan", regexp_s_scan, MRB_ARGS_REQ(2));

/* MatchData class */
Expand Down
Loading
Loading