diff --git a/NEWS.md b/NEWS.md index 09fc053ef0b478..1d637891d38a55 100644 --- a/NEWS.md +++ b/NEWS.md @@ -33,6 +33,30 @@ Note that each entry is kept to a minimum, see links for details. Note: We're only listing outstanding class updates. +* Enumerator + + * `Enumerator.produce` now accepts an optional `size` keyword argument + to specify the size of the enumerator. It can be an integer, + `Float::INFINITY`, a callable object (such as a lambda), or `nil` to + indicate unknown size. When not specified, the size is unknown (`nil`). + Previously, the size was always `Float::INFINITY` and not specifiable. + + ```ruby + # Infinite enumerator + enum = Enumerator.produce(1, size: Float::INFINITY, &:succ) + enum.size # => Float::INFINITY + + # Finite enumerator with known/computable size + abs_dir = File.expand_path("./baz") # => "/foo/bar/baz" + traverser = Enumerator.produce(abs_dir, size: -> { abs_dir.count("/") + 1 }) { + raise StopIteration if it == "/" + File.dirname(it) + } + traverser.size # => 4 + ``` + + [[Feature #21701]] + * Kernel * `Kernel#inspect` now checks for the existence of a `#instance_variables_to_inspect` method, @@ -454,3 +478,4 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21550]: https://bugs.ruby-lang.org/issues/21550 [Feature #21557]: https://bugs.ruby-lang.org/issues/21557 [Bug #21654]: https://bugs.ruby-lang.org/issues/21654 +[Feature #21701]: https://bugs.ruby-lang.org/issues/21701 diff --git a/defs/gmake.mk b/defs/gmake.mk index 36d65c0ea0b4c8..2422151009599c 100644 --- a/defs/gmake.mk +++ b/defs/gmake.mk @@ -2,6 +2,7 @@ reconfig config.status: export MAKE:=$(MAKE) export BASERUBY:=$(BASERUBY) +export GIT override gnumake_recursive := $(if $(findstring n,$(firstword $(MFLAGS))),,+) override mflags := $(filter-out -j%,$(MFLAGS)) MSPECOPT += $(if $(filter -j%,$(MFLAGS)),-j) diff --git a/enumerator.c b/enumerator.c index cbdd5629cf1046..c2b2bfa9a032dd 100644 --- a/enumerator.c +++ b/enumerator.c @@ -221,6 +221,7 @@ struct yielder { struct producer { VALUE init; VALUE proc; + VALUE size; }; typedef struct MEMO *lazyenum_proc_func(VALUE, struct MEMO *, VALUE, long); @@ -2876,6 +2877,7 @@ producer_mark_and_move(void *p) struct producer *ptr = p; rb_gc_mark_and_move(&ptr->init); rb_gc_mark_and_move(&ptr->proc); + rb_gc_mark_and_move(&ptr->size); } #define producer_free RUBY_TYPED_DEFAULT_FREE @@ -2919,12 +2921,13 @@ producer_allocate(VALUE klass) obj = TypedData_Make_Struct(klass, struct producer, &producer_data_type, ptr); ptr->init = Qundef; ptr->proc = Qundef; + ptr->size = Qnil; return obj; } static VALUE -producer_init(VALUE obj, VALUE init, VALUE proc) +producer_init(VALUE obj, VALUE init, VALUE proc, VALUE size) { struct producer *ptr; @@ -2936,6 +2939,7 @@ producer_init(VALUE obj, VALUE init, VALUE proc) RB_OBJ_WRITE(obj, &ptr->init, init); RB_OBJ_WRITE(obj, &ptr->proc, proc); + RB_OBJ_WRITE(obj, &ptr->size, size); return obj; } @@ -2986,12 +2990,18 @@ producer_each(VALUE obj) static VALUE producer_size(VALUE obj, VALUE args, VALUE eobj) { - return DBL2NUM(HUGE_VAL); + struct producer *ptr = producer_ptr(obj); + VALUE size = ptr->size; + + if (NIL_P(size)) return Qnil; + if (RB_INTEGER_TYPE_P(size) || RB_FLOAT_TYPE_P(size)) return size; + + return rb_funcall(size, id_call, 0); } /* * call-seq: - * Enumerator.produce(initial = nil) { |prev| block } -> enumerator + * Enumerator.produce(initial = nil, size: nil) { |prev| block } -> enumerator * * Creates an infinite enumerator from any block, just called over and * over. The result of the previous iteration is passed to the next one. @@ -3023,19 +3033,43 @@ producer_size(VALUE obj, VALUE args, VALUE eobj) * PATTERN = %r{\d+|[-/+*]} * Enumerator.produce { scanner.scan(PATTERN) }.slice_after { scanner.eos? }.first * # => ["7", "+", "38", "/", "6"] + * + * The optional +size+ keyword argument specifies the size of the enumerator, + * which can be retrieved by Enumerator#size. It can be an integer, + * +Float::INFINITY+, a callable object (such as a lambda), or +nil+ to + * indicate unknown size. When not specified, the size is unknown (+nil+). + * + * # Infinite enumerator + * enum = Enumerator.produce(1, size: Float::INFINITY, &:succ) + * enum.size # => Float::INFINITY + * + * # Finite enumerator with known/computable size + * abs_dir = File.expand_path("./baz") # => "/foo/bar/baz" + * traverser = Enumerator.produce(abs_dir, size: -> { abs_dir.count("/") + 1 }) { + * raise StopIteration if it == "/" + * File.dirname(it) + * } + * traverser.size # => 4 */ static VALUE enumerator_s_produce(int argc, VALUE *argv, VALUE klass) { - VALUE init, producer; + VALUE init, producer, opts, size; + ID keyword_ids[1]; if (!rb_block_given_p()) rb_raise(rb_eArgError, "no block given"); - if (rb_scan_args(argc, argv, "01", &init) == 0) { + keyword_ids[0] = rb_intern("size"); + rb_scan_args_kw(RB_SCAN_ARGS_LAST_HASH_KEYWORDS, argc, argv, "01:", &init, &opts); + rb_get_kwargs(opts, keyword_ids, 0, 1, &size); + + size = UNDEF_P(size) ? Qnil : convert_to_feasible_size_value(size); + + if (argc == 0 || (argc == 1 && !NIL_P(opts))) { init = Qundef; } - producer = producer_init(producer_allocate(rb_cEnumProducer), init, rb_block_proc()); + producer = producer_init(producer_allocate(rb_cEnumProducer), init, rb_block_proc(), size); return rb_enumeratorize_with_size_kw(producer, sym_each, 0, 0, producer_size, RB_NO_KEYWORDS); } diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index 6c178c12f2b4f4..a897c86b657e9c 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -38,8 +38,12 @@ $defs.push("-D""OPENSSL_SUPPRESS_DEPRECATED") +# Missing in TruffleRuby +have_func("rb_call_super_kw(0, NULL, 0)", "ruby.h") +# Ruby 3.1 have_func("rb_io_descriptor", "ruby/io.h") -have_func("rb_io_maybe_wait(0, Qnil, Qnil, Qnil)", "ruby/io.h") # Ruby 3.1 +have_func("rb_io_maybe_wait(0, Qnil, Qnil, Qnil)", "ruby/io.h") +# Ruby 3.2 have_func("rb_io_timeout", "ruby/io.h") Logging::message "=== Checking for system dependent stuff... ===\n" diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index 5fd6bff98b7bbe..9c63d9451aa59d 100644 --- a/ext/openssl/ossl.c +++ b/ext/openssl/ossl.c @@ -254,12 +254,17 @@ ossl_to_der_if_possible(VALUE obj) /* * Errors */ +static ID id_i_errors; + +static void collect_errors_into(VALUE ary); + VALUE ossl_make_error(VALUE exc, VALUE str) { unsigned long e; const char *data; int flags; + VALUE errors = rb_ary_new(); if (NIL_P(str)) str = rb_str_new(NULL, 0); @@ -276,10 +281,12 @@ ossl_make_error(VALUE exc, VALUE str) rb_str_cat_cstr(str, msg ? msg : "(null)"); if (flags & ERR_TXT_STRING && data) rb_str_catf(str, " (%s)", data); - ossl_clear_error(); + collect_errors_into(errors); } - return rb_exc_new_str(exc, str); + VALUE obj = rb_exc_new_str(exc, str); + rb_ivar_set(obj, id_i_errors, errors); + return obj; } void @@ -300,13 +307,12 @@ ossl_raise(VALUE exc, const char *fmt, ...) rb_exc_raise(ossl_make_error(exc, err)); } -void -ossl_clear_error(void) +static void +collect_errors_into(VALUE ary) { - if (dOSSL == Qtrue) { + if (dOSSL == Qtrue || !NIL_P(ary)) { unsigned long e; const char *file, *data, *func, *lib, *reason; - char append[256] = ""; int line, flags; #ifdef HAVE_ERR_GET_ERROR_ALL @@ -318,13 +324,18 @@ ossl_clear_error(void) lib = ERR_lib_error_string(e); reason = ERR_reason_error_string(e); + VALUE str = rb_sprintf("error:%08lX:%s:%s:%s", e, lib ? lib : "", + func ? func : "", reason ? reason : ""); if (flags & ERR_TXT_STRING) { if (!data) data = "(null)"; - snprintf(append, sizeof(append), " (%s)", data); + rb_str_catf(str, " (%s)", data); } - rb_warn("error on stack: error:%08lX:%s:%s:%s%s", e, lib ? lib : "", - func ? func : "", reason ? reason : "", append); + + if (dOSSL == Qtrue) + rb_warn("error on stack: %"PRIsVALUE, str); + if (!NIL_P(ary)) + rb_ary_push(ary, str); } } else { @@ -332,6 +343,47 @@ ossl_clear_error(void) } } +void +ossl_clear_error(void) +{ + collect_errors_into(Qnil); +} + +/* + * call-seq: + * ossl_error.detailed_message(**) -> string + * + * Returns the exception message decorated with the captured \OpenSSL error + * queue entries. + */ +static VALUE +osslerror_detailed_message(int argc, VALUE *argv, VALUE self) +{ + VALUE str; +#ifdef HAVE_RB_CALL_SUPER_KW + // Ruby >= 3.2 + if (RTEST(rb_funcall(rb_eException, rb_intern("method_defined?"), 1, + ID2SYM(rb_intern("detailed_message"))))) + str = rb_call_super_kw(argc, argv, RB_PASS_CALLED_KEYWORDS); + else +#endif + str = rb_funcall(self, rb_intern("message"), 0); + VALUE errors = rb_attr_get(self, id_i_errors); + + // OpenSSLError was not created by ossl_make_error() + if (!RB_TYPE_P(errors, T_ARRAY)) + return str; + + str = rb_str_resurrect(str); + rb_str_catf(str, "\nOpenSSL error queue reported %ld errors:", + RARRAY_LEN(errors)); + for (long i = 0; i < RARRAY_LEN(errors); i++) { + VALUE err = RARRAY_AREF(errors, i); + rb_str_catf(str, "\n%"PRIsVALUE, err); + } + return str; +} + /* * call-seq: * OpenSSL.errors -> [String...] @@ -1009,10 +1061,26 @@ Init_openssl(void) rb_global_variable(&eOSSLError); /* - * Generic error, - * common for all classes under OpenSSL module + * Generic error class for OpenSSL. All error classes in this library + * inherit from this class. + * + * This class indicates that an error was reported by the underlying + * \OpenSSL library. + */ + eOSSLError = rb_define_class_under(mOSSL, "OpenSSLError", rb_eStandardError); + /* + * \OpenSSL error queue entries captured at the time the exception was + * raised. The same information is printed to stderr if OpenSSL.debug is + * set to +true+. + * + * This is an array of zero or more strings, ordered from the oldest to the + * newest. The format of the strings is not stable and may vary across + * versions of \OpenSSL or versions of this Ruby extension. + * + * See also the man page ERR_get_error(3). */ - eOSSLError = rb_define_class_under(mOSSL,"OpenSSLError",rb_eStandardError); + rb_attr(eOSSLError, rb_intern_const("errors"), 1, 0, 0); + rb_define_method(eOSSLError, "detailed_message", osslerror_detailed_message, -1); /* * Init debug core @@ -1028,6 +1096,7 @@ Init_openssl(void) * Get ID of to_der */ ossl_s_to_der = rb_intern("to_der"); + id_i_errors = rb_intern("@errors"); /* * Init components diff --git a/prism/prism.c b/prism/prism.c index 20037b5b9f6b6c..7059db8f1d6c49 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -20395,6 +20395,9 @@ pm_named_capture_escape_unicode(pm_parser_t *parser, pm_buffer_t *unescaped, con } size_t length = pm_strspn_hexadecimal_digit(cursor, end - cursor); + if (length == 0) { + break; + } uint32_t value = escape_unicode(parser, cursor, length); (void) pm_buffer_append_unicode_codepoint(unescaped, value); diff --git a/test/openssl/test_ossl.rb b/test/openssl/test_ossl.rb index 554038bbdb58eb..51262985f5655e 100644 --- a/test/openssl/test_ossl.rb +++ b/test/openssl/test_ossl.rb @@ -66,16 +66,27 @@ def test_memcmp_timing end if ENV["OSSL_TEST_ALL"] == "1" def test_error_data - # X509V3_EXT_nconf_nid() called from OpenSSL::X509::ExtensionFactory#create_ext is a function - # that uses ERR_raise_data() to append additional information about the error. + # X509V3_EXT_nconf_nid() called from + # OpenSSL::X509::ExtensionFactory#create_ext is a function that uses + # ERR_raise_data() to append additional information about the error. # # The generated message should look like: # "subjectAltName = IP:not.a.valid.ip.address: bad ip address (value=not.a.valid.ip.address)" # "subjectAltName = IP:not.a.valid.ip.address: error in extension (name=subjectAltName, value=IP:not.a.valid.ip.address)" + # + # The string inside parentheses is the ERR_TXT_STRING data, and is appended + # by ossl_make_error(), so we check it here. ef = OpenSSL::X509::ExtensionFactory.new - assert_raise_with_message(OpenSSL::X509::ExtensionError, /value=(IP:)?not.a.valid.ip.address\)/) { + e = assert_raise(OpenSSL::X509::ExtensionError) { ef.create_ext("subjectAltName", "IP:not.a.valid.ip.address") } + assert_match(/not.a.valid.ip.address\)\z/, e.message) + + # We currently craft the strings based on ERR_error_string()'s style: + # error:::: (data) + assert_instance_of(Array, e.errors) + assert_match(/\Aerror:.*not.a.valid.ip.address\)\z/, e.errors.last) + assert_include(e.detailed_message, "not.a.valid.ip.address") end end diff --git a/test/ruby/test_enumerator.rb b/test/ruby/test_enumerator.rb index ddba1c09cae905..5fabea645d2b8d 100644 --- a/test/ruby/test_enumerator.rb +++ b/test/ruby/test_enumerator.rb @@ -886,12 +886,13 @@ def test_chain_undef_methods def test_produce assert_raise(ArgumentError) { Enumerator.produce } + assert_raise(ArgumentError) { Enumerator.produce(a: 1, b: 1) {} } # Without initial object passed_args = [] enum = Enumerator.produce { |obj| passed_args << obj; (obj || 0).succ } assert_instance_of(Enumerator, enum) - assert_equal Float::INFINITY, enum.size + assert_nil enum.size assert_equal [1, 2, 3], enum.take(3) assert_equal [nil, 1, 2], passed_args @@ -899,22 +900,14 @@ def test_produce passed_args = [] enum = Enumerator.produce(1) { |obj| passed_args << obj; obj.succ } assert_instance_of(Enumerator, enum) - assert_equal Float::INFINITY, enum.size + assert_nil enum.size assert_equal [1, 2, 3], enum.take(3) assert_equal [1, 2], passed_args - # With initial keyword arguments - passed_args = [] - enum = Enumerator.produce(a: 1, b: 1) { |obj| passed_args << obj; obj.shift if obj.respond_to?(:shift)} - assert_instance_of(Enumerator, enum) - assert_equal Float::INFINITY, enum.size - assert_equal [{b: 1}, [1], :a, nil], enum.take(4) - assert_equal [{b: 1}, [1], :a], passed_args - # Raising StopIteration words = "The quick brown fox jumps over the lazy dog.".scan(/\w+/) enum = Enumerator.produce { words.shift or raise StopIteration } - assert_equal Float::INFINITY, enum.size + assert_nil enum.size assert_instance_of(Enumerator, enum) assert_equal %w[The quick brown fox jumps over the lazy dog], enum.to_a @@ -924,7 +917,7 @@ def test_produce obj.respond_to?(:first) or raise StopIteration obj.first } - assert_equal Float::INFINITY, enum.size + assert_nil enum.size assert_instance_of(Enumerator, enum) assert_nothing_raised { assert_equal [ @@ -935,6 +928,25 @@ def test_produce "abc", ], enum.to_a } + + # With size keyword argument + enum = Enumerator.produce(1, size: 10) { |obj| obj.succ } + assert_equal 10, enum.size + assert_equal [1, 2, 3], enum.take(3) + + enum = Enumerator.produce(1, size: -> { 5 }) { |obj| obj.succ } + assert_equal 5, enum.size + + enum = Enumerator.produce(1, size: nil) { |obj| obj.succ } + assert_equal nil, enum.size + + enum = Enumerator.produce(1, size: Float::INFINITY) { |obj| obj.succ } + assert_equal Float::INFINITY, enum.size + + # Without initial value but with size + enum = Enumerator.produce(size: 3) { |obj| (obj || 0).succ } + assert_equal 3, enum.size + assert_equal [1, 2, 3], enum.take(3) end def test_chain_each_lambda