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
4 changes: 4 additions & 0 deletions build_config/asan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

conf.gembox 'full-core'

# The UTF-8 decoders walk a string a byte at a time, and the sanitizer builds
# are what catch a walk that reads past the end.
conf.cc.defines << 'MRB_UTF8_STRING'

conf.enable_sanitizer "address,undefined"
conf.enable_debug
conf.enable_bintest
Expand Down
3 changes: 3 additions & 0 deletions build_config/boxing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
conf.compilers.each do |c|
c.defines << "MRB_#{boxing.upcase}_BOXING"
c.defines << "MRB_INT#{int}"
# UTF-8 lengths and offsets are mrb_int, and these are the only builds
# that vary its width.
c.defines << "MRB_UTF8_STRING"
c.flags << "-m#{bit}"
end
conf.linker.flags << "-m#{bit}"
Expand Down
17 changes: 10 additions & 7 deletions build_config/ci/gcc-clang.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
# mruby-regexp/test/ascii_case.rb needs, so both sides stay covered.
conf.cc.defines << 'MRB_REGEXP_UNICODE_CASE'

# mruby-encoding no longer turns UTF-8 on for the whole build, so a build that
# wants it says so. The other builds in this file keep the default, which is
# what the non-UTF-8 side of mruby-encoding needs, so both sides stay covered
# without a job of their own.
conf.cc.defines << 'MRB_UTF8_STRING'

conf.enable_test
end

Expand Down Expand Up @@ -53,13 +59,10 @@
MRuby::Build.new('default') do |conf|
conf.toolchain

# The one build here on the default gembox. It leaves out mruby-encoding,
# which is what defines MRB_UTF8_STRING, so its strings index by byte. The
# tests written as the byte-indexed mirror of the UTF-8 ones (String#scrub
# degrading to a no-op, the byte-counting halves of mruby-regexp and
# mruby-string-ext) run nowhere else: every other build in CI, here and in
# ci/msvc, takes full-core. Tests only, since the binaries this gembox adds
# are the same ones the bintest above already covers.
# The one build here on the default gembox. Every other build in CI, here and
# in ci/msvc, takes full-core, so the gems this gembox leaves out are only
# compiled away here. Tests only, since the binaries this gembox adds are the
# same ones the bintest above already covers.
conf.gembox 'default'

conf.enable_test
Expand Down
1 change: 1 addition & 0 deletions build_config/clang-asan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
conf.toolchain :clang
# include the GEM box
conf.gembox 'full-core'
conf.cc.defines << 'MRB_UTF8_STRING'

# Turn on `enable_debug` for better debugging
conf.enable_sanitizer "address,undefined"
Expand Down
80 changes: 44 additions & 36 deletions mrbgems/mruby-encoding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ This mrbgem provides a lightweight, "poorman's" encoding functionality for mruby
- **License:** MIT
- **Author:** mruby developers
- **Supported Encodings:**
- `Encoding::UTF_8`
- `Encoding::ASCII_8BIT` (aliased as `Encoding::BINARY`)
- `Encoding::UTF_8`, only in a build that defines `MRB_UTF8_STRING`

## Functionality

Expand All @@ -32,6 +32,7 @@ A module (not a class, unlike standard Ruby) that holds encoding constants.
- Changes the string's reported encoding to the specified `encoding_name` (e.g., "UTF-8", "ASCII-8BIT", "BINARY").
- The actual byte sequence of the string is not changed.
- Raises an `ArgumentError` if an unsupported encoding name is provided.
Without `MRB_UTF8_STRING`, `"UTF-8"` is such a name.

### `Integer` Method

Expand All @@ -40,57 +41,64 @@ A module (not a class, unlike standard Ruby) that holds encoding constants.
- If `encoding_name` is "UTF-8", the integer is treated as a Unicode codepoint.
- If `encoding_name` is "ASCII-8BIT" or "BINARY" (the default), the integer is treated as a byte value (0-255).
- Raises a `RangeError` if the integer is out of the valid range for the specified encoding.
- Raises an `ArgumentError` for unknown encoding names.
- Raises an `ArgumentError` for unknown encoding names, which without
`MRB_UTF8_STRING` includes `"UTF-8"`.

## Builds without `MRB_UTF8_STRING`

This gem does not define `MRB_UTF8_STRING` for the build; a build that wants
UTF-8 defines it itself. Without it mruby reads a string as bytes, so there is
no UTF-8 for this gem to name and UTF-8 becomes an encoding the build has no
entry for:

- `Encoding::UTF_8` is not defined, so naming it raises `NameError`.
- `String#encoding` answers `Encoding::BINARY` for every string.
- `String#force_encoding("UTF-8")` raises
`ArgumentError: unknown encoding name - UTF-8`, as any other unknown name
does.
- `Integer#chr("UTF-8")` raises the same `ArgumentError`.
- `String#valid_encoding?` answers true for every string, because every sequence
of bytes is valid where the string is read as bytes.

This is what CRuby does with a name it has no encoding for. To tell the two
builds apart, compare `__ENCODING__` against `"UTF-8"`.

## Usage Example

```ruby
# main.rb
if __ENCODING__ == "UTF-8"
s = "helloあ"
puts s.encoding #=> Encoding::UTF_8
puts s.encoding #=> UTF-8
puts s.valid_encoding? #=> true

s2 = "\xff".force_encoding("UTF-8")
puts s2.valid_encoding? #=> false
# the bytes are not touched, only the way they are read
bytes = "\xE3\x81\x82" # UTF-8 bytes for "あ"
puts bytes.encoding #=> UTF-8
puts bytes.length #=> 1
puts bytes.b.length #=> 3

broken = "\xff\xfe".force_encoding("UTF-8")
puts broken.valid_encoding? #=> false

s3 = "world"
s3.force_encoding("BINARY")
puts s3.encoding #=> Encoding::BINARY
puts s3.valid_encoding? #=> true (ASCII-8BIT strings are generally considered valid)
puts s3.encoding #=> ASCII-8BIT
puts s3.valid_encoding? #=> true

puts 65.chr #=> "A" (defaults to ASCII-8BIT)
puts 230.chr("UTF-8") #=> "æ" (if U+00E6 is æ)
# For mruby, this might be different based on actual UTF-8 char mapping
# For example, 12354.chr("UTF-8") might be "あ"
puts 12354.chr("UTF-8") #=> "あ"
# 0x110000.chr("UTF-8") #=> RangeError
else
s = "hello"
puts s.encoding #=> Encoding::BINARY (or ASCII-8BIT)

# Attempting to force to UTF-8 in a non-UTF-8 mruby build might be limited
# or behave as ASCII-8BIT depending on mruby's core string handling.
end

# Force encoding
my_string = "\xE3\x81\x82" # UTF-8 bytes for "あ"
puts my_string.encoding # Might be BINARY by default if not created as UTF-8 literal

my_string.force_encoding("UTF-8")
puts my_string.encoding #=> Encoding::UTF_8
puts my_string #=> あ

invalid_utf8 = "\xff\xfe"
invalid_utf8.force_encoding("UTF-8")
puts invalid_utf8.valid_encoding? #=> false

# Integer#chr
puts 65.chr # => "A"
puts 65.chr("BINARY") # => "A"
puts s.encoding #=> ASCII-8BIT

# When mruby is compiled with MRB_UTF8_STRING
if Object.const_defined?(:MRB_UTF8_STRING)
puts 12354.chr("UTF-8") # => "あ"
# puts 0x110000.chr("UTF-8") #=> RangeError
s.force_encoding("BINARY") # a name this build has
# s.force_encoding("UTF-8") #=> ArgumentError: unknown encoding name - UTF-8
# Encoding::UTF_8 #=> NameError: uninitialized constant Encoding::UTF_8
# 12354.chr("UTF-8") #=> ArgumentError: unknown encoding name - UTF-8
end

# Integer#chr reads a byte value whatever the build
puts 65.chr #=> "A"
puts 65.chr("BINARY") #=> "A"
```
1 change: 0 additions & 1 deletion mrbgems/mruby-encoding/mrbgem.rake
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@ MRuby::Gem::Specification.new('mruby-encoding') do |spec|
spec.author = 'mruby developers'
spec.summary = "Poorman's Encoding for mruby"
spec.build.defines << "HAVE_MRUBY_ENCODING_GEM"
spec.build.defines << "MRB_UTF8_STRING"
spec.add_test_dependency 'mruby-string-ext'
end
18 changes: 15 additions & 3 deletions mrbgems/mruby-encoding/src/encoding.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,20 @@ get_encoding(mrb_state *mrb, mrb_sym enc)
*
* "hello".encoding #=> "UTF-8"
* "\xff\xfe".encoding #=> "ASCII-8BIT"
*
* Without MRB_UTF8_STRING there is no UTF-8 to name, so every string is
* ASCII-8BIT (BINARY).
*/
static mrb_value
str_encoding(mrb_state *mrb, mrb_value self)
{
#ifdef MRB_UTF8_STRING
struct RString *s = mrb_str_ptr(self);
if (RSTR_BINARY_P(s)) {
return get_encoding(mrb, MRB_SYM(BINARY));
if (!RSTR_BINARY_P(s)) {
return get_encoding(mrb, MRB_SYM(UTF_8));
}
return get_encoding(mrb, MRB_SYM(UTF_8));
#endif
return get_encoding(mrb, MRB_SYM(BINARY));
}

/*
Expand All @@ -58,6 +63,9 @@ str_encoding(mrb_state *mrb, mrb_value self)
* str = "hello"
* str.force_encoding("ASCII-8BIT") #=> "hello"
* str.encoding #=> "ASCII-8BIT"
*
* Without MRB_UTF8_STRING, "UTF-8" is an encoding this build does not know,
* and naming it raises ArgumentError as any other unknown name does.
*/
static mrb_value
str_force_encoding(mrb_state *mrb, mrb_value self)
Expand All @@ -71,9 +79,11 @@ str_force_encoding(mrb_state *mrb, mrb_value self)
MRB_STR_CASECMP_P(enc, ENC_BINARY)) {
s->flags |= MRB_STR_BINARY;
}
#ifdef MRB_UTF8_STRING
else if (MRB_STR_CASECMP_P(enc, ENC_UTF8)) {
s->flags &= ~MRB_STR_BINARY;
}
#endif
else {
mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown encoding name - %v", enc);
}
Expand All @@ -100,7 +110,9 @@ mrb_mruby_encoding_gem_init(mrb_state* mrb)
mrb_value b = mrb_str_new_lit_frozen(mrb, ENC_ASCII_8BIT);
mrb_define_const_id(mrb, e, MRB_SYM(ASCII_8BIT), b);
mrb_define_const_id(mrb, e, MRB_SYM(BINARY), b);
#ifdef MRB_UTF8_STRING
mrb_define_const_id(mrb, e, MRB_SYM(UTF_8), mrb_str_new_lit_frozen(mrb, ENC_UTF8));
#endif
}

void
Expand Down
19 changes: 19 additions & 0 deletions mrbgems/mruby-encoding/test/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@
else
a = "hello"
assert_equal Encoding::BINARY, a.encoding
# `b` sets the byte-indexed flag, but with no UTF-8 to read a string as
# instead, every string answers the same way whether the flag is on or not
assert_equal Encoding::BINARY, a.b.encoding
assert_equal Encoding::BINARY, "\xff\xfe".encoding
end
end

Expand Down Expand Up @@ -406,3 +410,18 @@
assert_equal Encoding::UTF_8, "あ".center(3).encoding
end
end

assert('a build without UTF-8 does not know the name') do
# There is no UTF-8 here for a string to be read as, so "UTF-8" is a name
# this build has no encoding for, and it is turned away the way any other
# such name is. CRuby answers the same for a name it has no entry for.
assert_raise(ArgumentError) { "hello".force_encoding("UTF-8") }
assert_raise(ArgumentError) { "hello".force_encoding("utf-8") }
assert_raise(NameError) { Encoding::UTF_8 }

# the names it does have are unaffected
s = "hello"
assert_equal s, s.force_encoding("ASCII-8BIT")
assert_equal s, s.force_encoding("BINARY")
assert_equal Encoding::BINARY, s.encoding
end unless UTF8STRING
4 changes: 2 additions & 2 deletions mrbgems/mruby-regexp/mrbgem.rake
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ MRuby::Gem::Specification.new('mruby-regexp') do |spec|
# The engine reads UTF-8 whatever a build's strings index by, so it asks core
# for the functions that answer what a run of bytes spells. They wait
# behind MRB_UTF8_STRING otherwise, and this build has no reason to set that:
# mruby-encoding is what does, and the default gembox carries this gem
# without it.
# a build that wants UTF-8 asks for it in build_config, and the default
# gembox carries this gem without it.
spec.build.defines << 'MRB_UTF8_SCAN'

# Enumerator is optional: only String#gsub without a block reaches `to_enum`,
Expand Down
6 changes: 3 additions & 3 deletions mrbgems/mruby-string-ext/mrbgem.rake
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ MRuby::Gem::Specification.new('mruby-string-ext') do |spec|
# multibyte length/index) and their non-UTF-8 no-op mirror are split with a
# `skip unless/if` on whether a multibyte char reports length 1, so they
# cover complementary build modes. Forcing UTF-8 on every test build would
# leave the non-UTF-8 mirror unreachable. UTF-8 coverage comes from
# full-core builds (mruby-encoding defines MRB_UTF8_STRING); the mirror runs
# on gemboxes without it.
# leave the non-UTF-8 mirror unreachable. UTF-8 coverage comes from the
# builds that ask for MRB_UTF8_STRING themselves (see build_config); the
# mirror runs on the builds that do not.
end
Loading