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/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 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 diff --git a/NEWS.md b/NEWS.md index b9b5fb3ed247ca..59a2b4b49d4c44 100644 --- a/NEWS.md +++ b/NEWS.md @@ -19,14 +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/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/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/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/date/date_core.c b/ext/date/date_core.c index 6bcf272b62d8b0..9755fb819eaaa9 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -452,11 +452,43 @@ 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) + +/* + * 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); +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) { int d, rm, rd; + 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++) if (c_valid_civil_p(y, 1, d, sg, &rm, &rd, rjd, ns)) return 1; @@ -468,6 +500,9 @@ c_find_ldoy(int y, double sg, int *rjd, int *ns) { int i, rm, rd; + 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++) if (c_valid_civil_p(y, 12, 31 - i, sg, &rm, &rd, rjd, ns)) return 1; @@ -493,6 +528,9 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns) { int i, rm, rd; + 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++) if (c_valid_civil_p(y, m, 31 - i, sg, &rm, &rd, rjd, ns)) return 1; @@ -502,55 +540,69 @@ 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; + + 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); - if (m <= 2) { - y -= 1; - m += 12; - } - 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; 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 ((c_gregorian_only_p(sg) || 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,147 @@ 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_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. + * 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, 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 = (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_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_EPOCH; + + /* Extract century and day within 400-year cycle */ + /* Use Euclidean (floor) division for negative values */ + n1 = 4 * r0 + 3; + q1 = DIV(n1, NS_DAYS_IN_400_YEARS); + r1 = MOD(n1, NS_DAYS_IN_400_YEARS) / 4; + + /* Calculate year within century and day of year */ + n2 = 4 * r1 + 3; + /* Use 64-bit arithmetic to avoid overflow */ + u2 = (uint64_t)NS_YEAR_MULTIPLIER * (uint64_t)n2; + q2 = (int)(u2 >> 32); + r2 = (int)((uint32_t)u2 / NS_YEAR_MULTIPLIER / 4); + + /* Calculate month and day using integer arithmetic */ + n3 = NS_MONTH_COEFF * r2 + NS_MONTH_OFFSET; + q3 = n3 >> 16; + r3 = (n3 & 0xFFFF) / NS_MONTH_COEFF; + + /* Combine century and year */ + y0 = NS_YEARS_PER_CENTURY * q1 + q2; + + /* Adjust for January/February (shift from fiscal year) */ + j = (r2 >= NS_DAYS_BEFORE_NEW_YEAR) ? 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) { 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; 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]; diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 05bae94529b9db..cc2294a795a378 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.dev"; #include @@ -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) @@ -1649,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) @@ -1723,10 +1725,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) diff --git a/ext/strscan/strscan.c b/ext/strscan/strscan.c index 11e3d309a81757..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.6" +#define STRSCAN_VERSION "3.1.7.dev" #ifdef HAVE_RB_DEPRECATE_CONSTANT 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/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/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/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/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 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/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}." diff --git a/lib/resolv.rb b/lib/resolv.rb index 0e62aaf8510496..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 @@ -930,8 +931,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 diff --git a/lib/rubygems.rb b/lib/rubygems.rb index e99176fec0e107..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" @@ -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/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/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/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 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 { 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 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/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index d5d2648e1bc649..7a01909eeb9a4b 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -822,4 +822,124 @@ 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 + + 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 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 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 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 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 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 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 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) 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 || "* "]