From b996e6d3dc520dc4aa8b22735838aac37ec06c0a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 19 Aug 2026 10:59:23 +0900 Subject: [PATCH 01/15] [ruby/psych] Propagate an exception raised in handler#event_location Every case of the event switch overwrote the rb_protect state, so only the second result was ever tested and the state from the event_location call was dropped. The parse then returned normally with the exception silently discarded. https://github.com/ruby/psych/commit/186c3fa343 Co-Authored-By: Claude Opus 5 --- ext/psych/psych_parser.c | 4 ++++ ext/psych/psych_parser_fy.c | 4 ++++ test/psych/test_parser.rb | 14 ++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/ext/psych/psych_parser.c b/ext/psych/psych_parser.c index 2729273751fe8c..b50a17ac70baf1 100644 --- a/ext/psych/psych_parser.c +++ b/ext/psych/psych_parser.c @@ -312,6 +312,10 @@ static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) event_args[3] = end_line; event_args[4] = end_column; rb_protect(protected_event_location, (VALUE)event_args, &state); + if (state) { + yaml_event_delete(&event); + rb_jump_tag(state); + } switch(event.type) { case YAML_STREAM_START_EVENT: diff --git a/ext/psych/psych_parser_fy.c b/ext/psych/psych_parser_fy.c index 96aa0fe6c25892..cb98496ebc26f2 100644 --- a/ext/psych/psych_parser_fy.c +++ b/ext/psych/psych_parser_fy.c @@ -348,6 +348,10 @@ static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) event_args[3] = SIZET2NUM(em ? (size_t)em->line : 0); event_args[4] = SIZET2NUM(em ? (size_t)em->column : 0); rb_protect(protected_event_location, (VALUE)event_args, &state); + if (state) { + fy_parser_event_free(parser->fyp, event); + rb_jump_tag(state); + } switch (event->type) { case FYET_STREAM_START: diff --git a/test/psych/test_parser.rb b/test/psych/test_parser.rb index 786cf016359b17..0464f2a73fa3b0 100644 --- a/test/psych/test_parser.rb +++ b/test/psych/test_parser.rb @@ -70,6 +70,20 @@ def test_exception_memory_leak end end + def test_event_location_exception_is_propagated + klass = Class.new(Psych::Handler) do + def event_location start_line, start_column, end_line, end_column + raise "from event_location" + end + end + + parser = Psych::Parser.new klass.new + 2.times do + ex = assert_raise(RuntimeError) { parser.parse "--- hello\n" } + assert_equal "from event_location", ex.message + end + end + def test_multiparse 3.times do @parser.parse '--- foo' From 0d4afb71fc0bcb2451b57d7f2820cf2f9d3a87e2 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 19 Aug 2026 10:59:46 +0900 Subject: [PATCH 02/15] [ruby/psych] Stop the parse loop on an event that carries no type Once libyaml has set stream_end_produced, every further yaml_parser_parse succeeds with a zeroed event, so YAML_STREAM_END_EVENT can no longer be reached and the loop calls handler#empty forever at full CPU. A parser left in that state should terminate instead. No test covers this directly because the loop only reaches it through a reentrant parse, which the parser now rejects outright. https://github.com/ruby/psych/commit/a667509a4a Co-Authored-By: Claude Opus 5 --- ext/psych/psych_parser.c | 4 ++++ ext/psych/psych_parser_fy.c | 3 +++ 2 files changed, 7 insertions(+) diff --git a/ext/psych/psych_parser.c b/ext/psych/psych_parser.c index b50a17ac70baf1..efe8c809582e34 100644 --- a/ext/psych/psych_parser.c +++ b/ext/psych/psych_parser.c @@ -500,7 +500,11 @@ static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) rb_protect(protected_end_mapping, handler, &state); break; case YAML_NO_EVENT: + /* Once libyaml has produced the stream end, every later call + * succeeds with a zeroed event and YAML_STREAM_END_EVENT can no + * longer be reached. Stop rather than loop forever. */ rb_protect(protected_empty, handler, &state); + done = 1; break; case YAML_STREAM_END_EVENT: rb_protect(protected_end_stream, handler, &state); diff --git a/ext/psych/psych_parser_fy.c b/ext/psych/psych_parser_fy.c index cb98496ebc26f2..670e7973be76b6 100644 --- a/ext/psych/psych_parser_fy.c +++ b/ext/psych/psych_parser_fy.c @@ -477,7 +477,10 @@ static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) rb_protect(protected_end_mapping, handler, &state); break; case FYET_NONE: + /* An event with no type cannot advance the stream, so stop + * rather than loop forever. */ rb_protect(protected_empty, handler, &state); + done = 1; break; case FYET_STREAM_END: rb_protect(protected_end_stream, handler, &state); From 84a30ecc874a8d464f761e44a90a1fbcc4b59eb7 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 19 Aug 2026 11:00:18 +0900 Subject: [PATCH 03/15] [ruby/psych] Reject a reentrant Psych::Parser#parse The parser calls back into Ruby for every event, so a handler can call #parse again on the same object. That reinitialised the parser and re-pointed it at new input while the outer loop was still driving it, which left the outer loop reading the inner document's freed buffer, or resuming on a parser with no input at all and tripping libyaml's read_handler assertion. Psych::Exception is used so the caller can rescue it, and the in-use flag is cleared with rb_ensure so a parser stays reusable afterwards. https://github.com/ruby/psych/commit/5f2e37dc1c Co-Authored-By: Claude Opus 5 --- ext/psych/psych_parser.c | 83 ++++++++++++++++++++++++++++++------- ext/psych/psych_parser_fy.c | 55 ++++++++++++++++++++++-- test/psych/test_parser.rb | 69 ++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 20 deletions(-) diff --git a/ext/psych/psych_parser.c b/ext/psych/psych_parser.c index efe8c809582e34..2b50fcaedd431a 100644 --- a/ext/psych/psych_parser.c +++ b/ext/psych/psych_parser.c @@ -52,19 +52,28 @@ static int io_reader(void * data, unsigned char *buf, size_t size, size_t *read) return 1; } +/* The parser calls back into Ruby for every event, so a handler can call + * Psych::Parser#parse again on the same object. parse() reinitialises the + * parser it is handed, which would pull the input out from under the loop + * still driving it, so keep a flag to reject a reentrant call. */ +typedef struct { + yaml_parser_t yaml_parser; + int parsing; +} psych_parser_t; + static void dealloc(void * ptr) { - yaml_parser_t * parser; + psych_parser_t * parser; - parser = (yaml_parser_t *)ptr; - yaml_parser_delete(parser); + parser = (psych_parser_t *)ptr; + yaml_parser_delete(&parser->yaml_parser); xfree(parser); } #if 0 static size_t memsize(const void *ptr) { - const yaml_parser_t *parser = ptr; + const psych_parser_t *parser = ptr; /* TODO: calculate parser's size */ return 0; } @@ -81,10 +90,10 @@ static const rb_data_type_t psych_parser_type = { static VALUE allocate(VALUE klass) { - yaml_parser_t * parser; - VALUE obj = TypedData_Make_Struct(klass, yaml_parser_t, &psych_parser_type, parser); + psych_parser_t * parser; + VALUE obj = TypedData_Make_Struct(klass, psych_parser_t, &psych_parser_type, parser); - yaml_parser_initialize(parser); + yaml_parser_initialize(&parser->yaml_parser); return obj; } @@ -257,9 +266,22 @@ static VALUE protected_event_location(VALUE pointer) return rb_funcall3(args[0], id_event_location, 4, args + 1); } -static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) +struct parse_args { + psych_parser_t * psych_parser; + VALUE self; + VALUE handler; + VALUE yaml; + VALUE path; +}; + +static VALUE parse_body(VALUE ptr) { - yaml_parser_t * parser; + struct parse_args * pargs = (struct parse_args *)ptr; + yaml_parser_t * parser = &pargs->psych_parser->yaml_parser; + VALUE self = pargs->self; + VALUE handler = pargs->handler; + VALUE yaml = pargs->yaml; + VALUE path = pargs->path; yaml_event_t event; int done = 0; int state = 0; @@ -267,8 +289,6 @@ static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) int encoding = rb_utf8_encindex(); rb_encoding * internal_enc = rb_default_internal_encoding(); - TypedData_Get_Struct(self, yaml_parser_t, &psych_parser_type, parser); - yaml_parser_delete(parser); yaml_parser_initialize(parser); @@ -518,6 +538,37 @@ static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) return self; } +static VALUE parse_ensure(VALUE ptr) +{ + psych_parser_t * parser = (psych_parser_t *)ptr; + + parser->parsing = 0; + + return Qnil; +} + +static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) +{ + psych_parser_t * parser; + struct parse_args pargs; + + TypedData_Get_Struct(self, psych_parser_t, &psych_parser_type, parser); + + if (parser->parsing) { + rb_raise(rb_const_get(mPsych, rb_intern("Exception")), + "parser is already parsing, it cannot be reused from a handler callback"); + } + parser->parsing = 1; + + pargs.psych_parser = parser; + pargs.self = self; + pargs.handler = handler; + pargs.yaml = yaml; + pargs.path = path; + + return rb_ensure(parse_body, (VALUE)&pargs, parse_ensure, (VALUE)parser); +} + /* * call-seq: * parser.mark # => # @@ -529,13 +580,13 @@ static VALUE mark(VALUE self) { VALUE mark_klass; VALUE args[3]; - yaml_parser_t * parser; + psych_parser_t * parser; - TypedData_Get_Struct(self, yaml_parser_t, &psych_parser_type, parser); + TypedData_Get_Struct(self, psych_parser_t, &psych_parser_type, parser); mark_klass = rb_const_get_at(cPsychParser, rb_intern("Mark")); - args[0] = SIZET2NUM(parser->mark.index); - args[1] = SIZET2NUM(parser->mark.line); - args[2] = SIZET2NUM(parser->mark.column); + args[0] = SIZET2NUM(parser->yaml_parser.mark.index); + args[1] = SIZET2NUM(parser->yaml_parser.mark.line); + args[2] = SIZET2NUM(parser->yaml_parser.mark.column); return rb_class_new_instance(3, args, mark_klass); } diff --git a/ext/psych/psych_parser_fy.c b/ext/psych/psych_parser_fy.c index 670e7973be76b6..ccb05776439372 100644 --- a/ext/psych/psych_parser_fy.c +++ b/ext/psych/psych_parser_fy.c @@ -41,6 +41,11 @@ typedef struct { size_t mark_line; size_t mark_column; size_t mark_index; + /* The parser calls back into Ruby for every event, so a handler can call + * Psych::Parser#parse again on the same object. parse() destroys and + * recreates fyp, which would pull the parser out from under the loop still + * driving it, so keep a flag to reject a reentrant call. */ + int parsing; } psych_fy_parser_t; static const struct fy_parse_cfg psych_parse_cfg = { @@ -256,17 +261,28 @@ static VALUE token_to_str(struct fy_token *tok, int encoding, rb_encoding *inter return str; } -static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) -{ +struct parse_args { psych_fy_parser_t *parser; + VALUE self; + VALUE handler; + VALUE yaml; + VALUE path; +}; + +static VALUE parse_body(VALUE ptr) +{ + struct parse_args *pargs = (struct parse_args *)ptr; + psych_fy_parser_t *parser = pargs->parser; + VALUE self = pargs->self; + VALUE handler = pargs->handler; + VALUE yaml = pargs->yaml; + VALUE path = pargs->path; struct fy_event *event; int done = 0; int state = 0; int encoding = rb_utf8_encindex(); rb_encoding *internal_enc = rb_default_internal_encoding(); - TypedData_Get_Struct(self, psych_fy_parser_t, &psych_parser_type, parser); - /* Use a pristine parser for each parse, like fy-tool does. Reusing a * parser across documents via fy_parser_reset() left the default tag * handles unset for bare (no "---") tag-led documents. */ @@ -496,6 +512,37 @@ static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) return self; } +static VALUE parse_ensure(VALUE ptr) +{ + psych_fy_parser_t *parser = (psych_fy_parser_t *)ptr; + + parser->parsing = 0; + + return Qnil; +} + +static VALUE parse(VALUE self, VALUE handler, VALUE yaml, VALUE path) +{ + psych_fy_parser_t *parser; + struct parse_args pargs; + + TypedData_Get_Struct(self, psych_fy_parser_t, &psych_parser_type, parser); + + if (parser->parsing) { + rb_raise(rb_const_get(mPsych, rb_intern("Exception")), + "parser is already parsing, it cannot be reused from a handler callback"); + } + parser->parsing = 1; + + pargs.parser = parser; + pargs.self = self; + pargs.handler = handler; + pargs.yaml = yaml; + pargs.path = path; + + return rb_ensure(parse_body, (VALUE)&pargs, parse_ensure, (VALUE)parser); +} + /* * call-seq: * parser.mark # => # diff --git a/test/psych/test_parser.rb b/test/psych/test_parser.rb index 0464f2a73fa3b0..aeb7424470eb54 100644 --- a/test/psych/test_parser.rb +++ b/test/psych/test_parser.rb @@ -26,6 +26,39 @@ def #{m} *args end end + # Calls Parser#parse again, once, from inside a callback of the parse it is + # already handling. + class ReentrantHandler < Handler + attr_accessor :parser, :inner_yaml + attr_reader :inner_error, :scalars, :empty_calls + + def initialize + @parser = nil + @inner_yaml = nil + @inner_error = nil + @scalars = [] + @empty_calls = 0 + end + + def empty + @empty_calls += 1 + raise "handler#empty keeps being called, the parse loop is not terminating" if @empty_calls > 1000 + end + + def scalar value, anchor, tag, plain, quoted, style + @scalars << value + + inner, @inner_yaml = @inner_yaml, nil + return unless inner + + begin + @parser.parse inner + rescue => e + @inner_error = e + end + end + end + def setup super @handler = EventCatcher.new @@ -84,6 +117,42 @@ def event_location start_line, start_column, end_line, end_column end end + def test_parse_is_not_reentrant + pend "Failing on JRuby" if RUBY_PLATFORM =~ /java/ + + handler = ReentrantHandler.new + handler.inner_yaml = "--- inner\n" + parser = Psych::Parser.new handler + handler.parser = parser + + parser.parse "--- outer\n" + + assert_kind_of Psych::Exception, handler.inner_error + assert_equal ['outer'], handler.scalars + assert_equal 0, handler.empty_calls + + # The in-use flag is cleared when the parse finishes, so the same parser + # can be used again afterwards. + handler.scalars.clear + parser.parse "--- second\n" + assert_equal ['second'], handler.scalars + end + + def test_parse_is_not_reentrant_with_invalid_inner_document + pend "Failing on JRuby" if RUBY_PLATFORM =~ /java/ + + handler = ReentrantHandler.new + handler.inner_yaml = "--- \x00bad\n" + parser = Psych::Parser.new handler + handler.parser = parser + + parser.parse "--- outer\n" + + assert_kind_of Psych::Exception, handler.inner_error + assert_equal ['outer'], handler.scalars + assert_equal 0, handler.empty_calls + end + def test_multiparse 3.times do @parser.parse '--- foo' From 4b3d2563c58b6c13cb6c558b2183b05c19bea119 Mon Sep 17 00:00:00 2001 From: Nikita Vasilevsky Date: Tue, 21 Jul 2026 16:56:14 -0400 Subject: [PATCH 04/15] [ruby/psych] Remove unused visitor helpers merge_key was added empty in 2025502c while merge handling remained inline in revive_hash, and it has never been called. dump_list was added empty in 59ecddb while array dumping was implemented inline in visit_array_subclass, and it has never been called. Psych visitor dispatch only targets visit_* methods. https://github.com/ruby/psych/commit/e61b6fd005 --- ext/psych/lib/psych/visitors/to_ruby.rb | 3 --- ext/psych/lib/psych/visitors/yaml_tree.rb | 3 --- 2 files changed, 6 deletions(-) diff --git a/ext/psych/lib/psych/visitors/to_ruby.rb b/ext/psych/lib/psych/visitors/to_ruby.rb index 79565dfcbccfa4..d0bf07071def08 100644 --- a/ext/psych/lib/psych/visitors/to_ruby.rb +++ b/ext/psych/lib/psych/visitors/to_ruby.rb @@ -445,9 +445,6 @@ def deduplicate key end end - def merge_key hash, key, val - end - def revive klass, node s = register(node, klass.allocate) init_with(s, revive_hash({}, node, true), node) diff --git a/ext/psych/lib/psych/visitors/yaml_tree.rb b/ext/psych/lib/psych/visitors/yaml_tree.rb index b6c86f4c94046f..9c2c4aa437e416 100644 --- a/ext/psych/lib/psych/visitors/yaml_tree.rb +++ b/ext/psych/lib/psych/visitors/yaml_tree.rb @@ -496,9 +496,6 @@ def visit_hash_subclass o end end - def dump_list o - end - def dump_exception o, msg tag = ['!ruby/exception', o.class.name].join ':' From 676b8917149190c75b72754bdfa0ccf3d4573f4c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 21 Aug 2026 10:50:54 +0900 Subject: [PATCH 05/15] [ruby/psych] v5.5.0 https://github.com/ruby/psych/commit/9b12bb3fb6 --- ext/psych/lib/psych/versions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/psych/lib/psych/versions.rb b/ext/psych/lib/psych/versions.rb index 6c1679bf65bd00..ae73330f2ef5ca 100644 --- a/ext/psych/lib/psych/versions.rb +++ b/ext/psych/lib/psych/versions.rb @@ -2,7 +2,7 @@ module Psych # The version of Psych you are using - VERSION = '5.4.0' + VERSION = '5.5.0' if RUBY_ENGINE == 'jruby' DEFAULT_SNAKEYAML_VERSION = '2.10'.freeze From f083749d7a41e97bf2918336e74add1b1b03d2b6 Mon Sep 17 00:00:00 2001 From: git Date: Fri, 21 Aug 2026 01:52:42 +0000 Subject: [PATCH 06/15] Update default gems list at 676b8917149190c75b72754bdfa0cc [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index c3599f0dbcbef4..064327addcbb12 100644 --- a/NEWS.md +++ b/NEWS.md @@ -179,7 +179,7 @@ They are still available on rubygems.org and can be installed with * 0.6.3 to [v0.6.4][pp-v0.6.4] * prism 1.9.0 * 1.7.0 to [v1.8.0][prism-v1.8.0], [v1.8.1][prism-v1.8.1], [v1.9.0][prism-v1.9.0] -* psych 5.4.0 +* psych 5.5.0 * 5.3.1 to [v5.4.0][psych-v5.4.0] * resolv 0.7.1 * 0.7.0 to [v0.7.1][resolv-v0.7.1] From d81ab89e7fbfe0a63d3eaab87a0876895a29a6b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:12:06 +0000 Subject: [PATCH 07/15] Bump taiki-e/install-action Bumps the github-actions group with 1 update in the / directory: [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `taiki-e/install-action` from 2.86.2 to 2.86.3 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/b6b84cf49ebfe0176417bdce007c624f0db37f20...5b4d68e2e660441203ab128a23676f1e4faf1532) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.86.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 4e574b6be167f8..376163fcdc11a7 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@b6b84cf49ebfe0176417bdce007c624f0db37f20 # v2.86.2 + - uses: taiki-e/install-action@5b4d68e2e660441203ab128a23676f1e4faf1532 # v2.86.3 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 2f323a61f4a78b..4fb0bd5100345b 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -152,7 +152,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@b6b84cf49ebfe0176417bdce007c624f0db37f20 # v2.86.2 + - uses: taiki-e/install-action@5b4d68e2e660441203ab128a23676f1e4faf1532 # v2.86.3 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 0f024f53b0994c38832cb39f8567f5d35a3e67f3 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 21 Aug 2026 12:46:04 +0900 Subject: [PATCH 08/15] Bump the vcpkg builtin-baseline for the gmp msys2 404 The gmp portfile at the previous baseline downloads autoconf2.71-2.71-3-any.pkg.tar.zst from msys2 mirrors by a pinned URL, but msys2 has rotated the package to -4, so every mirror returns 404 and Windows CI fails to build gmp whenever the vcpkg binary cache misses. The new baseline includes microsoft/vcpkg@37bb045f3c, which updates the URL. Co-Authored-By: Claude Fable 5 --- vcpkg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 29c5a243a632b0..a290c262c71c53 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,5 +7,5 @@ "openssl", "zlib" ], - "builtin-baseline": "9e593bb18ea69cc5095e012465dcd675a822ed0d" + "builtin-baseline": "7f3781e19cc7d4e4882a4caec01668c6f7b5c163" } \ No newline at end of file From b90cd56b27037e08a29b16bec4474f349332cfe5 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 21 Aug 2026 12:35:26 +0900 Subject: [PATCH 09/15] [ruby/rubygems] Update vendored net-http-persistent to 4.0.8 Upstream now handles the cgi/escape fallback and parses no_proxy with URI.decode_www_form itself, so the vendoring patch shrinks to the require rewrites and pipeline removal. https://github.com/ruby/rubygems/commit/8b98cab475 Co-Authored-By: Claude Fable 5 --- .../lib/net/http/persistent.rb | 18 ++++++++++++------ .../lib/net/http/persistent/pool.rb | 2 +- .../net/http/persistent/timed_stack_multi.rb | 11 ++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb index 93e403a5bb367d..00f2bca14ad31f 100644 --- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb @@ -1,10 +1,7 @@ require_relative '../../../../../vendored_net_http' require_relative '../../../../../vendored_uri' -begin - require 'cgi/escape' -rescue LoadError - require 'cgi/util' # for escaping -end +require 'cgi/escape' +require 'cgi/util' unless defined?(CGI::EscapeExt) require_relative '../../../../connection_pool/lib/connection_pool' autoload :OpenSSL, 'openssl' @@ -180,7 +177,7 @@ class Gem::Net::HTTP::Persistent ## # The version of Gem::Net::HTTP::Persistent you are using - VERSION = '4.0.6' + VERSION = '4.0.8' ## # Error class for errors raised by Gem::Net::HTTP::Persistent. Various @@ -475,6 +472,13 @@ def self.detect_idle_timeout uri, max = 10 attr_reader :verify_hostname + + ## + # Sets whether to ignore end-of-file when reading a response body + # with Content-Length headers. + + attr_accessor :ignore_eof + ## # Creates a new Gem::Net::HTTP::Persistent. # @@ -514,6 +518,7 @@ def initialize name: nil, proxy: nil, pool_size: DEFAULT_POOL_SIZE @max_retries = 1 @socket_options = [] @ssl_generation = 0 # incremented when SSL session variables change + @ignore_eof = nil @socket_options << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if Socket.const_defined? :TCP_NODELAY @@ -642,6 +647,7 @@ def connection_for uri reset connection end + http.ignore_eof = @ignore_eof if @ignore_eof http.keep_alive_timeout = @idle_timeout if @idle_timeout http.max_retries = @max_retries if http.respond_to?(:max_retries=) http.read_timeout = @read_timeout if @read_timeout diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb index 04a1e754bfdd0d..5e45ea77d1afde 100644 --- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb @@ -4,7 +4,7 @@ class Gem::Net::HTTP::Persistent::Pool < Bundler::ConnectionPool # :nodoc: attr_reader :key # :nodoc: def initialize(options = {}, &block) - super + super(**options, &block) @available = Gem::Net::HTTP::Persistent::TimedStackMulti.new(@size, &block) @key = "current-#{@available.object_id}" diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb index 034fbe39b811f8..57dfefdaae5098 100644 --- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb +++ b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb @@ -1,5 +1,10 @@ class Gem::Net::HTTP::Persistent::TimedStackMulti < Bundler::ConnectionPool::TimedStack # :nodoc: + ## + # Detects if Bundler::ConnectionPool 3.0+ is being used (needed for TimedStack subclass compatibility) + + CP_USES_KEYWORD_ARGS = Gem::Version.new(Bundler::ConnectionPool::VERSION) >= Gem::Version.new('3.0.0') # :nodoc: + ## # Returns a new hash that has arrays for keys # @@ -11,7 +16,11 @@ def self.hash_of_arrays # :nodoc: end def initialize(size = 0, &block) - super + if CP_USES_KEYWORD_ARGS + super(size: size, &block) + else + super(size, &block) + end @enqueued = 0 @ques = self.class.hash_of_arrays From 8a552222a2efc87997211b3006899afc75bd047d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 21 Aug 2026 12:35:36 +0900 Subject: [PATCH 10/15] [ruby/rubygems] Update vendored connection_pool to 3.0.2 net-http-persistent 4.0.8 detects the keyword-argument API of connection_pool 3.x, so the 2.x pin is no longer needed. https://github.com/ruby/rubygems/commit/c900f46e71 Co-Authored-By: Claude Fable 5 --- .../connection_pool/lib/connection_pool.rb | 92 +++++-------------- .../lib/connection_pool/fork.rb | 40 ++++++++ .../lib/connection_pool/timed_stack.rb | 67 +++++++------- .../lib/connection_pool/version.rb | 2 +- .../lib/connection_pool/wrapper.rb | 43 +++------ 5 files changed, 111 insertions(+), 133 deletions(-) create mode 100644 lib/bundler/vendor/connection_pool/lib/connection_pool/fork.rb diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb index e8aaf70016bdd1..c52694160800d7 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb @@ -39,76 +39,30 @@ class TimeoutError < ::Gem::Timeout::Error; end # - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true # class Bundler::ConnectionPool - DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}.freeze - - def self.wrap(options, &block) - Wrapper.new(options, &block) + def self.wrap(**, &) + Wrapper.new(**, &) end - if Process.respond_to?(:fork) - INSTANCES = ObjectSpace::WeakMap.new - private_constant :INSTANCES - - def self.after_fork - INSTANCES.values.each do |pool| - next unless pool.auto_reload_after_fork - - # We're on after fork, so we know all other threads are dead. - # All we need to do is to ensure the main thread doesn't have a - # checked out connection - pool.checkin(force: true) - pool.reload do |connection| - # Unfortunately we don't know what method to call to close the connection, - # so we try the most common one. - connection.close if connection.respond_to?(:close) - end - end - nil - end - - if ::Process.respond_to?(:_fork) # MRI 3.1+ - module ForkTracker - def _fork - pid = super - if pid == 0 - Bundler::ConnectionPool.after_fork - end - pid - end - end - Process.singleton_class.prepend(ForkTracker) - end - else - INSTANCES = nil - private_constant :INSTANCES - - def self.after_fork - # noop - end - end - - def initialize(options = {}, &block) - raise ArgumentError, "Connection pool requires a block" unless block - - options = DEFAULTS.merge(options) + attr_reader :size - @size = Integer(options.fetch(:size)) - @timeout = options.fetch(:timeout) - @auto_reload_after_fork = options.fetch(:auto_reload_after_fork) + def initialize(timeout: 5, size: 5, auto_reload_after_fork: true, name: nil, &) + raise ArgumentError, "Connection pool requires a block" unless block_given? - @available = TimedStack.new(@size, &block) + @size = Integer(size) + @timeout = Float(timeout) + @available = TimedStack.new(size: @size, &) @key = :"pool-#{@available.object_id}" @key_count = :"pool-#{@available.object_id}-count" @discard_key = :"pool-#{@available.object_id}-discard" - INSTANCES[self] = self if @auto_reload_after_fork && INSTANCES + INSTANCES[self] = self if auto_reload_after_fork && INSTANCES end - def with(options = {}) + def with(**) # We need to manage exception handling manually here in order # to work correctly with `Gem::Timeout.timeout` and `Thread#raise`. # Otherwise an interrupted Thread can leak connections. Thread.handle_interrupt(Exception => :never) do - conn = checkout(options) + conn = checkout(**) begin Thread.handle_interrupt(Exception => :immediate) do yield conn @@ -154,13 +108,15 @@ def discard_current_connection(&block) ::Thread.current[@discard_key] = block || proc { |conn| conn } end - def checkout(options = {}) + def checkout(timeout: @timeout, **) if ::Thread.current[@key] ::Thread.current[@key_count] += 1 ::Thread.current[@key] else + conn = @available.pop(timeout:, **) + ::Thread.current[@key] = conn ::Thread.current[@key_count] = 1 - ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout, options) + conn end end @@ -195,29 +151,24 @@ def checkin(force: false) # Shuts down the Bundler::ConnectionPool by passing each connection to +block+ and # then removing it from the pool. Attempting to checkout a connection after # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+. - def shutdown(&block) - @available.shutdown(&block) + def shutdown(&) + @available.shutdown(&) end ## # Reloads the Bundler::ConnectionPool by passing each connection to +block+ and then # removing it the pool. Subsequent checkouts will create new connections as # needed. - def reload(&block) - @available.shutdown(reload: true, &block) + def reload(&) + @available.shutdown(reload: true, &) end ## Reaps idle connections that have been idle for over +idle_seconds+. # +idle_seconds+ defaults to 60. - def reap(idle_seconds = 60, &block) - @available.reap(idle_seconds, &block) + def reap(idle_seconds: 60, &) + @available.reap(idle_seconds:, &) end - # Size of this connection pool - attr_reader :size - # Automatically drop all connections after fork - attr_reader :auto_reload_after_fork - # Number of pool entries available for checkout at this instant. def available @available.length @@ -231,3 +182,4 @@ def idle require_relative "connection_pool/timed_stack" require_relative "connection_pool/wrapper" +require_relative "connection_pool/fork" diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/fork.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/fork.rb new file mode 100644 index 00000000000000..69683d16c26800 --- /dev/null +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/fork.rb @@ -0,0 +1,40 @@ +class Bundler::ConnectionPool + if Process.respond_to?(:fork) + INSTANCES = ObjectSpace::WeakMap.new + private_constant :INSTANCES + + def self.after_fork + INSTANCES.each_value do |pool| + # We're in after_fork, so we know all other threads are dead. + # All we need to do is ensure the main thread doesn't have a + # checked out connection + pool.checkin(force: true) + pool.reload do |connection| + # Unfortunately we don't know what method to call to close the connection, + # so we try the most common one. + connection.close if connection.respond_to?(:close) + end + end + nil + end + + module ForkTracker + def _fork + pid = super + if pid == 0 + Bundler::ConnectionPool.after_fork + end + pid + end + end + Process.singleton_class.prepend(ForkTracker) + else + # JRuby, et al + INSTANCES = nil + private_constant :INSTANCES + + def self.after_fork + # noop + end + end +end diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb index 026d2c5be27a4b..d62a44159b5db7 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb @@ -5,7 +5,7 @@ # # Examples: # -# ts = TimedStack.new(1) { MyConnection.new } +# ts = TimedStack.new(size: 1) { MyConnection.new } # # # fetch a connection # conn = ts.pop @@ -22,7 +22,7 @@ class Bundler::ConnectionPool::TimedStack ## # Creates a new pool with +size+ connections that are created from the given # +block+. - def initialize(size = 0, &block) + def initialize(size: 0, &block) @create_block = block @created = 0 @que = [] @@ -33,15 +33,15 @@ def initialize(size = 0, &block) end ## - # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be + # Returns +obj+ to the stack. Additional kwargs are ignored in TimedStack but may be # used by subclasses that extend TimedStack. - def push(obj, options = {}) + def push(obj, **) @mutex.synchronize do if @shutdown_block @created -= 1 unless @created == 0 @shutdown_block.call(obj) else - store_connection obj, options + store_connection obj, ** end @resource.broadcast @@ -58,28 +58,23 @@ def push(obj, options = {}) # @option options [Class] :exception (Bundler::ConnectionPool::TimeoutError) Exception class to raise # if an entry was not available within the timeout period. Use `exception: false` to return nil. # - # The +timeout+ argument will be removed in 3.0. # Other options may be used by subclasses that extend TimedStack. - def pop(timeout = 0.5, options = {}) - options, timeout = timeout, 0.5 if Hash === timeout - timeout = options.fetch :timeout, timeout - + def pop(timeout: 0.5, exception: Bundler::ConnectionPool::TimeoutError, **) deadline = current_time + timeout @mutex.synchronize do loop do raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block - if (conn = try_fetch_connection(options)) + if (conn = try_fetch_connection(**)) return conn end - connection = try_create(options) + connection = try_create(**) return connection if connection to_wait = deadline - current_time if to_wait <= 0 - exc = options.fetch(:exception, Bundler::ConnectionPool::TimeoutError) - if exc - raise Bundler::ConnectionPool::TimeoutError, "Waited #{timeout} sec, #{length}/#{@max} available" + if exception + raise exception, "Waited #{timeout} sec, #{length}/#{@max} available" else return nil end @@ -108,21 +103,20 @@ def shutdown(reload: false, &block) ## # Reaps connections that were checked in more than +idle_seconds+ ago. - def reap(idle_seconds, &block) - raise ArgumentError, "reap must receive a block" unless block + def reap(idle_seconds:) + raise ArgumentError, "reap must receive a block" unless block_given? raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric) raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block - idle.times do - conn = - @mutex.synchronize do - raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block - - reserve_idle_connection(idle_seconds) - end + count = idle + count.times do + conn = @mutex.synchronize do + raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block + reserve_idle_connection(idle_seconds) + end break unless conn - block.call(conn) + yield conn end end @@ -162,15 +156,15 @@ def current_time # This method must returns a connection from the stack if one exists. Allows # subclasses with expensive match/search algorithms to avoid double-handling # their stack. - def try_fetch_connection(options = nil) - connection_stored?(options) && fetch_connection(options) + def try_fetch_connection(**) + connection_stored?(**) && fetch_connection(**) end ## # This is an extension point for TimedStack and is called with a mutex. # # This method must returns true if a connection is available on the stack. - def connection_stored?(options = nil) + def connection_stored?(**) !@que.empty? end @@ -178,7 +172,7 @@ def connection_stored?(options = nil) # This is an extension point for TimedStack and is called with a mutex. # # This method must return a connection from the stack. - def fetch_connection(options = nil) + def fetch_connection(**) @que.pop&.first end @@ -186,8 +180,8 @@ def fetch_connection(options = nil) # This is an extension point for TimedStack and is called with a mutex. # # This method must shut down all connections on the stack. - def shutdown_connections(options = nil) - while (conn = try_fetch_connection(options)) + def shutdown_connections(**) + while (conn = try_fetch_connection(**)) @created -= 1 unless @created == 0 @shutdown_block.call(conn) end @@ -203,6 +197,8 @@ def reserve_idle_connection(idle_seconds) @created -= 1 unless @created == 0 + # Most active elements are at the tail of the array. + # Most idle will be at the head so `shift` rather than `pop`. @que.shift.first end @@ -211,14 +207,17 @@ def reserve_idle_connection(idle_seconds) # # Returns true if the first connection in the stack has been idle for more than idle_seconds def idle_connections?(idle_seconds) - connection_stored? && (current_time - @que.first.last > idle_seconds) + return unless connection_stored? + # Most idle will be at the head so `first` + age = (current_time - @que.first.last) + age > idle_seconds end ## # This is an extension point for TimedStack and is called with a mutex. # # This method must return +obj+ to the stack. - def store_connection(obj, options = nil) + def store_connection(obj, **) @que.push [obj, current_time] end @@ -227,7 +226,7 @@ def store_connection(obj, options = nil) # # This method must create a connection if and only if the total number of # connections allowed has not been met. - def try_create(options = nil) + def try_create(**) unless @created == @max object = @create_block.call @created += 1 diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb index 2e9eebdbb6d2dc..509d5af2db81fd 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb @@ -1,3 +1,3 @@ class Bundler::ConnectionPool - VERSION = "2.5.5" + VERSION = "3.0.2" end diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb index dd796d1021f9d1..d11d6c8eb4b129 100644 --- a/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb +++ b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb @@ -2,20 +2,20 @@ class Bundler::ConnectionPool class Wrapper < ::BasicObject METHODS = [:with, :pool_shutdown, :wrapped_pool] - def initialize(options = {}, &block) - @pool = options.fetch(:pool) { ::Bundler::ConnectionPool.new(options, &block) } + def initialize(**options, &) + @pool = options.fetch(:pool) { ::Bundler::ConnectionPool.new(**options, &) } end def wrapped_pool @pool end - def with(&block) - @pool.with(&block) + def with(**, &) + @pool.with(**, &) end - def pool_shutdown(&block) - @pool.shutdown(&block) + def pool_shutdown(&) + @pool.shutdown(&) end def pool_size @@ -26,31 +26,18 @@ def pool_available @pool.available end - def respond_to?(id, *args) - METHODS.include?(id) || with { |c| c.respond_to?(id, *args) } + def respond_to?(id, *, **) + METHODS.include?(id) || with { |c| c.respond_to?(id, *, **) } end - # rubocop:disable Style/MissingRespondToMissing - if ::RUBY_VERSION >= "3.0.0" - def method_missing(name, *args, **kwargs, &block) - with do |connection| - connection.send(name, *args, **kwargs, &block) - end - end - elsif ::RUBY_VERSION >= "2.7.0" - ruby2_keywords def method_missing(name, *args, &block) - with do |connection| - connection.send(name, *args, &block) - end - end - else - def method_missing(name, *args, &block) - with do |connection| - connection.send(name, *args, &block) - end + def respond_to_missing?(id, *, **) + with { |c| c.respond_to?(id, *, **) } + end + + def method_missing(name, *, **, &) + with do |connection| + connection.send(name, *, **, &) end end - # rubocop:enable Style/MethodMissingSuper - # rubocop:enable Style/MissingRespondToMissing end end From 0e1aac21f0c438ac8cee975fc671ff7d9d385210 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 21 Aug 2026 12:35:46 +0900 Subject: [PATCH 11/15] [ruby/rubygems] Update vendored thor to 1.5.0 The hidden-command exclusion in find_command_possibilities was merged upstream, so the patch now only strips LCSDiff and guards the pathname require. https://github.com/ruby/rubygems/commit/581b5aba79 Co-Authored-By: Claude Fable 5 --- lib/bundler/vendor/thor/lib/thor.rb | 35 +++++++++++++++++- .../thor/lib/thor/actions/inject_into_file.rb | 37 ++++++++++++++++++- lib/bundler/vendor/thor/lib/thor/base.rb | 3 +- .../vendor/thor/lib/thor/shell/basic.rb | 20 +++++++--- .../vendor/thor/lib/thor/shell/color.rb | 2 + lib/bundler/vendor/thor/lib/thor/version.rb | 2 +- 6 files changed, 88 insertions(+), 11 deletions(-) diff --git a/lib/bundler/vendor/thor/lib/thor.rb b/lib/bundler/vendor/thor/lib/thor.rb index 945bdbd5515fe5..b78593a6ac3b7a 100644 --- a/lib/bundler/vendor/thor/lib/thor.rb +++ b/lib/bundler/vendor/thor/lib/thor.rb @@ -625,7 +625,7 @@ def normalize_command_name(meth) #:nodoc: # alias name. def find_command_possibilities(meth) len = meth.to_s.length - possibilities = all_commands.reject { |_k, c| c.hidden? }.merge(map).keys.select { |n| meth == n[0, len] }.sort + possibilities = all_commands.reject {|k, v| v.is_a?(HiddenCommand) }.merge(map).keys.select { |n| meth == n[0, len] }.sort unique_possibilities = possibilities.map { |k| map[k] || k }.uniq if possibilities.include?(meth) @@ -671,4 +671,37 @@ def help(command = nil, subcommand = false) self.class.help(shell, subcommand) end end + + map TREE_MAPPINGS => :tree + + desc "tree", "Print a tree of all available commands" + def tree + build_command_tree(self.class, "") + end + +private + + def build_command_tree(klass, indent) + # Print current class name if it's not the root Bundler::Thor class + unless klass == Bundler::Thor + say "#{indent}#{klass.namespace || 'default'}", :blue + indent = "#{indent} " + end + + # Print all commands for this class + visible_commands = klass.commands.reject { |_, cmd| cmd.hidden? || cmd.name == "help" } + commands_count = visible_commands.count + visible_commands.sort.each_with_index do |(command_name, command), i| + description = command.description.split("\n").first || "" + icon = i == (commands_count - 1) ? "└─" : "├─" + say "#{indent}#{icon} ", nil, false + say command_name, :green, false + say " (#{description})" unless description.empty? + end + + # Print all subcommands (from registered Bundler::Thor subclasses) + klass.subcommand_classes.each do |_, subclass| + build_command_tree(subclass, indent) + end + end end diff --git a/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb index 70526e615f1859..229f294b10c1ca 100644 --- a/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb +++ b/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb @@ -2,6 +2,38 @@ class Bundler::Thor module Actions + WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"} + + # Injects the given content into a file, raising an error if the contents of + # the file are not changed. Different from gsub_file, this method is reversible. + # + # ==== Parameters + # destination:: Relative path to the destination root + # data:: Data to add to the file. Can be given as a block. + # config:: give :verbose => false to not log the status and the flag + # for injection (:after or :before) or :force => true for + # insert two or more times the same content. + # + # ==== Examples + # + # insert_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n" + # + # insert_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do + # gems = ask "Which gems would you like to add?" + # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") + # end + # + def insert_into_file!(destination, *args, &block) + data = block_given? ? block : args.shift + + config = args.shift || {} + config[:after] = /\z/ unless config.key?(:before) || config.key?(:after) + config = config.merge({error_on_no_change: true}) + + action InjectIntoFile.new(self, destination, data, config) + end + alias_method :inject_into_file!, :insert_into_file! + # Injects the given content into a file. Different from gsub_file, this # method is reversible. # @@ -21,8 +53,6 @@ module Actions # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") # end # - WARNINGS = {unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the content has already been inserted!"} - def insert_into_file(destination, *args, &block) data = block_given? ? block : args.shift @@ -47,6 +77,7 @@ def initialize(base, destination, data, config) @replacement = data.is_a?(Proc) ? data.call : data @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp) + @error_on_no_change = @config.fetch(:error_on_no_change, false) end def invoke! @@ -59,6 +90,8 @@ def invoke! if exists? if replace!(/#{flag}/, content, config[:force]) say_status(:invoke) + elsif @error_on_no_change + raise Bundler::Thor::Error, "The content of #{destination} did not change" elsif replacement_present? say_status(:unchanged, color: :blue) else diff --git a/lib/bundler/vendor/thor/lib/thor/base.rb b/lib/bundler/vendor/thor/lib/thor/base.rb index b156899c1e0a4d..f378a48d589e36 100644 --- a/lib/bundler/vendor/thor/lib/thor/base.rb +++ b/lib/bundler/vendor/thor/lib/thor/base.rb @@ -13,8 +13,9 @@ class Bundler::Thor autoload :RakeCompat, File.expand_path("rake_compat", __dir__) autoload :Group, File.expand_path("group", __dir__) - # Shortcuts for help. + # Shortcuts for help and tree commands. HELP_MAPPINGS = %w(-h -? --help -D) + TREE_MAPPINGS = %w(-t --tree) # Bundler::Thor methods that should not be overwritten by the user. THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root diff --git a/lib/bundler/vendor/thor/lib/thor/shell/basic.rb b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb index da02b9422758e5..fcd84f45e76220 100644 --- a/lib/bundler/vendor/thor/lib/thor/shell/basic.rb +++ b/lib/bundler/vendor/thor/lib/thor/shell/basic.rb @@ -311,13 +311,11 @@ def file_collision_help(block_given) #:nodoc: end def show_diff(destination, content) #:nodoc: - diff_cmd = ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u" - require "tempfile" Tempfile.open(File.basename(destination), File.dirname(destination), binmode: true) do |temp| temp.write content temp.rewind - system %(#{diff_cmd} "#{destination}" "#{temp.path}") + system(*diff_tool, destination, temp.path) end end @@ -369,15 +367,25 @@ def answer_match(possibilities, answer, case_insensitive) def merge(destination, content) #:nodoc: require "tempfile" - Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination)) do |temp| + Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination), binmode: true) do |temp| temp.write content temp.rewind - system(merge_tool, temp.path, destination) + system(*merge_tool, temp.path, destination) end end def merge_tool #:nodoc: - @merge_tool ||= ENV["THOR_MERGE"] || "git difftool --no-index" + @merge_tool ||= begin + require "shellwords" + Shellwords.split(ENV["THOR_MERGE"] || "git difftool --no-index") + end + end + + def diff_tool #:nodoc: + @diff_cmd ||= begin + require "shellwords" + Shellwords.split(ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u") + end end end end diff --git a/lib/bundler/vendor/thor/lib/thor/shell/color.rb b/lib/bundler/vendor/thor/lib/thor/shell/color.rb index 5d708fadcad2dd..b09675682c871a 100644 --- a/lib/bundler/vendor/thor/lib/thor/shell/color.rb +++ b/lib/bundler/vendor/thor/lib/thor/shell/color.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "basic" class Bundler::Thor diff --git a/lib/bundler/vendor/thor/lib/thor/version.rb b/lib/bundler/vendor/thor/lib/thor/version.rb index 5474a2f71badc8..3fe3e3ae82e1cc 100644 --- a/lib/bundler/vendor/thor/lib/thor/version.rb +++ b/lib/bundler/vendor/thor/lib/thor/version.rb @@ -1,3 +1,3 @@ class Bundler::Thor - VERSION = "1.4.0" + VERSION = "1.5.0" end From b6fd0615ab43bfb3fcd5dcb66eb683d7cc91c6f7 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 21 Aug 2026 12:35:55 +0900 Subject: [PATCH 12/15] [ruby/rubygems] Update vendored timeout to 0.6.1 https://github.com/ruby/rubygems/commit/def91dda7f Co-Authored-By: Claude Fable 5 --- lib/rubygems/vendor/timeout/lib/timeout.rb | 267 +++++++++++++++------ 1 file changed, 190 insertions(+), 77 deletions(-) diff --git a/lib/rubygems/vendor/timeout/lib/timeout.rb b/lib/rubygems/vendor/timeout/lib/timeout.rb index 376b8c0e2b9e99..5235bc98460b14 100644 --- a/lib/rubygems/vendor/timeout/lib/timeout.rb +++ b/lib/rubygems/vendor/timeout/lib/timeout.rb @@ -20,9 +20,9 @@ module Gem::Timeout # The version - VERSION = "0.4.4" + VERSION = "0.6.1" - # Internal error raised to when a timeout is triggered. + # Internal exception raised to when a timeout is triggered. class ExitException < Exception def exception(*) # :nodoc: self @@ -44,12 +44,101 @@ def self.handle_timeout(message) # :nodoc: end # :stopdoc: - CONDVAR = ConditionVariable.new - QUEUE = Queue.new - QUEUE_MUTEX = Mutex.new - TIMEOUT_THREAD_MUTEX = Mutex.new - @timeout_thread = nil - private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX + + # We keep a private reference so that time mocking libraries won't break Gem::Timeout. + GET_TIME = Process.method(:clock_gettime) + if defined?(Ractor.make_shareable) + # Ractor.make_shareable(Method) only works on Ruby 4+ + Ractor.make_shareable(GET_TIME) rescue nil + end + private_constant :GET_TIME + + class State + def initialize + @condvar = ConditionVariable.new + @queue = Queue.new + @queue_mutex = Mutex.new + + @timeout_thread = nil + @timeout_thread_mutex = Mutex.new + end + + if defined?(Ractor.store_if_absent) && defined?(Ractor.shareable?) && Ractor.shareable?(GET_TIME) + # Ractor support if + # 1. Ractor.store_if_absent is available + # 2. Method object can be shareable (4.0~) + def self.instance + Ractor.store_if_absent :timeout_gem_state do + State.new + end + end + else + GLOBAL_STATE = State.new + + def self.instance + GLOBAL_STATE + end + end + + def create_timeout_thread + # Threads unexpectedly inherit the interrupt mask: https://github.com/ruby/timeout/issues/41 + # So reset the interrupt mask to the default one for the timeout thread + Thread.handle_interrupt(Object => :immediate) do + watcher = Thread.new do + requests = [] + while true + until @queue.empty? and !requests.empty? # wait to have at least one request + req = @queue.pop + requests << req unless req.done? + end + closest_deadline = requests.min_by(&:deadline).deadline + + now = 0.0 + @queue_mutex.synchronize do + while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and @queue.empty? + @condvar.wait(@queue_mutex, closest_deadline - now) + end + end + + requests.each do |req| + req.interrupt if req.expired?(now) + end + requests.reject!(&:done?) + end + end + + if !watcher.group.enclosed? && (!defined?(Ractor.main?) || Ractor.main?) + ThreadGroup::Default.add(watcher) + end + + watcher.name = "Gem::Timeout stdlib thread" + watcher.thread_variable_set(:"\0__detached_thread__", true) + watcher + end + end + + def ensure_timeout_thread_created + unless @timeout_thread&.alive? + # If the Mutex is already owned we are in a signal handler. + # In that case, just return and let the main thread create the Gem::Timeout thread. + return if @timeout_thread_mutex.owned? + + Sync.synchronize @timeout_thread_mutex do + unless @timeout_thread&.alive? + @timeout_thread = create_timeout_thread + end + end + end + end + + def add_request(request) + Sync.synchronize @queue_mutex do + @queue << request + @condvar.signal + end + end + end + private_constant :State class Request attr_reader :deadline @@ -64,6 +153,7 @@ def initialize(thread, timeout, exception_class, message) @done = false # protected by @mutex end + # Only called by the timeout thread, so does not need Sync.synchronize def done? @mutex.synchronize do @done @@ -74,6 +164,7 @@ def expired?(now) now >= @deadline end + # Only called by the timeout thread, so does not need Sync.synchronize def interrupt @mutex.synchronize do unless @done @@ -84,64 +175,36 @@ def interrupt end def finished - @mutex.synchronize do + Sync.synchronize @mutex do @done = true end end end private_constant :Request - def self.create_timeout_thread - watcher = Thread.new do - requests = [] - while true - until QUEUE.empty? and !requests.empty? # wait to have at least one request - req = QUEUE.pop - requests << req unless req.done? - end - closest_deadline = requests.min_by(&:deadline).deadline - - now = 0.0 - QUEUE_MUTEX.synchronize do - while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty? - CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now) - end - end - - requests.each do |req| - req.interrupt if req.expired?(now) - end - requests.reject!(&:done?) - end - end - ThreadGroup::Default.add(watcher) unless watcher.group.enclosed? - watcher.name = "Gem::Timeout stdlib thread" - watcher.thread_variable_set(:"\0__detached_thread__", true) - watcher - end - private_class_method :create_timeout_thread - - def self.ensure_timeout_thread_created - unless @timeout_thread and @timeout_thread.alive? - # If the Mutex is already owned we are in a signal handler. - # In that case, just return and let the main thread create the @timeout_thread. - return if TIMEOUT_THREAD_MUTEX.owned? - TIMEOUT_THREAD_MUTEX.synchronize do - unless @timeout_thread and @timeout_thread.alive? - @timeout_thread = create_timeout_thread - end + module Sync + # Calls mutex.synchronize(&block) but if that fails on CRuby due to being in a trap handler, + # run mutex.synchronize(&block) in a separate Thread instead. + def self.synchronize(mutex, &block) + begin + mutex.synchronize(&block) + rescue ThreadError => e + raise e unless e.message == "can't be called from trap context" + # Workaround CRuby issue https://bugs.ruby-lang.org/issues/19473 + # which raises on Mutex#synchronize in trap handler. + # It's expensive to create a Thread just for this, + # but better than failing. + Thread.new { + mutex.synchronize(&block) + }.join end end end - - # We keep a private reference so that time mocking libraries won't break - # Gem::Timeout. - GET_TIME = Process.method(:clock_gettime) - private_constant :GET_TIME + private_constant :Sync # :startdoc: - # Perform an operation in a block, raising an error if it takes longer than + # Perform an operation in a block, raising an exception if it takes longer than # +sec+ seconds to complete. # # +sec+:: Number of seconds to wait for the block to terminate. Any non-negative number @@ -149,45 +212,91 @@ def self.ensure_timeout_thread_created # value of 0 or +nil+ will execute the block without any timeout. # Any negative number will raise an ArgumentError. # +klass+:: Exception Class to raise if the block fails to terminate - # in +sec+ seconds. Omitting will use the default, Gem::Timeout::Error + # in +sec+ seconds. Omitting will use the default, Gem::Timeout::Error. # +message+:: Error message to raise with Exception Class. - # Omitting will use the default, "execution expired" + # Omitting will use the default, "execution expired". # # Returns the result of the block *if* the block completed before - # +sec+ seconds, otherwise throws an exception, based on the value of +klass+. + # +sec+ seconds, otherwise raises an exception, based on the value of +klass+. + # + # The exception raised to terminate the given block is the given +klass+, or + # Gem::Timeout::ExitException if +klass+ is not given. The reason for that behavior + # is that Gem::Timeout::Error inherits from RuntimeError and might be caught unexpectedly by +rescue+. + # Gem::Timeout::ExitException inherits from Exception so it will only be rescued by rescue Exception. + # Note that the Gem::Timeout::ExitException is translated to a Gem::Timeout::Error once it reaches the Gem::Timeout.timeout call, + # so outside that call it will be a Gem::Timeout::Error. # - # The exception thrown to terminate the given block cannot be rescued inside - # the block unless +klass+ is given explicitly. However, the block can use - # ensure to prevent the handling of the exception. For that reason, this - # method cannot be relied on to enforce timeouts for untrusted blocks. + # In general, be aware that the code block may rescue the exception, and in such a case not respect the timeout. + # Also, the block can use +ensure+ to prevent the handling of the exception. + # For those reasons, this method cannot be relied on to enforce timeouts for untrusted blocks. # # If a scheduler is defined, it will be used to handle the timeout by invoking - # Scheduler#timeout_after. + # Fiber::Scheduler#timeout_after. # # Note that this is both a method of module Gem::Timeout, so you can include # Gem::Timeout into your classes so they have a #timeout method, as well as # a module method, so you can call it directly as Gem::Timeout.timeout(). - def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+ + # + # ==== Ensuring the exception does not fire inside ensure blocks + # + # When using Gem::Timeout.timeout, it can be desirable to ensure the timeout exception does not fire inside an +ensure+ block. + # The simplest and best way to do so is to put the Gem::Timeout.timeout call inside the body of the +begin+/+ensure+/+end+: + # + # begin + # Gem::Timeout.timeout(sec) { some_long_operation } + # ensure + # cleanup # safe, cannot be interrupted by timeout + # end + # + # If that is not feasible, e.g. if there are +ensure+ blocks inside +some_long_operation+, + # they need to not be interrupted by timeout, and it's not possible to move these ensure blocks outside, + # one can use Thread.handle_interrupt to delay the timeout exception like so: + # + # Thread.handle_interrupt(Gem::Timeout::Error => :never) { + # Gem::Timeout.timeout(sec, Gem::Timeout::Error) do + # setup # timeout cannot happen here, no matter how long it takes + # Thread.handle_interrupt(Gem::Timeout::Error => :immediate) { + # some_long_operation # timeout can happen here + # } + # ensure + # cleanup # timeout cannot happen here, no matter how long it takes + # end + # } + # + # An important thing to note is the need to pass an exception +klass+ to Gem::Timeout.timeout, + # otherwise it does not work. Specifically, using Thread.handle_interrupt(Gem::Timeout::ExitException => ...) + # is unsupported and causes subtle errors like raising the wrong exception outside the block, do not use that. + # + # Note that Thread.handle_interrupt is somewhat dangerous because if setup or cleanup hangs + # then the current thread will hang too and the timeout will never fire. + # Also note the block might run for longer than +sec+ seconds: + # e.g. +some_long_operation+ executes for +sec+ seconds + whatever time cleanup takes. + # + # If you want the timeout to only happen on blocking operations, one can use +:on_blocking+ + # instead of +:immediate+. However, that means if the block uses no blocking operations after +sec+ seconds, + # the block will not be interrupted. + def self.timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+ return yield(sec) if sec == nil or sec.zero? raise ArgumentError, "Timeout sec must be a non-negative number" if 0 > sec message ||= "execution expired" if Fiber.respond_to?(:current_scheduler) && (scheduler = Fiber.current_scheduler)&.respond_to?(:timeout_after) - return scheduler.timeout_after(sec, klass || Error, message, &block) - end - - Gem::Timeout.ensure_timeout_thread_created - perform = Proc.new do |exc| - request = Request.new(Thread.current, sec, exc, message) - QUEUE_MUTEX.synchronize do - QUEUE << request - CONDVAR.signal + perform = Proc.new do |exc| + scheduler.timeout_after(sec, exc, message, &block) end - begin - return yield(sec) - ensure - request.finished + else + state = State.instance + state.ensure_timeout_thread_created + + perform = Proc.new do |exc| + request = Request.new(Thread.current, sec, exc, message) + state.add_request(request) + begin + return yield(sec) + ensure + request.finished + end end end @@ -197,5 +306,9 @@ def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+ Error.handle_timeout(message, &perform) end end - module_function :timeout + + # See Gem::Timeout.timeout + private def timeout(*args, &block) + Gem::Timeout.timeout(*args, &block) + end end From c8b4458732870f3dd09ebc5666f91a05ffa1a6ba Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 21 Aug 2026 12:36:03 +0900 Subject: [PATCH 13/15] [ruby/rubygems] Update vendored optparse to 0.8.1 https://github.com/ruby/rubygems/commit/21de1f2ff5 Co-Authored-By: Claude Fable 5 --- lib/rubygems/vendor/optparse/lib/optparse.rb | 53 +++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/lib/rubygems/vendor/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb index d39d9dd4e02f9d..66d5fe390e8ffc 100644 --- a/lib/rubygems/vendor/optparse/lib/optparse.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse.rb @@ -426,8 +426,9 @@ # class Gem::OptionParser # The version string - VERSION = "0.8.0" - Version = VERSION # for compatibility + VERSION = "0.8.1" + # An alias for compatibility + Version = VERSION # :stopdoc: NoArgument = [NO_ARGUMENT = :NONE, nil].freeze @@ -471,7 +472,6 @@ def candidate(key, icase = false, pat = nil, &_) Completion.candidate(key, icase, pat, &method(:each)) end - public def complete(key, icase = false, pat = nil) candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size} if candidates.size == 1 @@ -561,7 +561,7 @@ def initialize(pattern = nil, conv = nil, # Parses +arg+ and returns rest of +arg+ and matched portion to the # argument pattern. Yields when the pattern doesn't match substring. # - def parse_arg(arg) # :nodoc: + private def parse_arg(arg) # :nodoc: pattern or return nil, [arg] unless m = pattern.match(arg) yield(InvalidArgument, arg) @@ -579,14 +579,13 @@ def parse_arg(arg) # :nodoc: yield(InvalidArgument, arg) # didn't match whole arg return arg[s.length..-1], m end - private :parse_arg # # Parses argument, converts and returns +arg+, +block+ and result of # conversion. Yields at semi-error condition instead of raising an # exception. # - def conv_arg(arg, val = []) # :nodoc: + private def conv_arg(arg, val = []) # :nodoc: v, = *val if conv val = conv.call(*val) @@ -598,7 +597,6 @@ def conv_arg(arg, val = []) # :nodoc: end return arg, block, val end - private :conv_arg # # Produces the summary text. Each line of the summary is yielded to the @@ -882,14 +880,13 @@ def reject(t) # +lopts+:: Long style option list. # +nlopts+:: Negated long style options list. # - def update(sw, sopts, lopts, nsw = nil, nlopts = nil) # :nodoc: + private def update(sw, sopts, lopts, nsw = nil, nlopts = nil) # :nodoc: sopts.each {|o| @short[o] = sw} if sopts lopts.each {|o| @long[o] = sw} if lopts nlopts.each {|o| @long[o] = nsw} if nsw and nlopts used = @short.invert.update(@long.invert) @list.delete_if {|o| Switch === o and !used[o]} end - private :update # # Inserts +switch+ at the head of the list, and associates short, long @@ -1458,14 +1455,13 @@ def to_a; summarize("#{banner}".split(/^/)) end # +prv+:: Previously specified argument. # +msg+:: Exception message. # - def notwice(obj, prv, msg) # :nodoc: + private def notwice(obj, prv, msg) # :nodoc: unless !prv or prv == obj raise(ArgumentError, "argument #{msg} given twice: #{obj}", ParseError.filter_backtrace(caller(2))) end obj end - private :notwice SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc: @@ -1732,7 +1728,7 @@ def order!(argv = default_argv, into: nil, **keywords, &nonopt) parse_in_order(argv, setter, **keywords, &nonopt) end - def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, &nonopt) # :nodoc: + private def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, &nonopt) # :nodoc: opt, arg, val, rest = nil nonopt ||= proc {|a| throw :terminate, a} argv.unshift(arg) if arg = catch(:terminate) { @@ -1823,10 +1819,9 @@ def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, argv end - private :parse_in_order # Calls callback with _val_. - def callback!(cb, max_arity, *args) # :nodoc: + private def callback!(cb, max_arity, *args) # :nodoc: args.compact! if (size = args.size) < max_arity and cb.to_proc.lambda? @@ -1836,7 +1831,6 @@ def callback!(cb, max_arity, *args) # :nodoc: end cb.call(*args) end - private :callback! # # Parses command line arguments +argv+ in permutation mode and returns @@ -1950,24 +1944,22 @@ def self.getopts(*args, symbolize_names: false) # Traverses @stack, sending each element method +id+ with +args+ and # +block+. # - def visit(id, *args, &block) # :nodoc: + private def visit(id, *args, &block) # :nodoc: @stack.reverse_each do |el| el.__send__(id, *args, &block) end nil end - private :visit # # Searches +key+ in @stack for +id+ hash and returns or yields the result. # - def search(id, key) # :nodoc: + private def search(id, key) # :nodoc: block_given = block_given? visit(:search, id, key) do |k| return block_given ? yield(k) : k end end - private :search # # Completes shortened long style option switch and returns pair of @@ -1978,7 +1970,7 @@ def search(id, key) # :nodoc: # +icase+:: Search case insensitive if true. # +pat+:: Optional pattern for completion. # - def complete(typ, opt, icase = false, *pat) # :nodoc: + private def complete(typ, opt, icase = false, *pat) # :nodoc: if pat.empty? search(typ, opt) {|sw| return [sw, opt]} # exact match or... end @@ -1988,7 +1980,6 @@ def complete(typ, opt, icase = false, *pat) # :nodoc: exc = ambiguous ? AmbiguousOption : InvalidOption raise exc.new(opt, additional: proc {|o| additional_message(typ, o)}) end - private :complete # # Returns additional info. @@ -2323,42 +2314,42 @@ def message # Raises when ambiguously completable string is encountered. # class AmbiguousOption < ParseError - const_set(:Reason, 'ambiguous option') + Reason = 'ambiguous option' # :nodoc: end # # Raises when there is an argument for a switch which takes no argument. # class NeedlessArgument < ParseError - const_set(:Reason, 'needless argument') + Reason = 'needless argument' # :nodoc: end # # Raises when a switch with mandatory argument has no argument. # class MissingArgument < ParseError - const_set(:Reason, 'missing argument') + Reason = 'missing argument' # :nodoc: end # # Raises when switch is undefined. # class InvalidOption < ParseError - const_set(:Reason, 'invalid option') + Reason = 'invalid option' # :nodoc: end # # Raises when the given argument does not match required format. # class InvalidArgument < ParseError - const_set(:Reason, 'invalid argument') + Reason = 'invalid argument' # :nodoc: end # # Raises when the given argument word can't be completed uniquely. # class AmbiguousArgument < InvalidArgument - const_set(:Reason, 'ambiguous argument') + Reason = 'ambiguous argument' # :nodoc: end # @@ -2457,9 +2448,11 @@ def initialize(*args) # :nodoc: # and DecimalNumeric. See Acceptable argument classes (in source code). # module Acceptables - const_set(:DecimalInteger, Gem::OptionParser::DecimalInteger) - const_set(:OctalInteger, Gem::OptionParser::OctalInteger) - const_set(:DecimalNumeric, Gem::OptionParser::DecimalNumeric) + # :stopdoc: + DecimalInteger = Gem::OptionParser::DecimalInteger + OctalInteger = Gem::OptionParser::OctalInteger + DecimalNumeric = Gem::OptionParser::DecimalNumeric + # :startdoc: end end From 92e6e6806637301acb46092b53858f9ae2c79912 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 21 Aug 2026 12:36:12 +0900 Subject: [PATCH 14/15] [ruby/rubygems] Update vendored resolv to 0.7.1 https://github.com/ruby/rubygems/commit/201a08ab79 Co-Authored-By: Claude Fable 5 --- lib/rubygems/vendor/resolv/lib/resolv.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/rubygems/vendor/resolv/lib/resolv.rb b/lib/rubygems/vendor/resolv/lib/resolv.rb index 4f48e0642bf48a..f922c731b05026 100644 --- a/lib/rubygems/vendor/resolv/lib/resolv.rb +++ b/lib/rubygems/vendor/resolv/lib/resolv.rb @@ -35,7 +35,7 @@ class Gem::Resolv # The version string - VERSION = "0.7.0" + VERSION = "0.7.1" ## # Looks up the first IP address for +name+. @@ -487,13 +487,18 @@ def each_name(address) # * Gem::Resolv::DNS::Resource::IN::A # * Gem::Resolv::DNS::Resource::IN::AAAA # * Gem::Resolv::DNS::Resource::IN::ANY + # * Gem::Resolv::DNS::Resource::IN::CAA # * Gem::Resolv::DNS::Resource::IN::CNAME # * Gem::Resolv::DNS::Resource::IN::HINFO + # * Gem::Resolv::DNS::Resource::IN::HTTPS + # * Gem::Resolv::DNS::Resource::IN::LOC # * Gem::Resolv::DNS::Resource::IN::MINFO # * Gem::Resolv::DNS::Resource::IN::MX # * Gem::Resolv::DNS::Resource::IN::NS # * Gem::Resolv::DNS::Resource::IN::PTR # * Gem::Resolv::DNS::Resource::IN::SOA + # * Gem::Resolv::DNS::Resource::IN::SRV + # * Gem::Resolv::DNS::Resource::IN::SVCB # * Gem::Resolv::DNS::Resource::IN::TXT # * Gem::Resolv::DNS::Resource::IN::WKS # @@ -721,7 +726,8 @@ def request(sender, tout) begin reply, from = recv_reply(select_result[0]) rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD - Errno::ECONNRESET # Windows + Errno::ECONNRESET, # Windows + EOFError # No name server running on the server? # Don't wait anymore. raise ResolvTimeout @@ -930,8 +936,11 @@ def initialize(host, port=Port) end def recv_reply(readable_socks) - len = readable_socks[0].read(2).unpack('n')[0] + len_data = readable_socks[0].read(2) + raise EOFError if len_data.nil? || len_data.bytesize != 2 + len = len_data.unpack('n')[0] reply = @socks[0].read(len) + raise EOFError if reply.nil? || reply.bytesize != len return reply, nil end From 5fe8b5ca6963762c7e107b5ea463bb1c2ea5a415 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Thu, 20 Aug 2026 23:17:47 +0900 Subject: [PATCH 15/15] Use stable keys for the branch coverage structure The branch coverage structure was keyed by the address of the AST node, which is needed to let multiple compilations of the same node (e.g., an ensure clause) share one entry [Bug #16967]. However, ASTs are freed after compilation, so when code is eval'ed against the same path, a reused node address could merge unrelated branches into one entry, nondeterministically. Key the structure by [source_hash, node_id, first_lineno] instead. This is deterministic: different code eval'ed at the same path always gets separate entries, and re-evaluating the very same code at the same path and line accumulates the counters, like line coverage does. Co-Authored-By: Claude Fable 5 --- compile.c | 31 +++++++++++++++++++++---------- prism_compile.c | 12 ++++++------ test/coverage/test_coverage.rb | 28 ++++++++++++++++++++++++++++ thread.c | 8 ++++---- 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/compile.c b/compile.c index 5565192ca832aa..13509af55af39f 100644 --- a/compile.c +++ b/compile.c @@ -628,17 +628,28 @@ setup_branch(const rb_code_location_t *loc, const char *type, VALUE structure, V } static VALUE -decl_branch_base(rb_iseq_t *iseq, VALUE key, const rb_code_location_t *loc, const char *type) +decl_branch_base(rb_iseq_t *iseq, int node_id, const rb_code_location_t *loc, const char *type) { if (!branch_coverage_valid_p(iseq, loc->beg_pos.lineno)) return Qundef; /* - * if !structure[node] - * structure[node] = [type, first_lineno, first_column, last_lineno, last_column, branches = {}] + * A branch base is keyed by [source_hash (in two halves), node_id, first_lineno], + * which identifies the branch node stably even across (re-)evals against + * the same path. + * + * if !structure[key] + * structure[key] = [type, first_lineno, first_column, last_lineno, last_column, branches = {}] * else - * branches = structure[node][5] + * branches = structure[key][5] * end */ + uint64_t source_hash = ISEQ_BODY(iseq)->source_hash; + VALUE key = rb_ary_new_from_args(4, + ULONG2NUM((unsigned long)(source_hash >> 32)), + ULONG2NUM((unsigned long)(source_hash & 0xffffffff)), + INT2FIX(node_id), + INT2FIX(loc->beg_pos.lineno)); + rb_ary_freeze(key); VALUE structure = RARRAY_AREF(ISEQ_BRANCH_COVERAGE(iseq), 0); VALUE branch_base = rb_hash_aref(structure, key); @@ -7082,7 +7093,7 @@ compile_if(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int ADD_SEQ(ret, cond_seq); if (then_label->refcnt && else_label->refcnt) { - branches = decl_branch_base(iseq, PTR2NUM(node), nd_code_loc(node), type == NODE_IF ? "if" : "unless"); + branches = decl_branch_base(iseq, nd_node_id(node), nd_code_loc(node), type == NODE_IF ? "if" : "unless"); } if (then_label->refcnt) { @@ -7162,7 +7173,7 @@ compile_case(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_nod CHECK(COMPILE(head, "case base", RNODE_CASE(node)->nd_head)); - branches = decl_branch_base(iseq, PTR2NUM(node), nd_code_loc(node), "case"); + branches = decl_branch_base(iseq, nd_node_id(node), nd_code_loc(node), "case"); node = RNODE_CASE(node)->nd_body; EXPECT_NODE("NODE_CASE", node, NODE_WHEN, COMPILE_NG); @@ -7267,7 +7278,7 @@ compile_case2(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_no VALUE branches = Qfalse; int branch_id = 0; - branches = decl_branch_base(iseq, PTR2NUM(orig_node), nd_code_loc(orig_node), "case"); + branches = decl_branch_base(iseq, nd_node_id(orig_node), nd_code_loc(orig_node), "case"); INIT_ANCHOR(body_seq); endlabel = NEW_LABEL(nd_line(node)); @@ -8266,7 +8277,7 @@ compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_no INIT_ANCHOR(body_seq); INIT_ANCHOR(cond_seq); - branches = decl_branch_base(iseq, PTR2NUM(node), nd_code_loc(node), "case"); + branches = decl_branch_base(iseq, nd_node_id(node), nd_code_loc(node), "case"); node = RNODE_CASE3(node)->nd_body; EXPECT_NODE("NODE_CASE3", node, NODE_IN, COMPILE_NG); @@ -8470,7 +8481,7 @@ compile_loop(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, in if (tmp_label) ADD_LABEL(ret, tmp_label); ADD_LABEL(ret, redo_label); - branches = decl_branch_base(iseq, PTR2NUM(node), nd_code_loc(node), type == NODE_WHILE ? "while" : "until"); + branches = decl_branch_base(iseq, nd_node_id(node), nd_code_loc(node), type == NODE_WHILE ? "while" : "until"); const NODE *const coverage_node = RNODE_WHILE(node)->nd_body ? RNODE_WHILE(node)->nd_body : node; add_trace_branch_coverage( @@ -9113,7 +9124,7 @@ qcall_branch_start(rb_iseq_t *iseq, LINK_ANCHOR *const recv, VALUE *branches, co LABEL *else_label = NEW_LABEL(nd_line(line_node)); VALUE br = 0; - br = decl_branch_base(iseq, PTR2NUM(node), nd_code_loc(node), "&."); + br = decl_branch_base(iseq, nd_node_id(node), nd_code_loc(node), "&."); *branches = br; ADD_INSN(recv, line_node, dup); ADD_INSNL(recv, line_node, branchnil, else_label); diff --git a/prism_compile.c b/prism_compile.c index e9c21a9411ee24..81eda24a254dbb 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -1148,7 +1148,7 @@ pm_compile_conditional(rb_iseq_t *iseq, const pm_node_location_t *node_location, if (then_label->refcnt && else_label->refcnt && PM_BRANCH_COVERAGE_P(iseq)) { conditional_location = pm_code_location(scope_node, node); - branches = decl_branch_base(iseq, PTR2NUM(node), &conditional_location, type == PM_IF_NODE ? "if" : "unless"); + branches = decl_branch_base(iseq, (int) node->node_id, &conditional_location, type == PM_IF_NODE ? "if" : "unless"); } if (then_label->refcnt) { @@ -1278,7 +1278,7 @@ pm_compile_loop(rb_iseq_t *iseq, const pm_node_location_t *node_location, pm_nod // Establish branch coverage for the loop. if (PM_BRANCH_COVERAGE_P(iseq)) { rb_code_location_t loop_location = pm_code_location(scope_node, node); - VALUE branches = decl_branch_base(iseq, PTR2NUM(node), &loop_location, type == PM_WHILE_NODE ? "while" : "until"); + VALUE branches = decl_branch_base(iseq, (int) node->node_id, &loop_location, type == PM_WHILE_NODE ? "while" : "until"); rb_code_location_t branch_location = statements != NULL ? pm_code_location(scope_node, (const pm_node_t *) statements) : loop_location; add_trace_branch_coverage(iseq, ret, &branch_location, branch_location.beg_pos.column, 0, "body", branches); @@ -3789,7 +3789,7 @@ pm_compile_call(rb_iseq_t *iseq, const pm_call_node_t *call_node, LINK_ANCHOR *c .end_pos = { .lineno = end_location.line, .column = end_location.column } }; - branches = decl_branch_base(iseq, PTR2NUM(call_node), &code_location, "&."); + branches = decl_branch_base(iseq, (int) call_node->base.node_id, &code_location, "&."); } PUSH_INSN(ret, location, dup); @@ -7640,7 +7640,7 @@ pm_compile_case_node(rb_iseq_t *iseq, const pm_case_node_t *cast, const pm_node_ if (PM_BRANCH_COVERAGE_P(iseq)) { case_location = pm_code_location(scope_node, (const pm_node_t *) cast); - branches = decl_branch_base(iseq, PTR2NUM(cast), &case_location, "case"); + branches = decl_branch_base(iseq, (int) cast->base.node_id, &case_location, "case"); } // Loop through each clauses in the case node and compile each of @@ -7727,7 +7727,7 @@ pm_compile_case_node(rb_iseq_t *iseq, const pm_case_node_t *cast, const pm_node_ if (PM_BRANCH_COVERAGE_P(iseq)) { case_location = pm_code_location(scope_node, (const pm_node_t *) cast); - branches = decl_branch_base(iseq, PTR2NUM(cast), &case_location, "case"); + branches = decl_branch_base(iseq, (int) cast->base.node_id, &case_location, "case"); } // This is the label where everything will fall into if none of the @@ -7899,7 +7899,7 @@ pm_compile_case_match_node(rb_iseq_t *iseq, const pm_case_match_node_t *node, co if (PM_BRANCH_COVERAGE_P(iseq)) { case_location = pm_code_location(scope_node, (const pm_node_t *) node); - branches = decl_branch_base(iseq, PTR2NUM(node), &case_location, "case"); + branches = decl_branch_base(iseq, (int) node->base.node_id, &case_location, "case"); } // If there is only one pattern, then the behavior changes a bit. It diff --git a/test/coverage/test_coverage.rb b/test/coverage/test_coverage.rb index 8eb3f07be61d54..4a3b9690c09411 100644 --- a/test/coverage/test_coverage.rb +++ b/test/coverage/test_coverage.rb @@ -256,6 +256,34 @@ def test_eval_coverage end; end + def test_branch_coverage_for_eval_repeated + assert_in_out_err(["-W0", *ARGV], <<-"end;", ["2", "2", "[[0, 1], [0, 2]]"], []) + Coverage.start(eval: true, branches: true) + + code = <<-RUBY + def foo(x) + x ? 1 : 2 + end + RUBY + + # Evaluating different code at the same path yields separate entries + eval(code, TOPLEVEL_BINDING, "test.rb", 1) + eval(code.sub("foo", "bar"), TOPLEVEL_BINDING, "test.rb", 10) + foo(true) + r = Coverage.peek_result["test.rb"][:branches] + p r.size + + # Re-evaluating the very same code accumulates the counters instead of + # adding duplicated entries + eval(code, TOPLEVEL_BINDING, "test.rb", 1) + foo(true) + bar(false) + r = Coverage.peek_result["test.rb"][:branches] + p r.size + p r.values.map {|targets| targets.values.sort }.sort + end; + end + def test_eval_negative_lineno assert_in_out_err(ARGV, <<-"end;", ["[1, 1, 1]"], []) Coverage.start(eval: true, lines: true) diff --git a/thread.c b/thread.c index 924b3ece132280..33d45a8c343c67 100644 --- a/thread.c +++ b/thread.c @@ -6443,7 +6443,7 @@ rb_default_coverage(int n) branches = rb_ary_hidden_new_fill(2); /* internal data structures for branch coverage: * - * { branch base node => + * { branch base key (see decl_branch_base) => * [base_type, base_first_lineno, base_first_column, base_last_lineno, base_last_column, { * branch target id => * [target_type, target_first_lineno, target_first_column, target_last_lineno, target_last_column, target_counter_index], @@ -6453,10 +6453,10 @@ rb_default_coverage(int n) * } * * Example: - * { NODE_CASE => + * { [source_hash, node_id, lineno] => * [1, 0, 4, 3, { - * NODE_WHEN => [2, 8, 2, 9, 0], - * NODE_WHEN => [3, 8, 3, 9, 1], + * 0 => [2, 8, 2, 9, 0], + * 1 => [3, 8, 3, 9, 1], * ... * }], * ...