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
6 changes: 5 additions & 1 deletion include/mruby/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,11 @@ 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);

mrb_int mrb_utf8_to_buf(char *buf, uint32_t cp);
/* Write the UTF-8 spelling of a codepoint into a buffer of at least four
bytes, and return how many it took (1-4), or 0 for a value that spells no
character. What counts as one, and why a surrogate does spell one here
while mrb_utf8len() says it does not, is in the definition in string.c. */
mrb_int mrb_utf8_to_buf(char *buf, mrb_int cp);

/* What a run of bytes spells is a question apart from whether String indexes
by character, so a gem that reads UTF-8 on its own asks for these two by
Expand Down
8 changes: 3 additions & 5 deletions mrbgems/mruby-pack/src/pack.c
Original file line number Diff line number Diff line change
Expand Up @@ -777,13 +777,11 @@ pack_utf8(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, int count, u
int len;
mrb_int c = mrb_integer(o);

/* mrb_utf8_to_buf() takes a uint32_t, which wraps a value outside the
Unicode range into a codepoint, so the range has to be checked on the
mrb_int before the cast. */
if (c < 0 || 0x10FFFF < c) {
/* A value that spells no character writes no byte. */
len = (int)mrb_utf8_to_buf(utf8, c);
if (len == 0) {
mrb_raise(mrb, E_RANGE_ERROR, "pack(U): value out of range");
}
len = (int)mrb_utf8_to_buf(utf8, (uint32_t)c);

str = str_len_ensure(mrb, str, sidx + len);
memcpy(RSTRING_PTR(str) + sidx, utf8, len);
Expand Down
35 changes: 29 additions & 6 deletions mrbgems/mruby-pack/test/pack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,36 @@ def assert_pack tmpl, packed, unpacked
assert_equal [0xF4, 0x8F, 0xBF, 0xBF], [0x10FFFF].pack("U").unpack("C*")
assert_raise(RangeError) { [0x110000].pack("U") }

# The encoder takes a uint32_t, so a value that wraps into the Unicode range
# must not come out as the character it wraps to. The shift is computed at
# run time because the constant folder would reject the literal on a build
# with a 32-bit mrb_int and no bigint, where there is nothing to test.
# A value that would land inside the Unicode range if it were truncated to
# 32 bits must not come out as the character it truncates to.
# The shift width comes from a variable because `1 << 32` written out is
# constant folded, and the fold fails while this file is compiled on
# MRB_INT32 without bigint, dropping every test in it.
shift = 32
wrapping = ((1 << shift) + 0x41) rescue nil
assert_raise(RangeError) { [wrapping].pack("U") } if wrapping.is_a?(Integer)
wrapping = nil
wide = begin
wrapping = (1 << shift) + 0x41 # RangeError where mrb_int is 32 bits and bigint is absent
[][wrapping] # nil for an mrb_int index, RangeError for a big integer
true
rescue RangeError
false
end
# A big integer is not an mrb_int either: `pack` refuses it while converting
# the element, so the encoder never sees the value and the truncation this
# guards against never runs.
assert_raise(RangeError) { [wrapping].pack("U") } if wide
end

assert 'pack("U") with a UTF-16 surrogate' do
# A surrogate has a spelling here even though it is not a character: CRuby
# writes these three bytes too, and refuses the value in Integer#chr rather
# than here. unpack("U") reads them back, so the two stay a pair whatever
# the character scanner makes of the bytes.
assert_equal [0xED, 0xA0, 0x80], [0xD800].pack("U").unpack("C*")
assert_equal [0xED, 0xBF, 0xBF], [0xDFFF].pack("U").unpack("C*")
assert_equal [0xD800], [0xD800].pack("U").unpack("U*")
assert_equal [0xED, 0x9F, 0xBF], [0xD7FF].pack("U").unpack("C*")
assert_equal [0xEE, 0x80, 0x80], [0xE000].pack("U").unpack("C*")
end

assert 'unpack1' do
Expand Down
10 changes: 6 additions & 4 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,11 @@ parse_escape(re_compiler *c)
}
}

/* Reject what has no UTF-8 encoding. CRuby reports both a surrogate and a
value past the last plane as "invalid Unicode range", so neither ever
reaches mrb_utf8_to_buf(). */
/* Reject what a pattern may not name. CRuby reports both a surrogate and a
value past the last plane as "invalid Unicode range", and reports it where
the pattern is read rather than where it is emitted, so the check stays
here: mrb_utf8_to_buf() refuses the second on its own but spells the first,
and neither reaches it anyway. */
static void
check_unicode_cp(re_compiler *c, uint32_t cp)
{
Expand Down Expand Up @@ -1039,7 +1041,7 @@ emit_codepoint(re_compiler *c, uint32_t cp)
}
if ((c->flags & RE_FLAG_IGNORECASE) && emit_cp_folded(c, cp)) return;
char buf[4];
int len = (int)mrb_utf8_to_buf(buf, cp);
int len = (int)mrb_utf8_to_buf(buf, (mrb_int)cp);
for (int i = 0; i < len; i++) {
emit(c, RE_CHAR, (uint8_t)buf[i], 0);
}
Expand Down
10 changes: 5 additions & 5 deletions mrbgems/mruby-sprintf/src/sprintf.c
Original file line number Diff line number Diff line change
Expand Up @@ -536,13 +536,13 @@ mrb_str_format(mrb_state *mrb, mrb_int argc, const mrb_value *argv, mrb_value fm
/* Integer: encode directly to stack buffer (no allocation) */
mrb_int code = mrb_integer(val);
#ifdef MRB_UTF8_STRING
/* mrb_utf8_to_buf() writes nothing for a value it cannot encode,
and takes a uint32_t, which wraps a value outside the Unicode
range into a codepoint. Either way there is no byte to write. */
if (code < 0 || 0x10FFFF < code) {
/* A value that spells no character writes no byte, and is what
CRuby reports here as an invalid character rather than as a
range error. */
clen = (int)mrb_utf8_to_buf(cbuf, code);
if (clen == 0) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "invalid character");
}
clen = (int)mrb_utf8_to_buf(cbuf, (uint32_t)code);
#else
cbuf[0] = (char)(code & 0xff);
clen = 1;
Expand Down
35 changes: 29 additions & 6 deletions mrbgems/mruby-sprintf/test/sprintf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,34 @@ def mutator.to_s
# whatever byte the stack happened to hold there.
assert_raise(ArgumentError) { sprintf("%c", 0x110000) }
assert_raise(ArgumentError) { sprintf("%c", -1) }
# The encoder takes a uint32_t, so a value that wraps into the Unicode range
# must not come out as the character it wraps to. The shift is computed at
# run time because the constant folder would reject the literal on a build
# with a 32-bit mrb_int and no bigint, where there is nothing to test.
# A value that would land inside the Unicode range if it were truncated to
# 32 bits must not come out as the character it truncates to.
# The shift width comes from a variable because `1 << 32` written out is
# constant folded, and the fold fails while this file is compiled on
# MRB_INT32 without bigint, dropping every test in it.
shift = 32
wrapping = ((1 << shift) + 0x41) rescue nil
assert_raise(ArgumentError) { sprintf("%c", wrapping) } if wrapping.is_a?(Integer)
wrapping = nil
wide = begin
wrapping = (1 << shift) + 0x41 # RangeError where mrb_int is 32 bits and bigint is absent
[][wrapping] # nil for an mrb_int index, RangeError for a big integer
true
rescue RangeError
false
end
# A big integer is not an mrb_int either: `%c` takes it down the branch for
# an argument that is not an integer and refuses it there, so the encoder
# never sees the value and the truncation this guards against never runs.
assert_raise(ArgumentError) { sprintf("%c", wrapping) } if wide
end

assert('sprintf("%c") with a UTF-16 surrogate') do
skip unless __ENCODING__ == "UTF-8"
# A surrogate has a spelling here even though it is not a character: CRuby
# writes these three bytes too, and refuses the value in Integer#chr rather
# than here. So what the encoder writes is wider than what the character
# scanner reads back, and the string it builds is not valid UTF-8.
assert_equal "\xED\xA0\x80", sprintf("%c", 0xD800)
assert_equal "\xED\xBF\xBF", sprintf("%c", 0xDFFF)
assert_equal "\xED\x9F\xBF", sprintf("%c", 0xD7FF)
assert_equal "\xEE\x80\x80", sprintf("%c", 0xE000)
end
9 changes: 6 additions & 3 deletions mrbgems/mruby-string-ext/src/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ int_chr_utf8(mrb_state *mrb, mrb_value num)
mrb_int len;
mrb_value str;

/* Reject negative, above U+10FFFF, and UTF-16 surrogates (RFC 3629). */
if (cp < 0 || 0x10FFFF < cp || (0xD800 <= cp && cp <= 0xDFFF)) {
/* A value outside the Unicode range spells no character and comes back as a
zero length. A surrogate does spell one to the encoder, because CRuby's
sprintf("%c") and pack("U") spell one; Integer#chr is where CRuby refuses
it, so the refusal belongs here rather than in the encoder. */
len = mrb_utf8_to_buf(utf8, cp);
if (len == 0 || (0xD800 <= cp && cp <= 0xDFFF)) {
mrb_raisef(mrb, E_RANGE_ERROR, "%v out of char range", num);
}
len = mrb_utf8_to_buf(utf8, (uint32_t)cp);
str = mrb_str_new(mrb, utf8, len);
if (len == 1) {
RSTR_SET_ASCII_FLAG(mrb_str_ptr(str));
Expand Down
27 changes: 19 additions & 8 deletions src/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -336,15 +336,26 @@ mrb_gc_free_str(mrb_state *mrb, struct RString *str)
#define MASK01 0x01010101ul
#endif

/*
* Encode a Unicode codepoint to UTF-8 bytes.
* buf must have at least 4 bytes of space.
* Returns the number of bytes written (1-4), or 0 for invalid codepoint.
*/
/* Encode a Unicode codepoint to UTF-8 bytes, into a buffer of at least four.
Returns the number of bytes written (1-4), or 0 for a value outside
U+0000..U+10FFFF, which spells no character. The value arrives as an
mrb_int so that a negative one and one past the range are both this
function's answer to give; a caller reporting them differs only in which
exception it raises, and each raises what CRuby raises there.

A surrogate does encode. What CRuby writes for one is what mruby writes:
sprintf("%c", 0xD800) and [0xD800].pack("U") both yield ED A0 80 there.
Reading those bytes back is a separate question, and mrb_utf8len() answers
it by RFC 3629, under which a surrogate spells nothing. So what this writes
is deliberately wider than what that reads, and a string built from one is
valid_encoding? == false. */
mrb_int
mrb_utf8_to_buf(char *buf, uint32_t cp)
mrb_utf8_to_buf(char *buf, mrb_int cp)
{
if (cp < 0x80) {
if (cp < 0) {
return 0;
}
else if (cp < 0x80) {
buf[0] = (char)cp;
return 1;
}
Expand All @@ -366,7 +377,7 @@ mrb_utf8_to_buf(char *buf, uint32_t cp)
buf[3] = (char)(0x80 | (cp & 0x3F));
return 4;
}
return 0; /* invalid codepoint */
return 0; /* above U+10FFFF */
}

/* What a run of bytes spells is a question apart from whether String indexes
Expand Down
Loading