From 67f830e0925c328c19fc1c2f513a174c6e3ca63d Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Wed, 17 Dec 2025 14:18:43 +0900 Subject: [PATCH 01/31] [ruby/stringio] Development of 3.2.1 started. https://github.com/ruby/stringio/commit/c9cd1c9947 --- ext/stringio/stringio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 05bae94529b9db..11b3fff39c672a 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -13,7 +13,7 @@ **********************************************************************/ static const char *const -STRINGIO_VERSION = "3.2.0"; +STRINGIO_VERSION = "3.2.1"; #include From 354dc574de421e6cbfb4404abc49a5be462042a9 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 22 Dec 2025 18:42:36 -0600 Subject: [PATCH 02/31] [ruby/stringio] [DOC] Doc for StringIO#pread (https://github.com/ruby/stringio/pull/195) Previous doc unhelpfully pointed to `IO#pread`; this PR documents locally, with StringIO examples. https://github.com/ruby/stringio/commit/806f3d9741 --- doc/stringio/pread.rdoc | 65 +++++++++++++++++++++++++++++++++++++++++ ext/stringio/stringio.c | 6 ++-- 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 doc/stringio/pread.rdoc diff --git a/doc/stringio/pread.rdoc b/doc/stringio/pread.rdoc new file mode 100644 index 00000000000000..2dcbc18ad81a81 --- /dev/null +++ b/doc/stringio/pread.rdoc @@ -0,0 +1,65 @@ +**Note**: \Method +pread+ is different from other reading methods +in that it does not modify +self+ in any way; +thus, multiple threads may read safely from the same stream. + +Reads up to +maxlen+ bytes from the stream, +beginning at 0-based byte offset +offset+; +returns a string containing the read bytes. + +The returned string: + +- Contains +maxlen+ bytes from the stream, if available; + otherwise contains all available bytes. +- Has encoding +Encoding::ASCII_8BIT+. + +With only arguments +maxlen+ and +offset+ given, +returns a new string: + + english = 'Hello' # Five 1-byte characters. + strio = StringIO.new(english) + strio.pread(3, 0) # => "Hel" + strio.pread(3, 2) # => "llo" + strio.pread(0, 0) # => "" + strio.pread(50, 0) # => "Hello" + strio.pread(50, 2) # => "llo" + strio.pread(50, 4) # => "o" + strio.pread(0, 0).encoding + # => # + + russian = 'Привет' # Six 2-byte characters. + strio = StringIO.new(russian) + strio.pread(50, 0) # All 12 bytes. + # => "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" + strio.pread(3, 0) # => "\xD0\x9F\xD1" + strio.pread(3, 3) # => "\x80\xD0\xB8" + strio.pread(0, 0).encoding + # => # + + japanese = 'こんにちは' # Five 3-byte characters. + strio = StringIO.new(japanese) + strio.pread(50, 0) # All 15 bytes. + # => "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF" + strio.pread(6, 0) # => "\xE3\x81\x93\xE3\x82\x93" + strio.pread(1, 2) # => "\x93" + strio.pread(0, 0).encoding + # => # + +Raises an exception if +offset+ is out-of-range: + + strio = StringIO.new(english) + strio.pread(5, 50) # Raises EOFError: end of file reached + +With string argument +out_string+ given: + +- Reads as above. +- Overwrites the content of +out_string+ with the read bytes. + +Examples: + + out_string = 'Will be overwritten' + out_string.encoding # => # + result = StringIO.new(english).pread(50, 0, out_string) + result.__id__ == out_string.__id__ # => true + out_string # => "Hello" + out_string.encoding # => # + diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 11b3fff39c672a..1772be5f521d77 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -1723,10 +1723,10 @@ strio_read(int argc, VALUE *argv, VALUE self) /* * call-seq: - * pread(maxlen, offset) -> string - * pread(maxlen, offset, out_string) -> string + * pread(maxlen, offset, out_string = nil) -> new_string or out_string + * + * :include: stringio/pread.rdoc * - * See IO#pread. */ static VALUE strio_pread(int argc, VALUE *argv, VALUE self) From 9a76ccdbabbd7d2814a3106cc10d2740b6120ab9 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 22 Dec 2025 18:43:00 -0600 Subject: [PATCH 03/31] [ruby/stringio] [DOC] Doc for StringIO#putc (https://github.com/ruby/stringio/pull/196) Previous doc merely linked to `IO#putc`. The new doc stays local, provides examples using `StringIO` objects. https://github.com/ruby/stringio/commit/8983f32c50 --- doc/stringio/putc.rdoc | 82 +++++++++++++++++++++++++++++++++++++++++ ext/stringio/stringio.c | 5 ++- 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 doc/stringio/putc.rdoc diff --git a/doc/stringio/putc.rdoc b/doc/stringio/putc.rdoc new file mode 100644 index 00000000000000..4636ffa0db8b34 --- /dev/null +++ b/doc/stringio/putc.rdoc @@ -0,0 +1,82 @@ +Replaces one or more bytes at position +pos+ +with bytes of the given argument; +advances the position by the count of bytes written; +returns the argument. + +\StringIO object for 1-byte characters. + + strio = StringIO.new('foo') + strio.pos # => 0 + +With 1-byte argument, replaces one byte: + + strio.putc('b') + strio.string # => "boo" + strio.pos # => 1 + strio.putc('a') # => "a" + strio.string # => "bao" + strio.pos # => 2 + strio.putc('r') # => "r" + strio.string # => "bar" + strio.pos # => 3 + strio.putc('n') # => "n" + strio.string # => "barn" + strio.pos # => 4 + +Fills with null characters if necessary: + + strio.pos = 6 + strio.putc('x') # => "x" + strio.string # => "barn\u0000\u0000x" + strio.pos # => 7 + +With integer argument, replaces one byte with the low-order byte of the integer: + + strio = StringIO.new('foo') + strio.putc(70) + strio.string # => "Foo" + strio.putc(79) + strio.string # => "FOo" + strio.putc(79 + 1024) + strio.string # => "FOO" + +\StringIO object for Multi-byte characters: + + greek = 'αβγδε' # Five 2-byte characters. + strio = StringIO.new(greek) + strio.string# => "αβγδε" + strio.string.b # => "\xCE\xB1\xCE\xB2\xCE\xB3\xCE\xB4\xCE\xB5" + strio.string.bytesize # => 10 + strio.string.chars # => ["α", "β", "γ", "δ", "ε"] + strio.string.size # => 5 + +With 1-byte argument, replaces one byte of the string: + + strio.putc(' ') # 1-byte ascii space. + strio.pos # => 1 + strio.string # => " \xB1βγδε" + strio.string.b # => " \xB1\xCE\xB2\xCE\xB3\xCE\xB4\xCE\xB5" + strio.string.bytesize # => 10 + strio.string.chars # => [" ", "\xB1", "β", "γ", "δ", "ε"] + strio.string.size # => 6 + + strio.putc(' ') + strio.pos # => 2 + strio.string # => " βγδε" + strio.string.b # => " \xCE\xB2\xCE\xB3\xCE\xB4\xCE\xB5" + strio.string.bytesize # => 10 + strio.string.chars # => [" ", " ", "β", "γ", "δ", "ε"] + strio.string.size # => 6 + +With 2-byte argument, replaces two bytes of the string: + + strio.rewind + strio.putc('α') + strio.pos # => 2 + strio.string # => "αβγδε" + strio.string.b # => "\xCE\xB1\xCE\xB2\xCE\xB3\xCE\xB4\xCE\xB5" + strio.string.bytesize # => 10 + strio.string.chars # => ["α", "β", "γ", "δ", "ε"] + strio.string.size # => 5 + +Related: #getc, #ungetc. diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 1772be5f521d77..fa2a36cd0e58a8 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -1615,9 +1615,10 @@ strio_write(VALUE self, VALUE str) /* * call-seq: - * strio.putc(obj) -> obj + * putc(object) -> object + * + * :include: stringio/putc.rdoc * - * See IO#putc. */ static VALUE strio_putc(VALUE self, VALUE ch) From ae46f916f1e686b5f7cc80402f2e8b5f299abc3c Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 22 Dec 2025 18:43:58 -0600 Subject: [PATCH 04/31] [ruby/stringio] [DOC] Doc for StringIO#read (https://github.com/ruby/stringio/pull/197) Previous doc merely linked to `IO#read`; new doc stays local, shows examples using `StringIO`. https://github.com/ruby/stringio/commit/e8b66f8cdd --- doc/stringio/read.rdoc | 83 +++++++++++++++++++++++++++++++++++++++++ ext/stringio/stringio.c | 5 ++- 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 doc/stringio/read.rdoc diff --git a/doc/stringio/read.rdoc b/doc/stringio/read.rdoc new file mode 100644 index 00000000000000..46b9fa349f284f --- /dev/null +++ b/doc/stringio/read.rdoc @@ -0,0 +1,83 @@ +Reads and returns a string containing bytes read from the stream, +beginning at the current position; +advances the position by the count of bytes read. + +With no arguments given, +reads all remaining bytes in the stream; +returns a new string containing bytes read: + + strio = StringIO.new('Hello') # Five 1-byte characters. + strio.read # => "Hello" + strio.pos # => 5 + strio.read # => "" + StringIO.new('').read # => "" + +With non-negative argument +maxlen+ given, +reads +maxlen+ bytes as available; +returns a new string containing the bytes read, or +nil+ if none: + + strio.rewind + strio.read(3) # => "Hel" + strio.read(3) # => "lo" + strio.read(3) # => nil + + russian = 'Привет' # Six 2-byte characters. + russian.b + # => "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" + strio = StringIO.new(russian) + strio.read(6) # => "\xD0\x9F\xD1\x80\xD0\xB8" + strio.read(6) # => "\xD0\xB2\xD0\xB5\xD1\x82" + strio.read(6) # => nil + + japanese = 'こんにちは' + japanese.b + # => "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF" + strio = StringIO.new(japanese) + strio.read(9) # => "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB" + strio.read(9) # => "\xE3\x81\xA1\xE3\x81\xAF" + strio.read(9) # => nil + +With argument +max_len+ as +nil+ and string argument +out_string+ given, +reads the remaining bytes in the stream; +clears +out_string+ and writes the bytes into it; +returns +out_string+: + + out_string = 'Will be overwritten' + strio = StringIO.new('Hello') + strio.read(nil, out_string) # => "Hello" + strio.read(nil, out_string) # => "" + +With non-negative argument +maxlen+ and string argument +out_string+ given, +reads the +maxlen bytes from the stream, as availble; +clears +out_string+ and writes the bytes into it; +returns +out_string+ if any bytes were read, or +nil+ if none: + + out_string = 'Will be overwritten' + strio = StringIO.new('Hello') + strio.read(3, out_string) # => "Hel" + strio.read(3, out_string) # => "lo" + strio.read(3, out_string) # => nil + + out_string = 'Will be overwritten' + strio = StringIO.new(russian) + strio.read(6, out_string) # => "При" + strio.read(6, out_string) # => "вет" + strio.read(6, out_string) # => nil + strio.rewind + russian.b + # => "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82" + strio.read(3) # => "\xD0\x9F\xD1" + strio.read(3) # => "\x80\xD0\xB8" + + out_string = 'Will be overwritten' + strio = StringIO.new(japanese) + strio.read(9, out_string) # => "こんに" + strio.read(9, out_string) # => "ちは" + strio.read(9, out_string) # => nil + strio.rewind + japanese.b + # => "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF" + strio.read(4) # => "\xE3\x81\x93\xE3" + strio.read(4) # => "\x82\x93\xE3\x81" + +Related: #gets, #readlines. diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index fa2a36cd0e58a8..93a419ff3172fa 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -1650,9 +1650,10 @@ strio_putc(VALUE self, VALUE ch) /* * call-seq: - * strio.read([length [, outbuf]]) -> string, outbuf, or nil + * read(maxlen = nil, out_string = nil) → new_string, out_string, or nil + * + * :include: stringio/read.rdoc * - * See IO#read. */ static VALUE strio_read(int argc, VALUE *argv, VALUE self) From f09e35ee4a5283c1b6185383f9b89eb4caf99868 Mon Sep 17 00:00:00 2001 From: Maciej Mensfeld Date: Thu, 18 Dec 2025 09:36:35 +0100 Subject: [PATCH 05/31] [ruby/date] [ruby/date] Optimize Gregorian date conversions with Neri-Schneider algorithm Replace floating-point arithmetic and iterative loops with pure integer operations for ~40% faster Date operations. Date.ordinal and Date.commercial are ~2x faster due to O(1) first-day-of-year calculation. Reference: https://arxiv.org/abs/2102.06959 https://github.com/ruby/date/commit/cc639549d6 --- ext/date/date_core.c | 229 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 194 insertions(+), 35 deletions(-) diff --git a/ext/date/date_core.c b/ext/date/date_core.c index 6bcf272b62d8b0..e19248717b2d46 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -7,6 +7,7 @@ #include "ruby/util.h" #include #include +#include /* For uint64_t in Neri-Schneider algorithm */ #if defined(HAVE_SYS_TIME_H) #include #endif @@ -452,11 +453,27 @@ do {\ static int c_valid_civil_p(int, int, int, double, int *, int *, int *, int *); +/* Forward declarations for Neri-Schneider optimized functions */ +static int c_gregorian_civil_to_jd(int y, int m, int d); +static void c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd); +inline static int c_gregorian_fdoy(int y); +inline static int c_gregorian_ldoy(int y); +inline static int c_gregorian_ldom_jd(int y, int m); +inline static int ns_jd_in_range(int jd); + static int c_find_fdoy(int y, double sg, int *rjd, int *ns) { int d, rm, rd; + /* Fast path: pure Gregorian calendar */ + if (isinf(sg) && sg < 0) { + *rjd = c_gregorian_fdoy(y); + *ns = 1; + return 1; + } + + /* Keep existing loop for Julian/reform period */ for (d = 1; d < 31; d++) if (c_valid_civil_p(y, 1, d, sg, &rm, &rd, rjd, ns)) return 1; @@ -468,6 +485,14 @@ c_find_ldoy(int y, double sg, int *rjd, int *ns) { int i, rm, rd; + /* Fast path: pure Gregorian calendar */ + if (isinf(sg) && sg < 0) { + *rjd = c_gregorian_ldoy(y); + *ns = 1; + return 1; + } + + /* Keep existing loop for Julian/reform period */ for (i = 0; i < 30; i++) if (c_valid_civil_p(y, 12, 31 - i, sg, &rm, &rd, rjd, ns)) return 1; @@ -493,6 +518,14 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns) { int i, rm, rd; + /* Fast path: pure Gregorian calendar */ + if (isinf(sg) && sg < 0) { + *rjd = c_gregorian_ldom_jd(y, m); + *ns = 1; + return 1; + } + + /* Keep existing loop for Julian/reform period */ for (i = 0; i < 30; i++) if (c_valid_civil_p(y, m, 31 - i, sg, &rm, &rd, rjd, ns)) return 1; @@ -502,55 +535,74 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns) static void c_civil_to_jd(int y, int m, int d, double sg, int *rjd, int *ns) { - double a, b, jd; + int jd; - if (m <= 2) { - y -= 1; - m += 12; + /* Fast path: pure Gregorian calendar (sg == -infinity) */ + if (isinf(sg) && sg < 0) { + *rjd = c_gregorian_civil_to_jd(y, m, d); + *ns = 1; + return; } - a = floor(y / 100.0); - b = 2 - a + floor(a / 4.0); - jd = floor(365.25 * (y + 4716)) + - floor(30.6001 * (m + 1)) + - d + b - 1524; + + /* Calculate Gregorian JD using optimized algorithm */ + jd = c_gregorian_civil_to_jd(y, m, d); + if (jd < sg) { - jd -= b; + /* Before Gregorian switchover - use Julian calendar */ + int y2 = y, m2 = m; + if (m2 <= 2) { + y2 -= 1; + m2 += 12; + } + jd = (int)(floor(365.25 * (y2 + 4716)) + + floor(30.6001 * (m2 + 1)) + + d - 1524); *ns = 0; } - else + else { *ns = 1; + } - *rjd = (int)jd; + *rjd = jd; } static void c_jd_to_civil(int jd, double sg, int *ry, int *rm, int *rdom) { - double x, a, b, c, d, e, y, m, dom; - - if (jd < sg) - a = jd; - else { - x = floor((jd - 1867216.25) / 36524.25); - a = jd + 1 + x - floor(x / 4.0); - } - b = a + 1524; - c = floor((b - 122.1) / 365.25); - d = floor(365.25 * c); - e = floor((b - d) / 30.6001); - dom = b - d - floor(30.6001 * e); - if (e <= 13) { - m = e - 1; - y = c - 4716; - } - else { - m = e - 13; - y = c - 4715; + /* Fast path: pure Gregorian or date after switchover, within safe range */ + if (((isinf(sg) && sg < 0) || jd >= sg) && ns_jd_in_range(jd)) { + c_gregorian_jd_to_civil(jd, ry, rm, rdom); + return; } - *ry = (int)y; - *rm = (int)m; - *rdom = (int)dom; + /* Original algorithm for Julian calendar or extreme dates */ + { + double x, a, b, c, d, e, y, m, dom; + + if (jd < sg) + a = jd; + else { + x = floor((jd - 1867216.25) / 36524.25); + a = jd + 1 + x - floor(x / 4.0); + } + b = a + 1524; + c = floor((b - 122.1) / 365.25); + d = floor(365.25 * c); + e = floor((b - d) / 30.6001); + dom = b - d - floor(30.6001 * e); + if (e <= 13) { + m = e - 1; + y = c - 4716; + } + else { + m = e - 13; + y = c - 4715; + } + + *ry = (int)y; + *rm = (int)m; + *rdom = (int)dom; + } } static void @@ -725,6 +777,113 @@ c_gregorian_last_day_of_month(int y, int m) return monthtab[c_gregorian_leap_p(y) ? 1 : 0][m]; } +/* + * Neri-Schneider algorithm for optimized Gregorian date conversion. + * Reference: Neri & Schneider, "Euclidean Affine Functions and Applications + * to Calendar Algorithms", Software: Practice and Experience, 2023. + * https://arxiv.org/abs/2102.06959 + * + * This algorithm provides ~2-3x speedup over traditional floating-point + * implementations by using pure integer arithmetic with multiplication + * and bit-shifts instead of expensive division operations. + */ + +/* JDN of March 1, Year 0 in proleptic Gregorian calendar */ +#define NS_GREGORIAN_EPOCH 1721120 + +/* + * Safe bounds for Neri-Schneider algorithm to avoid integer overflow. + * These correspond to approximately years -1,000,000 to +1,000,000. + */ +#define NS_JD_MIN -364000000 +#define NS_JD_MAX 538000000 + +inline static int +ns_jd_in_range(int jd) +{ + return jd >= NS_JD_MIN && jd <= NS_JD_MAX; +} + +/* Optimized: Gregorian date -> Julian Day Number */ +static int +c_gregorian_civil_to_jd(int y, int m, int d) +{ + /* Shift epoch to March 1 of year 0 (Jan/Feb belong to previous year) */ + int j = (m < 3) ? 1 : 0; + int y0 = y - j; + int m0 = j ? m + 12 : m; + int d0 = d - 1; + + /* Calculate year contribution with leap year correction */ + int q1 = DIV(y0, 100); + int yc = DIV(1461 * y0, 4) - q1 + DIV(q1, 4); + + /* Calculate month contribution using integer arithmetic */ + int mc = (979 * m0 - 2919) / 32; + + /* Combine and add epoch offset to get JDN */ + return yc + mc + d0 + NS_GREGORIAN_EPOCH; +} + +/* Optimized: Julian Day Number -> Gregorian date */ +static void +c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd) +{ + int r0, n1, q1, r1, n2, q2, r2, n3, q3, r3, y0, j; + uint64_t u2; + + /* Convert JDN to rata die (March 1, Year 0 epoch) */ + r0 = jd - NS_GREGORIAN_EPOCH; + + /* Extract century and day within 400-year cycle */ + /* Use Euclidean (floor) division for negative values */ + n1 = 4 * r0 + 3; + q1 = DIV(n1, 146097); /* Century */ + r1 = MOD(n1, 146097) / 4; /* Day within 400-year cycle */ + + /* Use 64-bit arithmetic for year calculation within century */ + n2 = 4 * r1 + 3; + u2 = (uint64_t)2939745 * (uint64_t)n2; + q2 = (int)(u2 >> 32); /* Year within century */ + r2 = (int)((uint32_t)u2 / 2939745 / 4); /* Day of year */ + + /* Calculate month and day using integer arithmetic */ + n3 = 2141 * r2 + 197913; + q3 = n3 >> 16; /* Month (3-14) */ + r3 = (n3 & 0xFFFF) / 2141; /* Day of month (0-based) */ + + /* Combine century and year */ + y0 = 100 * q1 + q2; + + /* Adjust for January/February (shift from fiscal year) */ + j = (r2 >= 306) ? 1 : 0; + + *ry = y0 + j; + *rm = j ? q3 - 12 : q3; + *rd = r3 + 1; +} + +/* O(1) first day of year for Gregorian calendar */ +inline static int +c_gregorian_fdoy(int y) +{ + return c_gregorian_civil_to_jd(y, 1, 1); +} + +/* O(1) last day of year for Gregorian calendar */ +inline static int +c_gregorian_ldoy(int y) +{ + return c_gregorian_civil_to_jd(y, 12, 31); +} + +/* O(1) last day of month (JDN) for Gregorian calendar */ +inline static int +c_gregorian_ldom_jd(int y, int m) +{ + return c_gregorian_civil_to_jd(y, m, c_gregorian_last_day_of_month(y, m)); +} + static int c_valid_julian_p(int y, int m, int d, int *rm, int *rd) { From ea03f263b51cfae3163e19dad5800c3a54d7cd1c Mon Sep 17 00:00:00 2001 From: Maciej Mensfeld Date: Thu, 18 Dec 2025 10:38:51 +0100 Subject: [PATCH 06/31] [ruby/date] improve styling https://github.com/ruby/date/commit/cd7a329dfd --- ext/date/date_core.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/date/date_core.c b/ext/date/date_core.c index e19248717b2d46..e055db9fa667db 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -456,10 +456,10 @@ static int c_valid_civil_p(int, int, int, double, /* Forward declarations for Neri-Schneider optimized functions */ static int c_gregorian_civil_to_jd(int y, int m, int d); static void c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd); -inline static int c_gregorian_fdoy(int y); -inline static int c_gregorian_ldoy(int y); -inline static int c_gregorian_ldom_jd(int y, int m); -inline static int ns_jd_in_range(int jd); +static int c_gregorian_fdoy(int y); +static int c_gregorian_ldoy(int y); +static int c_gregorian_ldom_jd(int y, int m); +static int ns_jd_in_range(int jd); static int c_find_fdoy(int y, double sg, int *rjd, int *ns) @@ -809,7 +809,7 @@ static int c_gregorian_civil_to_jd(int y, int m, int d) { /* Shift epoch to March 1 of year 0 (Jan/Feb belong to previous year) */ - int j = (m < 3) ? 1 : 0; + int j = (m < 3) ? 1 : 0; int y0 = y - j; int m0 = j ? m + 12 : m; int d0 = d - 1; From bcaa127ecd4f408299c8514569329df1b124f556 Mon Sep 17 00:00:00 2001 From: Maciej Mensfeld Date: Thu, 18 Dec 2025 12:36:36 +0100 Subject: [PATCH 07/31] [ruby/date] code remarks, macros and r2.6 support https://github.com/ruby/date/commit/2682dc79c0 --- ext/date/date_core.c | 94 ++++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/ext/date/date_core.c b/ext/date/date_core.c index e055db9fa667db..23fe03c73b5a5e 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -7,7 +7,6 @@ #include "ruby/util.h" #include #include -#include /* For uint64_t in Neri-Schneider algorithm */ #if defined(HAVE_SYS_TIME_H) #include #endif @@ -453,6 +452,9 @@ do {\ static int c_valid_civil_p(int, int, int, double, int *, int *, int *, int *); +/* Check if using pure Gregorian calendar (sg == -Infinity) */ +#define c_gregorian_only_p(sg) (isinf(sg) && (sg) < 0) + /* Forward declarations for Neri-Schneider optimized functions */ static int c_gregorian_civil_to_jd(int y, int m, int d); static void c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd); @@ -467,7 +469,7 @@ c_find_fdoy(int y, double sg, int *rjd, int *ns) int d, rm, rd; /* Fast path: pure Gregorian calendar */ - if (isinf(sg) && sg < 0) { + if (c_gregorian_only_p(sg)) { *rjd = c_gregorian_fdoy(y); *ns = 1; return 1; @@ -486,7 +488,7 @@ c_find_ldoy(int y, double sg, int *rjd, int *ns) int i, rm, rd; /* Fast path: pure Gregorian calendar */ - if (isinf(sg) && sg < 0) { + if (c_gregorian_only_p(sg)) { *rjd = c_gregorian_ldoy(y); *ns = 1; return 1; @@ -519,7 +521,7 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns) int i, rm, rd; /* Fast path: pure Gregorian calendar */ - if (isinf(sg) && sg < 0) { + if (c_gregorian_only_p(sg)) { *rjd = c_gregorian_ldom_jd(y, m); *ns = 1; return 1; @@ -537,8 +539,8 @@ c_civil_to_jd(int y, int m, int d, double sg, int *rjd, int *ns) { int jd; - /* Fast path: pure Gregorian calendar (sg == -infinity) */ - if (isinf(sg) && sg < 0) { + /* Fast path: pure Gregorian calendar */ + if (c_gregorian_only_p(sg)) { *rjd = c_gregorian_civil_to_jd(y, m, d); *ns = 1; return; @@ -570,7 +572,7 @@ static void c_jd_to_civil(int jd, double sg, int *ry, int *rm, int *rdom) { /* Fast path: pure Gregorian or date after switchover, within safe range */ - if (((isinf(sg) && sg < 0) || jd >= sg) && ns_jd_in_range(jd)) { + if ((c_gregorian_only_p(sg) || jd >= sg) && ns_jd_in_range(jd)) { c_gregorian_jd_to_civil(jd, ry, rm, rdom); return; } @@ -789,7 +791,40 @@ c_gregorian_last_day_of_month(int y, int m) */ /* JDN of March 1, Year 0 in proleptic Gregorian calendar */ -#define NS_GREGORIAN_EPOCH 1721120 +#define NS_EPOCH 1721120 + +/* Days in a 4-year cycle (3 normal years + 1 leap year) */ +#define NS_DAYS_IN_4_YEARS 1461 + +/* Days in a 400-year Gregorian cycle (97 leap years in 400 years) */ +#define NS_DAYS_IN_400_YEARS 146097 + +/* Years per century */ +#define NS_YEARS_PER_CENTURY 100 + +/* + * Multiplier for extracting year within century using fixed-point arithmetic. + * This is ceil(2^32 / NS_DAYS_IN_4_YEARS) for the Euclidean affine function. + */ +#define NS_YEAR_MULTIPLIER 2939745 + +/* + * Coefficients for month calculation from day-of-year. + * Maps day-of-year to month using: month = (NS_MONTH_COEFF * doy + NS_MONTH_OFFSET) >> 16 + */ +#define NS_MONTH_COEFF 2141 +#define NS_MONTH_OFFSET 197913 + +/* + * Coefficients for civil date to JDN month contribution. + * Maps month to accumulated days: days = (NS_CIVIL_MONTH_COEFF * m - NS_CIVIL_MONTH_OFFSET) / 32 + */ +#define NS_CIVIL_MONTH_COEFF 979 +#define NS_CIVIL_MONTH_OFFSET 2919 +#define NS_CIVIL_MONTH_DIVISOR 32 + +/* Days from March 1 to December 31 (for Jan/Feb year adjustment) */ +#define NS_DAYS_BEFORE_NEW_YEAR 306 /* * Safe bounds for Neri-Schneider algorithm to avoid integer overflow. @@ -815,14 +850,14 @@ c_gregorian_civil_to_jd(int y, int m, int d) int d0 = d - 1; /* Calculate year contribution with leap year correction */ - int q1 = DIV(y0, 100); - int yc = DIV(1461 * y0, 4) - q1 + DIV(q1, 4); + int q1 = DIV(y0, NS_YEARS_PER_CENTURY); + int yc = DIV(NS_DAYS_IN_4_YEARS * y0, 4) - q1 + DIV(q1, 4); /* Calculate month contribution using integer arithmetic */ - int mc = (979 * m0 - 2919) / 32; + int mc = (NS_CIVIL_MONTH_COEFF * m0 - NS_CIVIL_MONTH_OFFSET) / NS_CIVIL_MONTH_DIVISOR; /* Combine and add epoch offset to get JDN */ - return yc + mc + d0 + NS_GREGORIAN_EPOCH; + return yc + mc + d0 + NS_EPOCH; } /* Optimized: Julian Day Number -> Gregorian date */ @@ -830,33 +865,42 @@ static void c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd) { int r0, n1, q1, r1, n2, q2, r2, n3, q3, r3, y0, j; - uint64_t u2; +#ifdef HAVE_LONG_LONG + unsigned LONG_LONG u2; +#endif /* Convert JDN to rata die (March 1, Year 0 epoch) */ - r0 = jd - NS_GREGORIAN_EPOCH; + r0 = jd - NS_EPOCH; /* Extract century and day within 400-year cycle */ /* Use Euclidean (floor) division for negative values */ n1 = 4 * r0 + 3; - q1 = DIV(n1, 146097); /* Century */ - r1 = MOD(n1, 146097) / 4; /* Day within 400-year cycle */ + q1 = DIV(n1, NS_DAYS_IN_400_YEARS); + r1 = MOD(n1, NS_DAYS_IN_400_YEARS) / 4; - /* Use 64-bit arithmetic for year calculation within century */ + /* Calculate year within century and day of year */ n2 = 4 * r1 + 3; - u2 = (uint64_t)2939745 * (uint64_t)n2; - q2 = (int)(u2 >> 32); /* Year within century */ - r2 = (int)((uint32_t)u2 / 2939745 / 4); /* Day of year */ +#ifdef HAVE_LONG_LONG + /* Use 64-bit arithmetic to avoid overflow */ + u2 = (unsigned LONG_LONG)NS_YEAR_MULTIPLIER * (unsigned LONG_LONG)n2; + q2 = (int)(u2 >> 32); + r2 = (int)((unsigned int)u2 / NS_YEAR_MULTIPLIER / 4); +#else + /* Fallback for systems without 64-bit integers */ + q2 = n2 / NS_DAYS_IN_4_YEARS; + r2 = (n2 % NS_DAYS_IN_4_YEARS) / 4; +#endif /* Calculate month and day using integer arithmetic */ - n3 = 2141 * r2 + 197913; - q3 = n3 >> 16; /* Month (3-14) */ - r3 = (n3 & 0xFFFF) / 2141; /* Day of month (0-based) */ + n3 = NS_MONTH_COEFF * r2 + NS_MONTH_OFFSET; + q3 = n3 >> 16; + r3 = (n3 & 0xFFFF) / NS_MONTH_COEFF; /* Combine century and year */ - y0 = 100 * q1 + q2; + y0 = NS_YEARS_PER_CENTURY * q1 + q2; /* Adjust for January/February (shift from fiscal year) */ - j = (r2 >= 306) ? 1 : 0; + j = (r2 >= NS_DAYS_BEFORE_NEW_YEAR) ? 1 : 0; *ry = y0 + j; *rm = j ? q3 - 12 : q3; From 5960fb9fa000898b863565a8f48b81fd25bff88a Mon Sep 17 00:00:00 2001 From: Maciej Mensfeld Date: Thu, 18 Dec 2025 12:43:22 +0100 Subject: [PATCH 08/31] [ruby/date] remove redundant code https://github.com/ruby/date/commit/5e6a458179 --- ext/date/date_core.c | 46 +++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/ext/date/date_core.c b/ext/date/date_core.c index 23fe03c73b5a5e..061f4d80d001b9 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -455,6 +455,24 @@ static int c_valid_civil_p(int, int, int, double, /* Check if using pure Gregorian calendar (sg == -Infinity) */ #define c_gregorian_only_p(sg) (isinf(sg) && (sg) < 0) +/* + * Fast path macros for pure Gregorian calendar. + * Sets *rjd to the JD value, *ns to 1 (new style), and returns. + */ +#define GREGORIAN_JD_FAST_PATH_RET(sg, jd_expr, rjd, ns) \ + if (c_gregorian_only_p(sg)) { \ + *(rjd) = (jd_expr); \ + *(ns) = 1; \ + return 1; \ + } + +#define GREGORIAN_JD_FAST_PATH(sg, jd_expr, rjd, ns) \ + if (c_gregorian_only_p(sg)) { \ + *(rjd) = (jd_expr); \ + *(ns) = 1; \ + return; \ + } + /* Forward declarations for Neri-Schneider optimized functions */ static int c_gregorian_civil_to_jd(int y, int m, int d); static void c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd); @@ -468,12 +486,7 @@ c_find_fdoy(int y, double sg, int *rjd, int *ns) { int d, rm, rd; - /* Fast path: pure Gregorian calendar */ - if (c_gregorian_only_p(sg)) { - *rjd = c_gregorian_fdoy(y); - *ns = 1; - return 1; - } + GREGORIAN_JD_FAST_PATH_RET(sg, c_gregorian_fdoy(y), rjd, ns); /* Keep existing loop for Julian/reform period */ for (d = 1; d < 31; d++) @@ -487,12 +500,7 @@ c_find_ldoy(int y, double sg, int *rjd, int *ns) { int i, rm, rd; - /* Fast path: pure Gregorian calendar */ - if (c_gregorian_only_p(sg)) { - *rjd = c_gregorian_ldoy(y); - *ns = 1; - return 1; - } + GREGORIAN_JD_FAST_PATH_RET(sg, c_gregorian_ldoy(y), rjd, ns); /* Keep existing loop for Julian/reform period */ for (i = 0; i < 30; i++) @@ -520,12 +528,7 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns) { int i, rm, rd; - /* Fast path: pure Gregorian calendar */ - if (c_gregorian_only_p(sg)) { - *rjd = c_gregorian_ldom_jd(y, m); - *ns = 1; - return 1; - } + GREGORIAN_JD_FAST_PATH_RET(sg, c_gregorian_ldom_jd(y, m), rjd, ns); /* Keep existing loop for Julian/reform period */ for (i = 0; i < 30; i++) @@ -539,12 +542,7 @@ c_civil_to_jd(int y, int m, int d, double sg, int *rjd, int *ns) { int jd; - /* Fast path: pure Gregorian calendar */ - if (c_gregorian_only_p(sg)) { - *rjd = c_gregorian_civil_to_jd(y, m, d); - *ns = 1; - return; - } + GREGORIAN_JD_FAST_PATH(sg, c_gregorian_civil_to_jd(y, m, d), rjd, ns); /* Calculate Gregorian JD using optimized algorithm */ jd = c_gregorian_civil_to_jd(y, m, d); From 8024245854ac9e92947e7bd4a58223d8998d3893 Mon Sep 17 00:00:00 2001 From: Maciej Mensfeld Date: Thu, 18 Dec 2025 14:03:00 +0100 Subject: [PATCH 09/31] [ruby/date] remove conditional for uint64_t https://github.com/ruby/date/commit/47778c32d8 --- ext/date/date_core.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/ext/date/date_core.c b/ext/date/date_core.c index 061f4d80d001b9..9755fb819eaaa9 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -863,9 +863,7 @@ static void c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd) { int r0, n1, q1, r1, n2, q2, r2, n3, q3, r3, y0, j; -#ifdef HAVE_LONG_LONG - unsigned LONG_LONG u2; -#endif + uint64_t u2; /* Convert JDN to rata die (March 1, Year 0 epoch) */ r0 = jd - NS_EPOCH; @@ -878,16 +876,10 @@ c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd) /* Calculate year within century and day of year */ n2 = 4 * r1 + 3; -#ifdef HAVE_LONG_LONG /* Use 64-bit arithmetic to avoid overflow */ - u2 = (unsigned LONG_LONG)NS_YEAR_MULTIPLIER * (unsigned LONG_LONG)n2; + u2 = (uint64_t)NS_YEAR_MULTIPLIER * (uint64_t)n2; q2 = (int)(u2 >> 32); - r2 = (int)((unsigned int)u2 / NS_YEAR_MULTIPLIER / 4); -#else - /* Fallback for systems without 64-bit integers */ - q2 = n2 / NS_DAYS_IN_4_YEARS; - r2 = (n2 % NS_DAYS_IN_4_YEARS) / 4; -#endif + r2 = (int)((uint32_t)u2 / NS_YEAR_MULTIPLIER / 4); /* Calculate month and day using integer arithmetic */ n3 = NS_MONTH_COEFF * r2 + NS_MONTH_OFFSET; From c5376a3a167cbb90023e7610a4fafda22a5c381c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 5 Dec 2025 11:11:31 +0900 Subject: [PATCH 10/31] [ruby/rubygems] Remove deprecated, unused Gem::List https://github.com/ruby/rubygems/commit/43371085f4 --- lib/rubygems/resolver.rb | 1 - lib/rubygems/specification.rb | 1 - lib/rubygems/util/list.rb | 40 ----------------------------------- 3 files changed, 42 deletions(-) delete mode 100644 lib/rubygems/util/list.rb diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index ed4cbde3bab12c..bc4fef893ead65 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -2,7 +2,6 @@ require_relative "dependency" require_relative "exceptions" -require_relative "util/list" ## # Given a set of Gem::Dependency objects as +needed+ and a way to query the diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index a9ec6aa3a3c84c..3d1f2dad910485 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -11,7 +11,6 @@ require_relative "stub_specification" require_relative "platform" require_relative "specification_record" -require_relative "util/list" require "rbconfig" diff --git a/lib/rubygems/util/list.rb b/lib/rubygems/util/list.rb deleted file mode 100644 index 2899e8a2b9ed70..00000000000000 --- a/lib/rubygems/util/list.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module Gem - # The Gem::List class is currently unused and will be removed in the next major rubygems version - class List # :nodoc: - include Enumerable - attr_accessor :value, :tail - - def initialize(value = nil, tail = nil) - @value = value - @tail = tail - end - - def each - n = self - while n - yield n.value - n = n.tail - end - end - - def to_a - super.reverse - end - - def prepend(value) - List.new value, self - end - - def pretty_print(q) # :nodoc: - q.pp to_a - end - - def self.prepend(list, value) - return List.new(value) unless list - List.new value, list - end - end - deprecate_constant :List -end From bdbe8d50150447748eaa92a0cce7327d8dec9903 Mon Sep 17 00:00:00 2001 From: eileencodes Date: Tue, 25 Nov 2025 14:17:03 -0500 Subject: [PATCH 11/31] [ruby/rubygems] Write gem files atomically This change updates `write_binary` to use a new class, `AtomicFileWriter.open` to write the gem's files. This implementation is borrowed from Active Support's [`atomic_write`](https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb). Atomic write will write the files to a temporary file and then once created, sets permissions and renames the file. If the file is corrupted - ie on failed download, an error occurs, or for some other reason, the real file will not be created. The changes made here make `verify_gz` obsolete, we don't need to verify it if we have successfully created the file atomically. If it exists, it is not corrupt. If it is corrupt, the file won't exist on disk. While writing tests for this functionality I replaced the `RemoteFetcher` stub with `FakeFetcher` except for where we really do need to overwrite the `RemoteFetcher`. The new test implementation is much clearer on what it's trying to accomplish versus the prior test implementation. https://github.com/ruby/rubygems/commit/0cd4b54291 --- lib/bundler/shared_helpers.rb | 3 +- lib/rubygems.rb | 11 +- lib/rubygems/util/atomic_file_writer.rb | 67 ++++++++++++ test/rubygems/test_gem_installer.rb | 34 +++--- test/rubygems/test_gem_remote_fetcher.rb | 134 +++++++++++------------ 5 files changed, 158 insertions(+), 91 deletions(-) create mode 100644 lib/rubygems/util/atomic_file_writer.rb diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 6419e4299760b7..2aa8abe0a078b0 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -105,7 +105,8 @@ def set_bundle_environment def filesystem_access(path, action = :write, &block) yield(path.dup) rescue Errno::EACCES => e - raise unless e.message.include?(path.to_s) || action == :create + path_basename = File.basename(path.to_s) + raise unless e.message.include?(path_basename) || action == :create raise PermissionError.new(path, action) rescue Errno::EAGAIN diff --git a/lib/rubygems.rb b/lib/rubygems.rb index e99176fec0e107..41b39a808b637f 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -17,6 +17,7 @@ module Gem require_relative "rubygems/errors" require_relative "rubygems/target_rbconfig" require_relative "rubygems/win_platform" +require_relative "rubygems/util/atomic_file_writer" ## # RubyGems is the Ruby standard for publishing and managing third party @@ -833,14 +834,12 @@ def self.read_binary(path) end ## - # Safely write a file in binary mode on all platforms. + # Atomically write a file in binary mode on all platforms. def self.write_binary(path, data) - File.binwrite(path, data) - rescue Errno::ENOSPC - # If we ran out of space but the file exists, it's *guaranteed* to be corrupted. - File.delete(path) if File.exist?(path) - raise + Gem::AtomicFileWriter.open(path) do |file| + file.write(data) + end end ## diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb new file mode 100644 index 00000000000000..7d1d6a74168f95 --- /dev/null +++ b/lib/rubygems/util/atomic_file_writer.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# Based on ActiveSupport's AtomicFile implementation +# Copyright (c) David Heinemeier Hansson +# https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb +# Licensed under the MIT License + +module Gem + class AtomicFileWriter + ## + # Write to a file atomically. Useful for situations where you don't + # want other processes or threads to see half-written files. + + def self.open(file_name) + temp_dir = File.dirname(file_name) + require "tempfile" unless defined?(Tempfile) + + Tempfile.create(".#{File.basename(file_name)}", temp_dir) do |temp_file| + temp_file.binmode + return_value = yield temp_file + temp_file.close + + original_permissions = if File.exist?(file_name) + File.stat(file_name) + else + # If not possible, probe which are the default permissions in the + # destination directory. + probe_permissions_in(File.dirname(file_name)) + end + + # Set correct permissions on new file + if original_permissions + begin + File.chown(original_permissions.uid, original_permissions.gid, temp_file.path) + File.chmod(original_permissions.mode, temp_file.path) + rescue Errno::EPERM, Errno::EACCES + # Changing file ownership failed, moving on. + end + end + + # Overwrite original file with temp file + File.rename(temp_file.path, file_name) + return_value + end + end + + def self.probe_permissions_in(dir) # :nodoc: + basename = [ + ".permissions_check", + Thread.current.object_id, + Process.pid, + rand(1_000_000), + ].join(".") + + file_name = File.join(dir, basename) + File.open(file_name, "w") {} + File.stat(file_name) + rescue Errno::ENOENT + nil + ensure + begin + File.unlink(file_name) if File.exist?(file_name) + rescue SystemCallError + end + end + end +end diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 293fe1e823dd1a..0220a41f88a4f2 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2385,25 +2385,31 @@ def test_leaves_no_empty_cached_spec_when_no_more_disk_space installer = Gem::Installer.for_spec @spec installer.gem_home = @gemhome - File.singleton_class.class_eval do - alias_method :original_binwrite, :binwrite - - def binwrite(path, data) + assert_raise(Errno::ENOSPC) do + Gem::AtomicFileWriter.open(@spec.spec_file) do raise Errno::ENOSPC end end - assert_raise Errno::ENOSPC do - installer.write_spec - end - assert_path_not_exist @spec.spec_file - ensure - File.singleton_class.class_eval do - remove_method :binwrite - alias_method :binwrite, :original_binwrite - remove_method :original_binwrite - end + end + + def test_write_default_spec + @spec = setup_base_spec + @spec.files = %w[a.rb b.rb c.rb] + + installer = Gem::Installer.for_spec @spec + installer.gem_home = @gemhome + + installer.write_default_spec + + assert_path_exist installer.default_spec_file + + loaded = Gem::Specification.load installer.default_spec_file + + assert_equal @spec.files, loaded.files + assert_equal @spec.name, loaded.name + assert_equal @spec.version, loaded.version end def test_dir diff --git a/test/rubygems/test_gem_remote_fetcher.rb b/test/rubygems/test_gem_remote_fetcher.rb index 5c1d89fad6934a..9badd75b427169 100644 --- a/test/rubygems/test_gem_remote_fetcher.rb +++ b/test/rubygems/test_gem_remote_fetcher.rb @@ -60,7 +60,7 @@ def test_cache_update_path uri = Gem::URI "http://example/file" path = File.join @tempdir, "file" - fetcher = util_fuck_with_fetcher "hello" + fetcher = fake_fetcher(uri.to_s, "hello") data = fetcher.cache_update_path uri, path @@ -75,7 +75,7 @@ def test_cache_update_path_with_utf8_internal_encoding path = File.join @tempdir, "file" data = String.new("\xC8").force_encoding(Encoding::BINARY) - fetcher = util_fuck_with_fetcher data + fetcher = fake_fetcher(uri.to_s, data) written_data = fetcher.cache_update_path uri, path @@ -88,7 +88,7 @@ def test_cache_update_path_no_update uri = Gem::URI "http://example/file" path = File.join @tempdir, "file" - fetcher = util_fuck_with_fetcher "hello" + fetcher = fake_fetcher(uri.to_s, "hello") data = fetcher.cache_update_path uri, path, false @@ -97,103 +97,79 @@ def test_cache_update_path_no_update assert_path_not_exist path end - def util_fuck_with_fetcher(data, blow = false) - fetcher = Gem::RemoteFetcher.fetcher - fetcher.instance_variable_set :@test_data, data - - if blow - def fetcher.fetch_path(arg, *rest) - # OMG I'm such an ass - class << self; remove_method :fetch_path; end - def self.fetch_path(arg, *rest) - @test_arg = arg - @test_data - end + def test_cache_update_path_overwrites_existing_file + uri = Gem::URI "http://example/file" + path = File.join @tempdir, "file" - raise Gem::RemoteFetcher::FetchError.new("haha!", "") - end - else - def fetcher.fetch_path(arg, *rest) - @test_arg = arg - @test_data - end - end + # Create existing file with old content + File.write(path, "old content") + assert_equal "old content", File.read(path) + + fetcher = fake_fetcher(uri.to_s, "new content") - fetcher + data = fetcher.cache_update_path uri, path + + assert_equal "new content", data + assert_equal "new content", File.read(path) end def test_download - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + fetcher = fake_fetcher(a1_url, a1_data) a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://gems.example.com") - assert_equal("http://gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end def test_download_with_auth - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://user:password@gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + fetcher = fake_fetcher(a1_url, a1_data) a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://user:password@gems.example.com") - assert_equal("http://user:password@gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end def test_download_with_token - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://token@gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + fetcher = fake_fetcher(a1_url, a1_data) a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://token@gems.example.com") - assert_equal("http://token@gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end def test_download_with_x_oauth_basic - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://token:x-oauth-basic@gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + fetcher = fake_fetcher(a1_url, a1_data) a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://token:x-oauth-basic@gems.example.com") - assert_equal("http://token:x-oauth-basic@gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end def test_download_with_encoded_auth - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://user:%25pas%25sword@gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + fetcher = fake_fetcher(a1_url, a1_data) a1_cache_gem = @a1.cache_file assert_equal a1_cache_gem, fetcher.download(@a1, "http://user:%25pas%25sword@gems.example.com") - assert_equal("http://user:%25pas%25sword@gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end @@ -235,8 +211,9 @@ def test_download_local_space def test_download_install_dir a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + fetcher = fake_fetcher(a1_url, a1_data) install_dir = File.join @tempdir, "more_gems" @@ -245,8 +222,7 @@ def test_download_install_dir actual = fetcher.download(@a1, "http://gems.example.com", install_dir) assert_equal a1_cache_gem, actual - assert_equal("http://gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end @@ -282,7 +258,12 @@ def test_download_read_only FileUtils.chmod 0o555, @a1.cache_dir FileUtils.chmod 0o555, @gemhome - fetcher = util_fuck_with_fetcher File.read(@a1_gem) + fetcher = Gem::RemoteFetcher.fetcher + def fetcher.fetch_path(uri, *rest) + File.read File.join(@test_gem_dir, "a-1.gem") + end + fetcher.instance_variable_set(:@test_gem_dir, File.dirname(@a1_gem)) + fetcher.download(@a1, "http://gems.example.com") a1_cache_gem = File.join Gem.user_dir, "cache", @a1.file_name assert File.exist? a1_cache_gem @@ -301,19 +282,21 @@ def test_download_platform_legacy end e1.loaded_from = File.join(@gemhome, "specifications", e1.full_name) - e1_data = nil - File.open e1_gem, "rb" do |fp| - e1_data = fp.read - end + e1_data = File.open e1_gem, "rb", &:read - fetcher = util_fuck_with_fetcher e1_data, :blow_chunks + fetcher = Gem::RemoteFetcher.fetcher + def fetcher.fetch_path(uri, *rest) + @call_count ||= 0 + @call_count += 1 + raise Gem::RemoteFetcher::FetchError.new("error", uri) if @call_count == 1 + @test_data + end + fetcher.instance_variable_set(:@test_data, e1_data) e1_cache_gem = e1.cache_file assert_equal e1_cache_gem, fetcher.download(e1, "http://gems.example.com") - assert_equal("http://gems.example.com/gems/#{e1.original_name}.gem", - fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(e1_cache_gem) end @@ -592,6 +575,8 @@ def test_yaml_error_on_size end end + private + def assert_error(exception_class = Exception) got_exception = false @@ -603,4 +588,13 @@ def assert_error(exception_class = Exception) assert got_exception, "Expected exception conforming to #{exception_class}" end + + def fake_fetcher(url, data) + original_fetcher = Gem::RemoteFetcher.fetcher + fetcher = Gem::FakeFetcher.new + fetcher.data[url] = data + Gem::RemoteFetcher.fetcher = fetcher + ensure + Gem::RemoteFetcher.fetcher = original_fetcher + end end From 74becf1b61272c66e835c446525920eae0b8574a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 26 Dec 2025 09:32:02 +0900 Subject: [PATCH 12/31] Start to develop 4.1.0.dev --- lib/bundler/version.rb | 2 +- lib/rubygems.rb | 2 +- spec/bundler/realworld/fixtures/tapioca/Gemfile.lock | 2 +- spec/bundler/realworld/fixtures/warbler/Gemfile.lock | 2 +- tool/bundler/dev_gems.rb.lock | 2 +- tool/bundler/rubocop_gems.rb.lock | 2 +- tool/bundler/standard_gems.rb.lock | 2 +- tool/bundler/test_gems.rb.lock | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb index 732b4a05631e84..ca7bb0719aac24 100644 --- a/lib/bundler/version.rb +++ b/lib/bundler/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false module Bundler - VERSION = "4.0.3".freeze + VERSION = "4.1.0.dev".freeze def self.bundler_major_version @bundler_major_version ||= gem_version.segments.first diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 41b39a808b637f..b52dd1b9d3e99a 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -9,7 +9,7 @@ require "rbconfig" module Gem - VERSION = "4.0.3" + VERSION = "4.1.0.dev" end require_relative "rubygems/defaults" diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock index 2db720a69f4632..4ce06de722cfaa 100644 --- a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock +++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock @@ -46,4 +46,4 @@ DEPENDENCIES tapioca BUNDLED WITH - 4.0.3 + 4.1.0.dev diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock index c37fbbb7eed556..2f2deea99408cc 100644 --- a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock +++ b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock @@ -36,4 +36,4 @@ DEPENDENCIES warbler! BUNDLED WITH - 4.0.3 + 4.1.0.dev diff --git a/tool/bundler/dev_gems.rb.lock b/tool/bundler/dev_gems.rb.lock index fff9cfe70cb104..72abb0efcbe1f6 100644 --- a/tool/bundler/dev_gems.rb.lock +++ b/tool/bundler/dev_gems.rb.lock @@ -129,4 +129,4 @@ CHECKSUMS turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f BUNDLED WITH - 4.0.3 + 4.1.0.dev diff --git a/tool/bundler/rubocop_gems.rb.lock b/tool/bundler/rubocop_gems.rb.lock index 70704bfc38896c..251abc37dd0f23 100644 --- a/tool/bundler/rubocop_gems.rb.lock +++ b/tool/bundler/rubocop_gems.rb.lock @@ -156,4 +156,4 @@ CHECKSUMS unicode-emoji (4.1.0) sha256=4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5 BUNDLED WITH - 4.0.3 + 4.1.0.dev diff --git a/tool/bundler/standard_gems.rb.lock b/tool/bundler/standard_gems.rb.lock index 669e5492a8a0f0..1b48fe058257f4 100644 --- a/tool/bundler/standard_gems.rb.lock +++ b/tool/bundler/standard_gems.rb.lock @@ -176,4 +176,4 @@ CHECKSUMS unicode-emoji (4.1.0) sha256=4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5 BUNDLED WITH - 4.0.3 + 4.1.0.dev diff --git a/tool/bundler/test_gems.rb.lock b/tool/bundler/test_gems.rb.lock index d8f7d77122846d..6fb9d1c0c5eb77 100644 --- a/tool/bundler/test_gems.rb.lock +++ b/tool/bundler/test_gems.rb.lock @@ -103,4 +103,4 @@ CHECKSUMS tilt (2.6.1) sha256=35a99bba2adf7c1e362f5b48f9b581cce4edfba98117e34696dde6d308d84770 BUNDLED WITH - 4.0.3 + 4.1.0.dev From 4b7bbd43408f230997e216a557d586edd492172d Mon Sep 17 00:00:00 2001 From: Jean-Samuel Aubry-Guzzi Date: Wed, 29 Oct 2025 08:31:26 -0400 Subject: [PATCH 13/31] [ruby/resolv] Fix TCP Requester #recv_reply https://github.com/ruby/resolv/commit/96dc3d15fe --- lib/resolv.rb | 5 +++- test/resolv/test_dns.rb | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 0e62aaf8510496..e6153af2a9d1f5 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -930,8 +930,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 Errno::ECONNRESET if len_data.nil? + len = len_data.unpack('n')[0] reply = @socks[0].read(len) + raise Errno::ECONNRESET if reply.nil? return reply, nil end diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index d5d2648e1bc649..1dda9bc6278193 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -822,4 +822,63 @@ def test_multiple_servers_with_timeout_and_truncated_tcp_fallback end end end + + def test_tcp_connection_closed_before_length + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + + server_thread = Thread.new do + ct = t.accept + ct.recv(512) + ct.close + end + + client_thread = Thread.new do + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + assert_raise(Resolv::ResolvTimeout) do + requester.request(sender, 2) + end + ensure + requester.close + end + end + + server_thread.join + client_thread.join + end + end + + def test_tcp_connection_closed_after_length + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + + server_thread = Thread.new do + ct = t.accept + ct.recv(512) + ct.send([100].pack('n'), 0) + ct.close + end + + client_thread = Thread.new do + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + assert_raise(Resolv::ResolvTimeout) do + requester.request(sender, 2) + end + ensure + requester.close + end + end + + server_thread.join + client_thread.join + end + end end From f8d0960af260219ab7c10a797ac62ecad25b2974 Mon Sep 17 00:00:00 2001 From: Jean-Samuel Aubry-Guzzi Date: Mon, 8 Dec 2025 11:22:55 -0500 Subject: [PATCH 14/31] [ruby/resolv] Handle TCP Requester #recv_reply incomplete data https://github.com/ruby/resolv/commit/9c640bdc4a --- lib/resolv.rb | 7 +++-- test/resolv/test_dns.rb | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index e6153af2a9d1f5..fa7d4e2e4753b3 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -721,7 +721,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 @@ -931,10 +932,10 @@ def initialize(host, port=Port) def recv_reply(readable_socks) len_data = readable_socks[0].read(2) - raise Errno::ECONNRESET if len_data.nil? + raise EOFError if len_data.nil? || len_data.bytesize != 2 len = len_data.unpack('n')[0] reply = @socks[0].read(len) - raise Errno::ECONNRESET if reply.nil? + raise EOFError if reply.nil? || reply.bytesize != len return reply, nil end diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index 1dda9bc6278193..7a01909eeb9a4b 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -881,4 +881,65 @@ def test_tcp_connection_closed_after_length client_thread.join end end + + def test_tcp_connection_closed_with_partial_length_prefix + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + + server_thread = Thread.new do + ct = t.accept + ct.recv(512) + ct.write "A" # 1 byte + ct.close + end + + client_thread = Thread.new do + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + assert_raise(Resolv::ResolvTimeout) do + requester.request(sender, 2) + end + ensure + requester.close + end + end + + server_thread.join + client_thread.join + end + end + + def test_tcp_connection_closed_with_partial_message_body + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + + server_thread = Thread.new do + ct = t.accept + ct.recv(512) + ct.write([10].pack('n')) # length 10 + ct.write "12345" # 5 bytes (partial) + ct.close + end + + client_thread = Thread.new do + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + assert_raise(Resolv::ResolvTimeout) do + requester.request(sender, 2) + end + ensure + requester.close + end + end + + server_thread.join + client_thread.join + end + end end From 44a17656842a567eb82c43024daaf9fcaff61e5d Mon Sep 17 00:00:00 2001 From: t-mangoe Date: Sun, 21 Dec 2025 09:16:58 +0900 Subject: [PATCH 15/31] [ruby/timeout] add test case for string argument https://github.com/ruby/timeout/commit/fef9d07f44 --- test/test_timeout.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_timeout.rb b/test/test_timeout.rb index fead81f571929d..b11fc92aeab1cb 100644 --- a/test/test_timeout.rb +++ b/test/test_timeout.rb @@ -54,6 +54,12 @@ def test_raise_for_neg_second end end + def test_raise_for_string_argument + assert_raise(NoMethodError) do + Timeout.timeout("1") { sleep(0.01) } + end + end + def test_included c = Class.new do include Timeout From f3149af35a30c4eb334006faef729a6e514d4cf2 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sun, 21 Dec 2025 05:58:23 +0900 Subject: [PATCH 16/31] [ruby/prism] Sync `Prism::Translation::ParserCurrent` with Ruby 4.0 This PR updates the fallback version for `Prism::Translation::ParserCurrent` from 3.4 to 4.0. Currently, the fallback resolves to `Parser34`, as shown below: ```console $ ruby -v -rprism -rprism/translation/parser_current -e 'p Prism::Translation::ParserCurrent' ruby 3.0.7p220 (2024-04-23 revision https://github.com/ruby/prism/commit/724a071175) [x86_64-darwin23] warning: `Prism::Translation::Current` is loading Prism::Translation::Parser34, but you are running 3.0. Prism::Translation::Parser34 ``` Following the comment "Keep this in sync with released Ruby.", it seems like the right time to set this to Ruby 4.0, which is scheduled for release this week. https://github.com/ruby/prism/commit/115f0a118c --- lib/prism/translation/parser_current.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/prism/translation/parser_current.rb b/lib/prism/translation/parser_current.rb index ac6daf7082e416..f13eff6bbe2d90 100644 --- a/lib/prism/translation/parser_current.rb +++ b/lib/prism/translation/parser_current.rb @@ -16,7 +16,7 @@ module Translation ParserCurrent = Parser41 else # Keep this in sync with released Ruby. - parser = Parser34 + parser = Parser40 major, minor, _patch = Gem::Version.new(RUBY_VERSION).segments warn "warning: `Prism::Translation::Current` is loading #{parser.name}, " \ "but you are running #{major}.#{minor}." From 1f526b348978bf71efc75f562227c1e872df27fd Mon Sep 17 00:00:00 2001 From: Taketo Takashima Date: Wed, 17 Dec 2025 23:39:43 +0900 Subject: [PATCH 17/31] [ruby/ipaddr] Follow-up fix for InvalidAddressError messages https://github.com/ruby/ipaddr/commit/b92ef74b91 --- lib/ipaddr.rb | 14 +++++++------- test/test_ipaddr.rb | 27 +++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/ipaddr.rb b/lib/ipaddr.rb index 725ff309d27f48..6b67d7eec6706a 100644 --- a/lib/ipaddr.rb +++ b/lib/ipaddr.rb @@ -368,7 +368,7 @@ def _ipv4_compat? # :nodoc: # into an IPv4-mapped IPv6 address. def ipv4_mapped if !ipv4? - raise InvalidAddressError, "not an IPv4 address: #{@addr}" + raise InvalidAddressError, "not an IPv4 address: #{to_s}" end clone = self.clone.set(@addr | 0xffff00000000, Socket::AF_INET6) clone.instance_variable_set(:@mask_addr, @mask_addr | 0xffffffffffffffffffffffff00000000) @@ -380,7 +380,7 @@ def ipv4_mapped def ipv4_compat warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE if !ipv4? - raise InvalidAddressError, "not an IPv4 address: #{@addr}" + raise InvalidAddressError, "not an IPv4 address: #{to_s}" end clone = self.clone.set(@addr, Socket::AF_INET6) clone.instance_variable_set(:@mask_addr, @mask_addr | 0xffffffffffffffffffffffff00000000) @@ -413,7 +413,7 @@ def reverse # Returns a string for DNS reverse lookup compatible with RFC3172. def ip6_arpa if !ipv6? - raise InvalidAddressError, "not an IPv6 address: #{@addr}" + raise InvalidAddressError, "not an IPv6 address: #{to_s}" end return _reverse + ".ip6.arpa" end @@ -421,7 +421,7 @@ def ip6_arpa # Returns a string for DNS reverse lookup compatible with RFC1886. def ip6_int if !ipv6? - raise InvalidAddressError, "not an IPv6 address: #{@addr}" + raise InvalidAddressError, "not an IPv6 address: #{to_s}" end return _reverse + ".ip6.int" end @@ -743,19 +743,19 @@ def in6_addr(left) right = '' when RE_IPV6ADDRLIKE_COMPRESSED if $4 - left.count(':') <= 6 or raise InvalidAddressError, "invalid address: #{@addr}" + left.count(':') <= 6 or raise InvalidAddressError, "invalid address: #{left}" addr = in_addr($~[4,4]) left = $1 right = $3 + '0:0' else left.count(':') <= ($1.empty? || $2.empty? ? 8 : 7) or - raise InvalidAddressError, "invalid address: #{@addr}" + raise InvalidAddressError, "invalid address: #{left}" left = $1 right = $2 addr = 0 end else - raise InvalidAddressError, "invalid address: #{@addr}" + raise InvalidAddressError, "invalid address: #{left}" end l = left.split(':') r = right.split(':') diff --git a/test/test_ipaddr.rb b/test/test_ipaddr.rb index 005927cd054cd9..4b7229fc17b686 100644 --- a/test/test_ipaddr.rb +++ b/test/test_ipaddr.rb @@ -207,6 +207,15 @@ def test_ipv4_compat assert_equal(112, b.prefix) end + def test_ipv4_compat_with_error_message + e = assert_raise(IPAddr::InvalidAddressError) do + assert_warning(/obsolete/) { + IPAddr.new('2001:db8::').ipv4_compat + } + end + assert_equal('not an IPv4 address: 2001:db8::', e.message) + end + def test_ipv4_mapped a = IPAddr.new("::ffff:192.168.1.2") assert_equal("::ffff:192.168.1.2", a.to_s) @@ -224,6 +233,13 @@ def test_ipv4_mapped assert_equal(Socket::AF_INET6, b.family) end + def test_ipv4_mapped_with_error_message + e = assert_raise(IPAddr::InvalidAddressError) do + IPAddr.new('2001:db8::').ipv4_mapped + end + assert_equal('not an IPv4 address: 2001:db8::', e.message) + end + def test_reverse assert_equal("f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.0.5.0.5.0.e.f.f.3.ip6.arpa", IPAddr.new("3ffe:505:2::f").reverse) assert_equal("1.2.168.192.in-addr.arpa", IPAddr.new("192.168.2.1").reverse) @@ -231,16 +247,18 @@ def test_reverse def test_ip6_arpa assert_equal("f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.0.5.0.5.0.e.f.f.3.ip6.arpa", IPAddr.new("3ffe:505:2::f").ip6_arpa) - assert_raise(IPAddr::InvalidAddressError) { + e = assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("192.168.2.1").ip6_arpa } + assert_equal('not an IPv6 address: 192.168.2.1', e.message) end def test_ip6_int assert_equal("f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.0.5.0.5.0.e.f.f.3.ip6.int", IPAddr.new("3ffe:505:2::f").ip6_int) - assert_raise(IPAddr::InvalidAddressError) { + e = assert_raise(IPAddr::InvalidAddressError) { IPAddr.new("192.168.2.1").ip6_int } + assert_equal('not an IPv6 address: 192.168.2.1', e.message) end def test_prefix_writer @@ -631,5 +649,10 @@ def test_raises_invalid_address_error_with_error_message IPAddr.new('192.168.01.100') end assert_equal('zero-filled number in IPv4 address is ambiguous: 192.168.01.100', e.message) + + e = assert_raise(IPAddr::InvalidAddressError) do + IPAddr.new('INVALID') + end + assert_equal('invalid address: INVALID', e.message) end end From 8ccfb375b9ade4504c4012ce8c31adc7e12dc49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=85=E6=88=91=E5=B1=B1=E8=8F=9C=E3=80=85?= Date: Fri, 19 Dec 2025 01:30:37 +0900 Subject: [PATCH 18/31] [ruby/json] Update `fpconv_dtoa` definition to use `dest[32]` https://github.com/ruby/json/commit/4808fee9af --- ext/json/vendor/fpconv.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/vendor/fpconv.c b/ext/json/vendor/fpconv.c index 4888ab4b8bcefe..6c9bc2c103396d 100644 --- a/ext/json/vendor/fpconv.c +++ b/ext/json/vendor/fpconv.c @@ -449,7 +449,7 @@ static int filter_special(double fp, char* dest) * } * */ -static int fpconv_dtoa(double d, char dest[28]) +static int fpconv_dtoa(double d, char dest[32]) { char digits[18]; From 4d7db86a794581f1be405dcc70bb06549b8cf28f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 22 Dec 2025 09:16:09 +0100 Subject: [PATCH 19/31] [ruby/json] Add missing documentation for `allow_control_characters` parsing option https://github.com/ruby/json/commit/a5c160f372 --- ext/json/lib/json.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb index 43d96afa95835c..f619d93252d950 100644 --- a/ext/json/lib/json.rb +++ b/ext/json/lib/json.rb @@ -173,6 +173,18 @@ # When enabled: # JSON.parse('[1,]', allow_trailing_comma: true) # => [1] # +# --- +# +# Option +allow_control_characters+ (boolean) specifies whether to allow +# unescaped ASCII control characters, such as newlines, in strings; +# defaults to +false+. +# +# With the default, +false+: +# JSON.parse(%{"Hello\nWorld"}) # invalid ASCII control character in string (JSON::ParserError) +# +# When enabled: +# JSON.parse(%{"Hello\nWorld"}, allow_control_characters: true) # => "Hello\nWorld" +# # ====== Output Options # # Option +freeze+ (boolean) specifies whether the returned objects will be frozen; From fda7019c80c1026ee89ba772fb7db547c89e541a Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Mon, 8 Dec 2025 13:06:08 +0100 Subject: [PATCH 20/31] [ruby/net-protocol] Add Net::Protocol::TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT * To find out efficiently if TCPSocket#initialize supports the open_timeout keyword argument. https://github.com/ruby/net-protocol/commit/738c06f950 --- lib/net/protocol.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index 1443f3e8b71966..8c81298c0e420a 100644 --- a/lib/net/protocol.rb +++ b/lib/net/protocol.rb @@ -54,6 +54,16 @@ def ssl_socket_connect(s, timeout) s.connect end end + + tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters + TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]] + tcp_socket_parameters.include?([:key, :open_timeout]) + else + # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize + # See discussion in https://github.com/ruby/net-http/pull/224 + Socket.method(:tcp).parameters.include?([:key, :open_timeout]) + end + private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT end From 70c7f3ad77c09e379b9e7ffb8009ccc2cb47f234 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Wed, 17 Dec 2025 14:28:48 +0900 Subject: [PATCH 21/31] [ruby/strscan] Bump version https://github.com/ruby/strscan/commit/747a3b5def --- ext/strscan/strscan.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/strscan/strscan.c b/ext/strscan/strscan.c index 11e3d309a81757..c44e77ba91056c 100644 --- a/ext/strscan/strscan.c +++ b/ext/strscan/strscan.c @@ -22,7 +22,7 @@ extern size_t onig_region_memsize(const struct re_registers *regs); #include -#define STRSCAN_VERSION "3.1.6" +#define STRSCAN_VERSION "3.1.7" #ifdef HAVE_RB_DEPRECATE_CONSTANT From 93df96684860ea437de982d6778fc9d845d0505a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 26 Dec 2025 09:37:17 +0900 Subject: [PATCH 22/31] Mark development version for unreleased gems --- ext/stringio/stringio.c | 2 +- ext/strscan/strscan.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 93a419ff3172fa..cc2294a795a378 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -13,7 +13,7 @@ **********************************************************************/ static const char *const -STRINGIO_VERSION = "3.2.1"; +STRINGIO_VERSION = "3.2.1.dev"; #include diff --git a/ext/strscan/strscan.c b/ext/strscan/strscan.c index c44e77ba91056c..935fce19df94e9 100644 --- a/ext/strscan/strscan.c +++ b/ext/strscan/strscan.c @@ -22,7 +22,7 @@ extern size_t onig_region_memsize(const struct re_registers *regs); #include -#define STRSCAN_VERSION "3.1.7" +#define STRSCAN_VERSION "3.1.7.dev" #ifdef HAVE_RB_DEPRECATE_CONSTANT From 89af235435a911d86e04abdb1a54f4fe25dcaa6a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 26 Dec 2025 09:44:55 +0900 Subject: [PATCH 23/31] Added ruby_41? platform --- lib/bundler/current_ruby.rb | 2 +- spec/bundler/bundler/current_ruby_spec.rb | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/bundler/current_ruby.rb b/lib/bundler/current_ruby.rb index ab6520a106a40f..17c7655adb588f 100644 --- a/lib/bundler/current_ruby.rb +++ b/lib/bundler/current_ruby.rb @@ -11,7 +11,7 @@ def self.current_ruby end class CurrentRuby - ALL_RUBY_VERSIONS = [*18..27, *30..34, 40].freeze + ALL_RUBY_VERSIONS = [*18..27, *30..34, *40..41].freeze KNOWN_MINOR_VERSIONS = ALL_RUBY_VERSIONS.map {|v| v.digits.reverse.join(".") }.freeze KNOWN_MAJOR_VERSIONS = ALL_RUBY_VERSIONS.map {|v| v.digits.last.to_s }.uniq.freeze PLATFORM_MAP = { diff --git a/spec/bundler/bundler/current_ruby_spec.rb b/spec/bundler/bundler/current_ruby_spec.rb index aa19a414076508..79eb802aa52215 100644 --- a/spec/bundler/bundler/current_ruby_spec.rb +++ b/spec/bundler/bundler/current_ruby_spec.rb @@ -23,6 +23,7 @@ ruby_33: Gem::Platform::RUBY, ruby_34: Gem::Platform::RUBY, ruby_40: Gem::Platform::RUBY, + ruby_41: Gem::Platform::RUBY, mri: Gem::Platform::RUBY, mri_18: Gem::Platform::RUBY, mri_19: Gem::Platform::RUBY, @@ -40,6 +41,7 @@ mri_33: Gem::Platform::RUBY, mri_34: Gem::Platform::RUBY, mri_40: Gem::Platform::RUBY, + mri_41: Gem::Platform::RUBY, rbx: Gem::Platform::RUBY, truffleruby: Gem::Platform::RUBY, jruby: Gem::Platform::JAVA, @@ -61,7 +63,8 @@ windows_32: Gem::Platform::WINDOWS, windows_33: Gem::Platform::WINDOWS, windows_34: Gem::Platform::WINDOWS, - windows_40: Gem::Platform::WINDOWS } + windows_40: Gem::Platform::WINDOWS, + windows_41: Gem::Platform::WINDOWS } end let(:deprecated) do @@ -82,6 +85,7 @@ mswin_33: Gem::Platform::MSWIN, mswin_34: Gem::Platform::MSWIN, mswin_40: Gem::Platform::MSWIN, + mswin_41: Gem::Platform::MSWIN, mswin64: Gem::Platform::MSWIN64, mswin64_19: Gem::Platform::MSWIN64, mswin64_20: Gem::Platform::MSWIN64, @@ -98,6 +102,7 @@ mswin64_33: Gem::Platform::MSWIN64, mswin64_34: Gem::Platform::MSWIN64, mswin64_40: Gem::Platform::MSWIN64, + mswin64_41: Gem::Platform::MSWIN64, mingw: Gem::Platform::UNIVERSAL_MINGW, mingw_18: Gem::Platform::UNIVERSAL_MINGW, mingw_19: Gem::Platform::UNIVERSAL_MINGW, @@ -115,6 +120,7 @@ mingw_33: Gem::Platform::UNIVERSAL_MINGW, mingw_34: Gem::Platform::UNIVERSAL_MINGW, mingw_40: Gem::Platform::UNIVERSAL_MINGW, + mingw_41: Gem::Platform::UNIVERSAL_MINGW, x64_mingw: Gem::Platform::UNIVERSAL_MINGW, x64_mingw_20: Gem::Platform::UNIVERSAL_MINGW, x64_mingw_21: Gem::Platform::UNIVERSAL_MINGW, @@ -129,7 +135,8 @@ x64_mingw_32: Gem::Platform::UNIVERSAL_MINGW, x64_mingw_33: Gem::Platform::UNIVERSAL_MINGW, x64_mingw_34: Gem::Platform::UNIVERSAL_MINGW, - x64_mingw_40: Gem::Platform::UNIVERSAL_MINGW } + x64_mingw_40: Gem::Platform::UNIVERSAL_MINGW, + x64_mingw_41: Gem::Platform::UNIVERSAL_MINGW } end # rubocop:enable Naming/VariableNumber From 565ea26ad10ea8c3c6ce9bdae6cbb78353a9ad36 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 26 Dec 2025 10:14:16 +0900 Subject: [PATCH 24/31] Disabled to run lobsters benchmark because it didn't work with Ruby 4.1 yet --- .github/workflows/ubuntu.yml | 2 +- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 46ad7c82128e23..bf34baaced0fd2 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -243,7 +243,7 @@ jobs: path: ruby-bench - name: Run ruby-bench - run: ruby run_benchmarks.rb -e "ruby::../build/install/bin/ruby" ${{ matrix.bench_opts }} + run: rm -rf benchmarks/lobsters && ruby run_benchmarks.rb -e "ruby::../build/install/bin/ruby" ${{ matrix.bench_opts }} working-directory: ruby-bench - uses: ./.github/actions/slack diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index ed559d64e10d05..55bfcb30f22364 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -198,7 +198,7 @@ jobs: path: ruby-bench - name: Run ruby-bench - run: ruby run_benchmarks.rb -e "zjit::../build/install/bin/ruby ${{ matrix.ruby_opts }}" ${{ matrix.bench_opts }} + run: rm -rf benchmarks/lobsters && ruby run_benchmarks.rb -e "zjit::../build/install/bin/ruby ${{ matrix.ruby_opts }}" ${{ matrix.bench_opts }} working-directory: ruby-bench - uses: ./.github/actions/slack diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 37a9000d704c5a..29b7aaad592c7e 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -251,7 +251,7 @@ jobs: path: ruby-bench - name: Run ruby-bench - run: ruby run_benchmarks.rb -e "zjit::../build/install/bin/ruby ${{ matrix.ruby_opts }}" ${{ matrix.bench_opts }} + run: rm -rf benchmarks/lobsters && ruby run_benchmarks.rb -e "zjit::../build/install/bin/ruby ${{ matrix.ruby_opts }}" ${{ matrix.bench_opts }} working-directory: ruby-bench - uses: ./.github/actions/slack From e95a9942bb77ab17a6743a2703cc9c7d5293cc5c Mon Sep 17 00:00:00 2001 From: git Date: Fri, 26 Dec 2025 02:01:49 +0000 Subject: [PATCH 25/31] Update default gems list at 565ea26ad10ea8c3c6ce9bdae6cbb7 [ci skip] --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index b9b5fb3ed247ca..2cc6f599f056fd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -25,6 +25,10 @@ The following default gem is added. The following default gems are updated. +* RubyGems 4.1.0.dev +* bundler 4.1.0.dev +* stringio 3.2.1.dev +* strscan 3.1.7.dev The following bundled gems are updated. From 02275b1e53339f34e46b7e69e1512895ea105042 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 26 Dec 2025 11:11:56 +0900 Subject: [PATCH 26/31] uutils-coreutils 0.5.0 has been removed uutils wrapper --- .github/workflows/windows.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c19d9e4aa78577..7f3e60349642e0 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -112,7 +112,7 @@ jobs: # https://github.com/actions/virtual-environments/issues/712#issuecomment-613004302 run: | ::- Set up VC ${{ matrix.vc }} - set | uutils sort > old.env + set | sort > old.env call ..\src\win32\vssetup.cmd ^ -arch=${{ matrix.target || 'amd64' }} ^ ${{ matrix.vcvars && '-vcvars_ver=' || '' }}${{ matrix.vcvars }} @@ -122,8 +122,8 @@ jobs: set MAKEFLAGS=l set /a TEST_JOBS=(15 * %NUMBER_OF_PROCESSORS% / 10) > nul set RUBY_OPT_DIR=%GITHUB_WORKSPACE:\=/%/src/vcpkg_installed/%VCPKG_DEFAULT_TRIPLET% - set | uutils sort > new.env - uutils comm -13 old.env new.env >> %GITHUB_ENV% + set | sort > new.env + comm -13 old.env new.env >> %GITHUB_ENV% del *.env - name: baseruby version From 9824724b2ffe583302e9318c6eff7a440478125f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 26 Dec 2025 11:39:43 +0900 Subject: [PATCH 27/31] Skip test_write_binary(GemSingletonTest) at rbs tests ``` Errno::EACCES: Permission denied @ rb_file_s_rename ... D:/a/ruby/ruby/src/lib/rubygems/util/atomic_file_writer.rb:42:in 'File.rename' ``` It may caused with atomic_file_writer.rb --- tool/rbs_skip_tests_windows | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tool/rbs_skip_tests_windows b/tool/rbs_skip_tests_windows index 1b9f930e846624..35e78e237f1668 100644 --- a/tool/rbs_skip_tests_windows +++ b/tool/rbs_skip_tests_windows @@ -107,3 +107,7 @@ test_new(TempfileSingletonTest) test_open(ZlibGzipReaderSingletonTest) test_reachable_objects_from_root(ObjectSpaceTest) To avoid NoMemoryError with test-unit 3.7.5 + +# Errno::EACCES: Permission denied @ rb_file_s_rename +# D:/a/ruby/ruby/src/lib/rubygems/util/atomic_file_writer.rb:42:in 'File.rename' +test_write_binary(GemSingletonTest) From 594dd8bfd4c2b380dc7185d421d71b29c379356b Mon Sep 17 00:00:00 2001 From: Takashi Sakaguchi Date: Fri, 26 Dec 2025 12:37:46 +0900 Subject: [PATCH 28/31] [ruby/pp] Support private instance_variables_to_inspect (https://github.com/ruby/pp/pull/70) * Support private instance_variables_to_inspect in pp Ruby supports calling instance_variables_to_inspect even when it is defined as a private method (ruby/ruby#13555). This change aligns pp with Ruby's behavior. https://github.com/ruby/pp/commit/8450e76db6 --- lib/pp.rb | 2 +- test/test_pp.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pp.rb b/lib/pp.rb index fcd33ba80e9e2e..5fd29a373aa3e2 100644 --- a/lib/pp.rb +++ b/lib/pp.rb @@ -388,7 +388,7 @@ def pretty_print_cycle(q) # This method should return an array of names of instance variables as symbols or strings as: # +[:@a, :@b]+. def pretty_print_instance_variables - ivars = respond_to?(:instance_variables_to_inspect) ? instance_variables_to_inspect : instance_variables + ivars = respond_to?(:instance_variables_to_inspect, true) ? instance_variables_to_inspect || instance_variables : instance_variables ivars.sort end diff --git a/test/test_pp.rb b/test/test_pp.rb index 4a273e6edd0aec..922ed371af3e8f 100644 --- a/test/test_pp.rb +++ b/test/test_pp.rb @@ -146,7 +146,9 @@ def a.pretty_print_instance_variables() [:@b] end def test_iv_hiding_via_ruby a = Object.new - def a.instance_variables_to_inspect() [:@b] end + a.singleton_class.class_eval do + private def instance_variables_to_inspect() [:@b] end + end a.instance_eval { @a = "aaa"; @b = "bbb" } assert_match(/\A#\n\z/, PP.pp(a, ''.dup)) end From bad7dd5d74e9d14e84faf8cc8907dcdfdb8751e8 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 26 Dec 2025 09:16:36 +0900 Subject: [PATCH 29/31] [DOC] Separate updated gems lists into sections in NEWS.md --- NEWS.md | 8 ++++---- tool/update-NEWS-gemlist.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2cc6f599f056fd..59a2b4b49d4c44 100644 --- a/NEWS.md +++ b/NEWS.md @@ -19,18 +19,18 @@ Other changes are listed in the following sections. We also listed release history from the previous bundled version that is Ruby 3.4.0 if it has GitHub releases. -The following bundled gems are promoted from default gems. +### The following bundled gems are promoted from default gems. -The following default gem is added. +### The following default gem is added. -The following default gems are updated. +### The following default gems are updated. * RubyGems 4.1.0.dev * bundler 4.1.0.dev * stringio 3.2.1.dev * strscan 3.1.7.dev -The following bundled gems are updated. +### The following bundled gems are updated. ### RubyGems and Bundler diff --git a/tool/update-NEWS-gemlist.rb b/tool/update-NEWS-gemlist.rb index e1535eb400bc7e..0b5503580d3de9 100755 --- a/tool/update-NEWS-gemlist.rb +++ b/tool/update-NEWS-gemlist.rb @@ -6,10 +6,10 @@ update = ->(list, type, desc = "updated") do item = ->(mark = "* ") do - "The following #{type} gem#{list.size == 1 ? ' is' : 's are'} #{desc}.\n\n" + + "### The following #{type} gem#{list.size == 1 ? ' is' : 's are'} #{desc}.\n\n" + list.map {|g, v|"#{mark}#{g} #{v}\n"}.join("") + "\n" end - news.sub!(/^(?:\*( +))?The following #{type} gems? (?:are|is) #{desc}\.\n+(?:(?(1) \1)\*( *).*\n)*\n*/) do + news.sub!(/^(?:\*( +)|#+ *)?The following #{type} gems? (?:are|is) #{desc}\.\n+(?:(?(1) \1)\*( *).*\n)*\n*/) do item["#{$1&.<< " "}*#{$2 || ' '}"] end or news.sub!(/^## Stdlib updates(?:\n+The following.*(?:\n+( *\* *).*)*)*\n+\K/) do item[$1 || "* "] From b01fd2d8c3195cc9936f6be6f89df0de12394a28 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Thu, 25 Dec 2025 12:05:06 +0200 Subject: [PATCH 30/31] Fix RSET_IS_MEMBER macro parameter mismatch The RSET_IS_MEMBER macro had a parameter named 'sobj' but the macro body used 'set' instead, causing the first argument to be ignored. This worked by accident because all current callers use a variable named 'set', but would cause compilation failure if called with a differently named variable: error: use of undeclared identifier 'set' Changed the parameter name from 'sobj' to 'set' to match the macro body and be consistent with other RSET_* macros. --- set.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/set.c b/set.c index 469078ee9968af..d7704e9cf38a4f 100644 --- a/set.c +++ b/set.c @@ -116,7 +116,7 @@ static ID id_class_methods; #define RSET_SIZE(set) set_table_size(RSET_TABLE(set)) #define RSET_EMPTY(set) (RSET_SIZE(set) == 0) #define RSET_SIZE_NUM(set) SIZET2NUM(RSET_SIZE(set)) -#define RSET_IS_MEMBER(sobj, item) set_table_lookup(RSET_TABLE(set), (st_data_t)(item)) +#define RSET_IS_MEMBER(set, item) set_table_lookup(RSET_TABLE(set), (st_data_t)(item)) #define RSET_COMPARE_BY_IDENTITY(set) (RSET_TABLE(set)->type == &identhash) struct set_object { From 704ac72fb67bc431c7d77d0d0aaa7e5b5331b096 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Thu, 25 Dec 2025 00:08:38 +0900 Subject: [PATCH 31/31] Clarify the intent of the test for "ruby -h" to fit in 80x25 --- test/ruby/test_rubyoptions.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_rubyoptions.rb b/test/ruby/test_rubyoptions.rb index 735d2681fb6a13..cd2dd5d3ff7412 100644 --- a/test/ruby/test_rubyoptions.rb +++ b/test/ruby/test_rubyoptions.rb @@ -47,10 +47,15 @@ def test_source_file assert_in_out_err([], "", [], []) end + # This constant enforces the traditional 80x25 terminal size standard + TRADITIONAL_TERM_COLS = 80 # DO NOT MODIFY! + TRADITIONAL_TERM_ROWS = 25 # DO NOT MODIFY! + def test_usage + # This test checks if the output of `ruby -h` fits in 80x25 assert_in_out_err(%w(-h)) do |r, e| - assert_operator(r.size, :<=, 25) - longer = r[1..-1].select {|x| x.size >= 80} + assert_operator(r.size, :<=, TRADITIONAL_TERM_ROWS) + longer = r[1..-1].select {|x| x.size >= TRADITIONAL_TERM_COLS} assert_equal([], longer) assert_equal([], e) end