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
11 changes: 11 additions & 0 deletions include/mruby/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ mrb_int mrb_str_byte_to_char(mrb_state *mrb, mrb_value str, mrb_int bi);
definition in string.c for what it reads and what it leaves behind. */
mrb_bool mrb_str_valid_encoding_p(mrb_state *mrb, mrb_value str);

#ifdef MRB_UTF8_STRING
/* What RSTR_SINGLE_BYTE_P() reads, asking the bytes where the string does not
say rather than answering no for one nothing has read yet. See the
definition in string.c for what it leaves behind.

Only a build that reads its strings as characters has anything to tell a
single-byte string from, so a build indexing by byte carries no answer here
rather than one saying TRUE of every string. */
mrb_bool mrb_str_single_byte_p(mrb_state *mrb, mrb_value str);
#endif

/* Raise IndexError when `pos` lands inside a character of `str`, and return
otherwise. See the definition in string.c for which offsets are positions
the string has; a build without MRB_UTF8_STRING has one per byte, so this
Expand Down
16 changes: 11 additions & 5 deletions mrbgems/mruby-string-ext/src/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -1435,12 +1435,18 @@ str_casecmp(mrb_state *mrb, mrb_value self)
#ifdef MRB_UTF8_STRING
/* Whether a string holds anything the fold table could speak about. A string
of nothing but ASCII does not, and one read as bytes spells no characters
at all, so neither needs the walk. */
at all, so neither needs the walk: those are the two a single-byte string is
arrived at from.

The reading is asked for rather than taken off the flags, so that a string
nobody has read through yet is read through here. The answer is left on the
string, so the next comparison of it is a flag away; sending it down the
folding path instead would copy it, read the copy, and throw the answer away
with the copy, which is a cost that never stops being paid. */
static mrb_bool
str_folds_beyond_ascii(mrb_value str)
str_folds_beyond_ascii(mrb_state *mrb, mrb_value str)
{
struct RString *s = mrb_str_ptr(str);
return RSTR_CODERANGE(s) != MRB_STR_CODERANGE_7BIT && !RSTR_BINARY_P(s);
return !mrb_str_single_byte_p(mrb, str);
}

/* Fold the one side the tables have nothing to say about. Only one of the two
Expand Down Expand Up @@ -1485,7 +1491,7 @@ str_casecmp_p(mrb_state *mrb, mrb_value self)

/* Nothing above ASCII on either side leaves nothing for the tables to fold,
and the two strings order by their bytes as they always have. */
if (str_folds_beyond_ascii(self) || str_folds_beyond_ascii(other)) {
if (str_folds_beyond_ascii(mrb, self) || str_folds_beyond_ascii(mrb, other)) {
mrb_value a = mrb_str_dup(mrb, self);
mrb_value b = mrb_str_dup(mrb, other);
if (mrb_str_case_convert_unicode(mrb, a, MRB_CASE_FOLD) < 0) str_fold_ascii(mrb, a);
Expand Down
82 changes: 79 additions & 3 deletions src/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,22 @@ str_ascii_p(struct RString *s)
return TRUE;
}

/* Whether a character index into this string is already a byte index, asking
the bytes where the string does not say. RSTR_SINGLE_BYTE_P() reads what is
recorded and answers no for a string nothing has read yet, which sends every
later caller down the walking path however plain the bytes are. A string is
walked whole at most once here: the walk records what it finds, and it is
the same walk the character indexing would go on to do anyway. */
mrb_bool
mrb_str_single_byte_p(mrb_state *mrb, mrb_value str)
{
struct RString *s = mrb_str_ptr(str);
if (RSTR_CODERANGE(s) == MRB_STR_CODERANGE_UNKNOWN) {
mrb_str_valid_encoding_p(mrb, str);
}
return RSTR_SINGLE_BYTE_P(s);
}

/* map character index to byte offset index */
mrb_int
mrb_str_char_to_byte(mrb_state *mrb, mrb_value str, mrb_int off, mrb_int idx)
Expand Down Expand Up @@ -1027,12 +1043,57 @@ mrb_str_beg_len(mrb_int str_len, mrb_int *begp, mrb_int *lenp)
return TRUE;
}

#ifdef MRB_UTF8_STRING
/* What a substring needs of the string is where two positions are, not how
many the string has. Counting the whole of it to find that out reads every
byte however near the head the range sits, so the walk here stops at the
range instead: forward to `beg` for a position counted from the head, and
backward from the end for one counted from there. A position past the end is
what the forward walk reports by coming back longer than the string, since
mrb_str_char_to_byte() answers one byte more than it reached when the string
ends before the index does. */
static mrb_value
str_substr(mrb_state *mrb, mrb_value str, mrb_int beg, mrb_int len)
{
struct RString *s = mrb_str_ptr(str);
mrb_int slen = RSTR_LEN(s);

if (mrb_str_single_byte_p(mrb, str)) {
return mrb_str_beg_len(slen, &beg, &len) ?
mrb_str_byte_subseq(mrb, str, beg, len) : mrb_nil_value();
}
if (len < 0) return mrb_nil_value();

const char *o = RSTR_PTR(s);
mrb_int bbeg;
if (beg < 0) {
const char *e = o + slen;
const char *p = e;
for (mrb_int n = beg; n < 0; n++) {
/* stepping back off the first character leaves the string, which is the
negative index that names no position */
if (p == o) return mrb_nil_value();
p = mrb_utf8_char_head(o, p-1, e);
}
bbeg = (mrb_int)(p - o);
}
else {
bbeg = mrb_str_char_to_byte(mrb, str, 0, beg);
if (bbeg > slen) return mrb_nil_value();
}

mrb_int blen = mrb_str_char_to_byte(mrb, str, bbeg, len);
if (blen > slen - bbeg) blen = slen - bbeg;
return mrb_str_byte_subseq(mrb, str, bbeg, blen);
}
#else
static mrb_value
str_substr(mrb_state *mrb, mrb_value str, mrb_int beg, mrb_int len)
{
return mrb_str_beg_len(mrb_str_char_len(mrb, str), &beg, &len) ?
str_subseq(mrb, str, beg, len) : mrb_nil_value();
}
#endif

/*
* @param mrb The mruby state.
Expand Down Expand Up @@ -2257,6 +2318,14 @@ mrb_str_chomp_bang(mrb_state *mrb, mrb_value str)
if (!RSTR_SINGLE_BYTE_P(s) && mrb_utf8_char_head(p, pp, p + len) != pp) {
return mrb_nil_value();
}
/* Cutting bytes that are nothing but ASCII leaves what the rest is read as
standing, non-ASCII and all, so the coderange str_modify_keep_cr() kept
is still the answer. Cutting a non-ASCII byte can have taken the last of
them, and a string of nothing but ASCII stands at 7BIT rather than
VALID: what it is has to be asked again. */
if (search_nonascii(pp, pp + rslen) != pp + rslen) {
RSTR_CODERANGE_SET(s, MRB_STR_CODERANGE_UNKNOWN);
}
#endif
RSTR_SET_LEN(s, len - rslen);
p[RSTR_LEN(s)] = '\0';
Expand Down Expand Up @@ -2330,6 +2399,14 @@ mrb_str_chop_bang(mrb_state *mrb, mrb_value str)
len--;
}
}
#ifdef MRB_UTF8_STRING
/* see mrb_str_chomp_bang(): the character cut here is the last one, so a
non-ASCII lead byte at `len` is the whole of what leaves the string, and
it can have been the last non-ASCII there was. */
if ((signed char)RSTR_PTR(s)[len] < 0) {
RSTR_CODERANGE_SET(s, MRB_STR_CODERANGE_UNKNOWN);
}
#endif
RSTR_SET_LEN(s, len);
RSTR_PTR(s)[len] = '\0';
return str;
Expand Down Expand Up @@ -2623,7 +2700,7 @@ mrb_str_byteindex_m(mrb_state *mrb, mrb_value str)
static mrb_value
mrb_str_index_m(mrb_state *mrb, mrb_value str)
{
if (RSTR_CODERANGE(mrb_str_ptr(str)) == MRB_STR_CODERANGE_7BIT) {
if (mrb_str_single_byte_p(mrb, str)) {
return mrb_str_byteindex_m(mrb, str);
}

Expand Down Expand Up @@ -2922,8 +2999,7 @@ mrb_str_byterindex_m(mrb_state *mrb, mrb_value str)
static mrb_value
mrb_str_rindex_m(mrb_state *mrb, mrb_value str)
{
struct RString *s = mrb_str_ptr(str);
if (RSTR_SINGLE_BYTE_P(s)) {
if (mrb_str_single_byte_p(mrb, str)) {
return mrb_str_byterindex_m(mrb, str);
}

Expand Down
11 changes: 11 additions & 0 deletions test/t/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@
assert_equal "あ", "\x80あ"[1]
end if UTF8STRING

assert('String#[](UTF-8) counts a negative index back from the end') do
# Stepping back off the first character leaves the string, so a negative
# index reaching past the head names no position rather than wrapping to
# one. The length the range asks for is clamped to what is left after it.
assert_equal "あ", "あい"[-2]
assert_nil "あい"[-3]
assert_equal "あ", "あい"[-2, 1]
assert_nil "あい"[-3, 1]
assert_equal "あい", "あい"[-2, 5]
end if UTF8STRING

assert('String#[] with Range') do
a1 = 'abc'[1..0]
b1 = 'abc'[1..1]
Expand Down
Loading