From 0a31698437140428ac6064e7f73a346890af9f40 Mon Sep 17 00:00:00 2001 From: meganemura Date: Wed, 5 Aug 2026 21:51:47 +0900 Subject: [PATCH 01/10] [ruby/rubygems] Fix release date extraction for the current changelog header format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release_date_for in spec/support/build_metadata.rb matched the changelog's old `## VERSION (DATE)` header, but .changelog.yml has generated `## VERSION / DATE` headers since Bundler 4.0.9. The regex silently failed to match, so built_at was baked as nil into every release since, and Bundler::BuildMetadata.timestamp fell back to Time.now.utc on every invocation — `bundle version` always showed today's date instead of the real release date. spec/support/build_metadata.rb is release tooling for the bundler:build_metadata rake task, not part of the RSpec bundler suite; spec_helper.rb never requires it. The sibling release script tool/changelog.rb already has its test in test/test_changelog_generator.rb, so the regression test for this lives in test/test_build_metadata_generator.rb to match. https://github.com/ruby/rubygems/commit/b69384e96b Co-Authored-By: Claude Sonnet 5 --- spec/bundler/support/build_metadata.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb index 8469d96a760112..6cd6922c0261fc 100644 --- a/spec/bundler/support/build_metadata.rb +++ b/spec/bundler/support/build_metadata.rb @@ -45,7 +45,7 @@ def git_commit_sha def release_date_for(version, dir:) changelog = File.expand_path("CHANGELOG-bundler.md", dir) - File.readlines(changelog)[2].scan(/^## #{Regexp.escape(version)} \((.*)\)/).first&.first if File.exist?(changelog) + File.readlines(changelog)[2].scan(/^## #{Regexp.escape(version)} \/ (.*)$/).first&.first if File.exist?(changelog) end extend self From 74bc171e1ad528b762330712c78111f15e266b14 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 25 Aug 2026 10:29:17 +0900 Subject: [PATCH 02/10] [ruby/rubygems] Update changelog fixture to the current header format The fixture changelog still used the old `## VERSION (DATE)` header, so the fixed `release_date_for` no longer matched it and the released case of the version spec failed. https://github.com/ruby/rubygems/commit/36ee802a7e Co-Authored-By: Claude Fable 5 --- spec/bundler/support/path.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb index 68baec93778f22..2e7fcd95455dbc 100644 --- a/spec/bundler/support/path.rb +++ b/spec/bundler/support/path.rb @@ -307,7 +307,7 @@ def replace_required_ruby_version(version, dir:) def replace_changelog(version, dir:) changelog = File.expand_path("CHANGELOG-bundler.md", dir) contents = File.readlines(changelog) - contents = [contents[0], contents[1], "## #{version} (2100-01-01)\n", *contents[3..-1]].join + contents = [contents[0], contents[1], "## #{version} / 2100-01-01\n", *contents[3..-1]].join File.open(changelog, "w") {|f| f << contents } end From dda27dcf5c3bdfec239840477fb211e99c32cc22 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 25 Aug 2026 11:02:03 +0900 Subject: [PATCH 03/10] [ruby/rubygems] Use %r for the slash-containing regexp Fixes the Style/RegexpLiteral offense from CI. https://github.com/ruby/rubygems/commit/c4069df44d Co-Authored-By: Claude Fable 5 --- spec/bundler/support/build_metadata.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb index 6cd6922c0261fc..373bd7e55d5d26 100644 --- a/spec/bundler/support/build_metadata.rb +++ b/spec/bundler/support/build_metadata.rb @@ -45,7 +45,7 @@ def git_commit_sha def release_date_for(version, dir:) changelog = File.expand_path("CHANGELOG-bundler.md", dir) - File.readlines(changelog)[2].scan(/^## #{Regexp.escape(version)} \/ (.*)$/).first&.first if File.exist?(changelog) + File.readlines(changelog)[2].scan(%r{^## #{Regexp.escape(version)} / (.*)$}).first&.first if File.exist?(changelog) end extend self From 44da9d7384e6fd93595fddb8fa5cfd33a88e2d82 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 25 Aug 2026 14:46:49 +1200 Subject: [PATCH 04/10] Make `IO::Buffer` I/O operations single-transfer and composable. (#18483) --- NEWS.md | 10 + doc/language/fiber.md | 30 ++- include/ruby/fiber/scheduler.h | 41 ++-- include/ruby/io/buffer.h | 14 +- io.c | 14 +- io_buffer.c | 379 ++++++++++----------------------- scheduler.c | 98 ++++----- test/fiber/scheduler.rb | 114 ++-------- test/fiber/test_io_buffer.rb | 77 ++++++- test/ruby/test_io_buffer.rb | 47 ++-- 10 files changed, 331 insertions(+), 493 deletions(-) diff --git a/NEWS.md b/NEWS.md index 064327addcbb12..018e0e2c7b448e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -50,6 +50,16 @@ Note: We're only listing outstanding class updates. binary representation of a non-negative integer (its population count). [[Feature #20163]] +* IO::Buffer + + * `read`, `write`, `pread`, and `pwrite` now perform one IO operation using + `(offset, length)`, where `length` is the maximum transfer size. Short + transfers are returned directly. + + * The corresponding fiber scheduler hooks are no longer experimental and + now use the same `(offset, length)` argument order and single-operation + semantics. + * Kernel * `Kernel#autoload_relative` and `Module#autoload_relative` are added. diff --git a/doc/language/fiber.md b/doc/language/fiber.md index d9011cce2f2bbd..bdb41d9afe6125 100644 --- a/doc/language/fiber.md +++ b/doc/language/fiber.md @@ -83,19 +83,37 @@ class Scheduler end # Read from the given io into the specified buffer. - # WARNING: Experimental hook! Do not use in production code! # @parameter io [IO] The io to read from. # @parameter buffer [IO::Buffer] The buffer to read into. - # @parameter length [Integer] The minimum amount to read. - def io_read(io, buffer, length) + # @parameter offset [Integer] The offset in the buffer to read to. + # @parameter length [Integer] The maximum amount to read in one operation. + def io_read(io, buffer, offset, length) + end + + # Read from the given io at the specified position into the specified buffer. + # @parameter io [IO] The io to read from. + # @parameter buffer [IO::Buffer] The buffer to read into. + # @parameter from [Integer] The position in the io to read from. + # @parameter offset [Integer] The offset in the buffer to read to. + # @parameter length [Integer] The maximum amount to read in one operation. + def io_pread(io, buffer, from, offset, length) end # Write from the given buffer into the specified IO. - # WARNING: Experimental hook! Do not use in production code! # @parameter io [IO] The io to write to. # @parameter buffer [IO::Buffer] The buffer to write from. - # @parameter length [Integer] The minimum amount to write. - def io_write(io, buffer, length) + # @parameter offset [Integer] The offset in the buffer to write from. + # @parameter length [Integer] The maximum amount to write in one operation. + def io_write(io, buffer, offset, length) + end + + # Write from the given buffer to the specified position in the given io. + # @parameter io [IO] The io to write to. + # @parameter buffer [IO::Buffer] The buffer to write from. + # @parameter from [Integer] The position in the io to write to. + # @parameter offset [Integer] The offset in the buffer to write from. + # @parameter length [Integer] The maximum amount to write in one operation. + def io_pwrite(io, buffer, from, offset, length) end # Sleep the current task for the specified duration, or forever if not diff --git a/include/ruby/fiber/scheduler.h b/include/ruby/fiber/scheduler.h index 4d764f68ae06ed..73a1ed40640be0 100644 --- a/include/ruby/fiber/scheduler.h +++ b/include/ruby/fiber/scheduler.h @@ -24,7 +24,8 @@ RBIMPL_SYMBOL_EXPORT_BEGIN() // Version 3: Adds support for `fiber_interrupt`. -#define RUBY_FIBER_SCHEDULER_VERSION 3 +// Version 4: IO hooks use single-transfer `(offset, length)` semantics. +#define RUBY_FIBER_SCHEDULER_VERSION 4 struct timeval; struct rb_thread_struct; @@ -292,12 +293,12 @@ VALUE rb_fiber_scheduler_io_selectv(VALUE scheduler, int argc, VALUE *argv); * @param[in] scheduler Target scheduler. * @param[in] io An io object to read from. * @param[in] buffer The buffer to read to. - * @param[in] length The minimum number of bytes to read. - * @param[in] offset The offset in the buffer to read from. + * @param[in] offset The offset in the buffer to read to. + * @param[in] length The maximum number of bytes to read in one operation. * @retval RUBY_Qundef `scheduler` doesn't have `#io_read`. * @return otherwise What `scheduler.io_read` returns `[-errno, size]`. */ -VALUE rb_fiber_scheduler_io_read(VALUE scheduler, VALUE io, VALUE buffer, size_t length, size_t offset); +VALUE rb_fiber_scheduler_io_read(VALUE scheduler, VALUE io, VALUE buffer, size_t offset, size_t length); /** * Non-blocking write to the passed IO. @@ -305,12 +306,12 @@ VALUE rb_fiber_scheduler_io_read(VALUE scheduler, VALUE io, VALUE buffer, size_t * @param[in] scheduler Target scheduler. * @param[in] io An io object to write to. * @param[in] buffer The buffer to write from. - * @param[in] length The minimum number of bytes to write. * @param[in] offset The offset in the buffer to write from. + * @param[in] length The maximum number of bytes to write in one operation. * @retval RUBY_Qundef `scheduler` doesn't have `#io_write`. * @return otherwise What `scheduler.io_write` returns `[-errno, size]`. */ -VALUE rb_fiber_scheduler_io_write(VALUE scheduler, VALUE io, VALUE buffer, size_t length, size_t offset); +VALUE rb_fiber_scheduler_io_write(VALUE scheduler, VALUE io, VALUE buffer, size_t offset, size_t length); /** * Non-blocking read from the passed IO at the specified offset. @@ -319,12 +320,12 @@ VALUE rb_fiber_scheduler_io_write(VALUE scheduler, VALUE io, VALUE buffer, size_ * @param[in] io An io object to read from. * @param[in] from The offset to read from. * @param[in] buffer The buffer to read to. - * @param[in] length The minimum number of bytes to read. * @param[in] offset The offset in the buffer to read to. + * @param[in] length The maximum number of bytes to read in one operation. * @retval RUBY_Qundef `scheduler` doesn't have `#io_read`. * @return otherwise What `scheduler.io_read` returns. */ -VALUE rb_fiber_scheduler_io_pread(VALUE scheduler, VALUE io, rb_off_t from, VALUE buffer, size_t length, size_t offset); +VALUE rb_fiber_scheduler_io_pread(VALUE scheduler, VALUE io, rb_off_t from, VALUE buffer, size_t offset, size_t length); /** * Non-blocking write to the passed IO at the specified offset. @@ -333,12 +334,12 @@ VALUE rb_fiber_scheduler_io_pread(VALUE scheduler, VALUE io, rb_off_t from, VALU * @param[in] io An io object to write to. * @param[in] from The offset to write to. * @param[in] buffer The buffer to write from. - * @param[in] length The minimum number of bytes to write. * @param[in] offset The offset in the buffer to write from. + * @param[in] length The maximum number of bytes to write in one operation. * @retval RUBY_Qundef `scheduler` doesn't have `#io_write`. * @return otherwise What `scheduler.io_write` returns. */ -VALUE rb_fiber_scheduler_io_pwrite(VALUE scheduler, VALUE io, rb_off_t from, VALUE buffer, size_t length, size_t offset); +VALUE rb_fiber_scheduler_io_pwrite(VALUE scheduler, VALUE io, rb_off_t from, VALUE buffer, size_t offset, size_t length); /** * Non-blocking read from the passed IO using a native buffer. @@ -346,12 +347,11 @@ VALUE rb_fiber_scheduler_io_pwrite(VALUE scheduler, VALUE io, rb_off_t from, VAL * @param[in] scheduler Target scheduler. * @param[in] io An io object to read from. * @param[in] base The memory to read to. - * @param[in] size Size of the memory. - * @param[in] length The minimum number of bytes to read. + * @param[in] size The maximum number of bytes to read in one operation. * @retval RUBY_Qundef `scheduler` doesn't have `#io_read`. * @return otherwise What `scheduler.io_read` returns. */ -VALUE rb_fiber_scheduler_io_read_memory(VALUE scheduler, VALUE io, void *base, size_t size, size_t length); +VALUE rb_fiber_scheduler_io_read_memory(VALUE scheduler, VALUE io, void *base, size_t size); /** * Non-blocking write to the passed IO using a native buffer. @@ -359,12 +359,11 @@ VALUE rb_fiber_scheduler_io_read_memory(VALUE scheduler, VALUE io, void *base, s * @param[in] scheduler Target scheduler. * @param[in] io An io object to write to. * @param[in] base The memory to write from. - * @param[in] size Size of the memory. - * @param[in] length The minimum number of bytes to write. + * @param[in] size The maximum number of bytes to write in one operation. * @retval RUBY_Qundef `scheduler` doesn't have `#io_write`. * @return otherwise What `scheduler.io_write` returns. */ -VALUE rb_fiber_scheduler_io_write_memory(VALUE scheduler, VALUE io, const void *base, size_t size, size_t length); +VALUE rb_fiber_scheduler_io_write_memory(VALUE scheduler, VALUE io, const void *base, size_t size); /** * Non-blocking pread from the passed IO using a native buffer. @@ -373,12 +372,11 @@ VALUE rb_fiber_scheduler_io_write_memory(VALUE scheduler, VALUE io, const void * * @param[in] io An io object to read from. * @param[in] from The offset to read from. * @param[in] base The memory to read to. - * @param[in] size Size of the memory. - * @param[in] length The minimum number of bytes to read. + * @param[in] size The maximum number of bytes to read in one operation. * @retval RUBY_Qundef `scheduler` doesn't have `#io_read`. * @return otherwise What `scheduler.io_read` returns. */ -VALUE rb_fiber_scheduler_io_pread_memory(VALUE scheduler, VALUE io, rb_off_t from, void *base, size_t size, size_t length); +VALUE rb_fiber_scheduler_io_pread_memory(VALUE scheduler, VALUE io, rb_off_t from, void *base, size_t size); /** * Non-blocking pwrite to the passed IO using a native buffer. @@ -387,12 +385,11 @@ VALUE rb_fiber_scheduler_io_pread_memory(VALUE scheduler, VALUE io, rb_off_t fro * @param[in] io An io object to write to. * @param[in] from The offset to write from. * @param[in] base The memory to write from. - * @param[in] size Size of the memory. - * @param[in] length The minimum number of bytes to write. + * @param[in] size The maximum number of bytes to write in one operation. * @retval RUBY_Qundef `scheduler` doesn't have `#io_write`. * @return otherwise What `scheduler.io_write` returns. */ -VALUE rb_fiber_scheduler_io_pwrite_memory(VALUE scheduler, VALUE io, rb_off_t from, const void *base, size_t size, size_t length); +VALUE rb_fiber_scheduler_io_pwrite_memory(VALUE scheduler, VALUE io, rb_off_t from, const void *base, size_t size); /** * Non-blocking close the given IO. diff --git a/include/ruby/io/buffer.h b/include/ruby/io/buffer.h index 38c6bfcf3eded3..d67ec6246dd3be 100644 --- a/include/ruby/io/buffer.h +++ b/include/ruby/io/buffer.h @@ -21,7 +21,8 @@ RBIMPL_SYMBOL_EXPORT_BEGIN() // WARNING: This entire interface is experimental and may change in the future! #define RB_IO_BUFFER_EXPERIMENTAL 1 -#define RUBY_IO_BUFFER_VERSION 2 +// Version 3: IO operations use single-transfer `(offset, length)` semantics. +#define RUBY_IO_BUFFER_VERSION 3 // The `IO::Buffer` class. RUBY_EXTERN VALUE rb_cIOBuffer; @@ -107,11 +108,12 @@ VALUE rb_io_buffer_transfer(VALUE self); void rb_io_buffer_resize(VALUE self, size_t size); void rb_io_buffer_clear(VALUE self, uint8_t value, size_t offset, size_t length); -// The length is the minimum required length. -VALUE rb_io_buffer_read(VALUE self, VALUE io, size_t length, size_t offset); -VALUE rb_io_buffer_pread(VALUE self, VALUE io, rb_off_t from, size_t length, size_t offset); -VALUE rb_io_buffer_write(VALUE self, VALUE io, size_t length, size_t offset); -VALUE rb_io_buffer_pwrite(VALUE self, VALUE io, rb_off_t from, size_t length, size_t offset); +// The length is the maximum transfer length. Each function performs one +// logical IO operation and may return a short result. +VALUE rb_io_buffer_read(VALUE self, VALUE io, size_t offset, size_t length); +VALUE rb_io_buffer_pread(VALUE self, VALUE io, rb_off_t from, size_t offset, size_t length); +VALUE rb_io_buffer_write(VALUE self, VALUE io, size_t offset, size_t length); +VALUE rb_io_buffer_pwrite(VALUE self, VALUE io, rb_off_t from, size_t offset, size_t length); RBIMPL_SYMBOL_EXPORT_END() diff --git a/io.c b/io.c index f4dcc9883d2f6b..522c8da0484f66 100644 --- a/io.c +++ b/io.c @@ -1298,7 +1298,7 @@ rb_io_read_memory(rb_io_t *fptr, void *buf, size_t count) rb_thread_t *th = GET_THREAD(); VALUE scheduler = rb_fiber_scheduler_current_for_threadptr(th); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_read_memory(scheduler, fptr->self, buf, count, 0); + VALUE result = rb_fiber_scheduler_io_read_memory(scheduler, fptr->self, buf, count); if (!UNDEF_P(result)) { return rb_fiber_scheduler_io_result_apply(result); @@ -1332,7 +1332,7 @@ rb_io_write_memory(rb_io_t *fptr, const void *buf, size_t count) rb_thread_t *th = GET_THREAD(); VALUE scheduler = rb_fiber_scheduler_current_for_threadptr(th); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, buf, count, 0); + VALUE result = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, buf, count); if (!UNDEF_P(result)) { return rb_fiber_scheduler_io_result_apply(result); @@ -1371,7 +1371,7 @@ rb_writev_internal(rb_io_t *fptr, const struct iovec *iov, int iovcnt) VALUE scheduler = rb_fiber_scheduler_current_for_threadptr(th); if (scheduler != Qnil) { // This path assumes at least one `iov`: - VALUE result = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, iov[0].iov_base, iov[0].iov_len, 0); + VALUE result = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, iov[0].iov_base, iov[0].iov_len); if (!UNDEF_P(result)) { return rb_fiber_scheduler_io_result_apply(result); @@ -1425,7 +1425,7 @@ io_flush_buffer_sync(void *arg) static inline VALUE io_flush_buffer_fiber_scheduler(VALUE scheduler, rb_io_t *fptr) { - VALUE ret = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, fptr->wbuf.ptr+fptr->wbuf.off, fptr->wbuf.len, 0); + VALUE ret = rb_fiber_scheduler_io_write_memory(scheduler, fptr->self, fptr->wbuf.ptr+fptr->wbuf.off, fptr->wbuf.len); if (!UNDEF_P(ret)) { ssize_t result = rb_fiber_scheduler_io_result_apply(ret); if (result > 0) { @@ -3471,7 +3471,7 @@ io_read_memory_call(VALUE arg) VALUE scheduler = rb_fiber_scheduler_current(); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_read_memory(scheduler, iis->fptr->self, iis->buf, iis->capa, 0); + VALUE result = rb_fiber_scheduler_io_read_memory(scheduler, iis->fptr->self, iis->buf, iis->capa); if (!UNDEF_P(result)) { // This is actually returned as a pseudo-VALUE and later cast to a long: @@ -6229,7 +6229,7 @@ pread_internal_call(VALUE _arg) VALUE scheduler = rb_fiber_scheduler_current(); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_pread_memory(scheduler, arg->io->self, arg->offset, arg->buf, arg->count, 0); + VALUE result = rb_fiber_scheduler_io_pread_memory(scheduler, arg->io->self, arg->offset, arg->buf, arg->count); if (!UNDEF_P(result)) { return rb_fiber_scheduler_io_result_apply(result); @@ -6320,7 +6320,7 @@ pwrite_internal_call(VALUE _arg) VALUE scheduler = rb_fiber_scheduler_current(); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_pwrite_memory(scheduler, arg->io->self, arg->offset, arg->buf, arg->count, 0); + VALUE result = rb_fiber_scheduler_io_pwrite_memory(scheduler, arg->io->self, arg->offset, arg->buf, arg->count); if (!UNDEF_P(result)) { return rb_fiber_scheduler_io_result_apply(result); diff --git a/io_buffer.c b/io_buffer.c index 8099c23a37e6fe..8fe5db16adce8e 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -479,42 +479,10 @@ io_buffer_default_length(const struct rb_io_buffer *buffer, size_t offset) return buffer->size - offset; } -// Extract the optional length and offset arguments, returning the buffer. -// The length and offset are optional, but if they are provided, they must be -// positive integers. If the length is not provided, the default length is -// computed from the buffer size and offset. If the offset is not provided, it -// defaults to zero. -static inline struct rb_io_buffer * -io_buffer_extract_length_offset(VALUE self, int argc, VALUE argv[], size_t *length, size_t *offset) -{ - struct rb_io_buffer *buffer = get_io_buffer(self); - - if (argc >= 2 && !NIL_P(argv[1])) { - *offset = io_buffer_extract_offset(argv[1]); - } - else { - *offset = 0; - } - - if (argc >= 1 && !NIL_P(argv[0])) { - *length = io_buffer_extract_length(argv[0]); - } - else { - *length = io_buffer_default_length(buffer, *offset); - } - - return buffer; -} - // Extract the optional offset and length arguments, returning the buffer. -// Similar to `io_buffer_extract_length_offset` but with the order of arguments -// reversed. -// -// After much consideration, I decided to accept both forms. -// The `(offset, length)` order is more natural when referring about data, -// while the `(length, offset)` order is more natural when referring to -// read/write operations. In many cases, with the latter form, `offset` -// is usually not supplied. +// The offset and length are optional, but if they are provided, they must be +// positive integers. If the offset is not provided, it defaults to zero. If +// the length is not provided, it defaults to the buffer size minus the offset. static inline struct rb_io_buffer * io_buffer_extract_offset_length(VALUE self, int argc, VALUE argv[], size_t *offset, size_t *length) { @@ -3331,76 +3299,50 @@ io_buffer_blocking_region(VALUE io, struct rb_io_buffer *buffer, rb_blocking_fun struct io_buffer_read_internal_argument { // The file descriptor to read from: int descriptor; - // The base pointer to read from: + // The base pointer to read into: char *base; - // The size of the buffer: - size_t size; - // The minimum number of bytes to read: + // The maximum number of bytes to read: size_t length; }; static VALUE io_buffer_read_internal(void *_argument) { - size_t total = 0; struct io_buffer_read_internal_argument *argument = _argument; + ssize_t result = read(argument->descriptor, argument->base, argument->length); - while (true) { - ssize_t result = read(argument->descriptor, argument->base, argument->size); - - if (result < 0) { - return rb_fiber_scheduler_io_result(result, errno); - } - else if (result == 0) { - return rb_fiber_scheduler_io_result(total, 0); - } - else { - total += result; - - if (total >= argument->length) { - return rb_fiber_scheduler_io_result(total, 0); - } - - argument->base = argument->base + result; - argument->size = argument->size - result; - } - } + return rb_fiber_scheduler_io_result(result, errno); } VALUE -rb_io_buffer_read(VALUE self, VALUE io, size_t length, size_t offset) +rb_io_buffer_read(VALUE self, VALUE io, size_t offset, size_t length) { io = rb_io_get_io(io); + struct rb_io_buffer *buffer = get_io_buffer(self); + io_buffer_validate_for_writing(buffer); + io_buffer_validate_range(buffer, offset, length); + + if (length == 0) return SIZET2NUM(0); + VALUE scheduler = rb_fiber_scheduler_current(); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_read(scheduler, io, self, length, offset); + VALUE result = rb_fiber_scheduler_io_read(scheduler, io, self, offset, length); if (!UNDEF_P(result)) { return result; } } - struct rb_io_buffer *buffer = get_io_buffer(self); - - io_buffer_validate_range(buffer, offset, length); - - int descriptor = rb_io_descriptor(io); - - void * base; + void *base; size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); - size = size - offset; - if (size == 0) return SIZET2NUM(0); - RUBY_ASSERT(base != NULL); - base = (unsigned char*)base + offset; struct io_buffer_read_internal_argument argument = { - .descriptor = descriptor, - .base = base, - .size = size, + .descriptor = rb_io_descriptor(io), + .base = (char*)base + offset, .length = length, }; @@ -3408,26 +3350,22 @@ rb_io_buffer_read(VALUE self, VALUE io, size_t length, size_t offset) } /* - * call-seq: read(io, [length, [offset]]) -> read length or -errno + * call-seq: read(io, [offset, [length]]) -> read length or -errno * - * Read at least +length+ bytes from the +io+, into the buffer starting at - * +offset+. If an error occurs, return -errno. - * - * If +length+ is not given or +nil+, it defaults to the size of the buffer - * minus the offset, i.e. the entire buffer. - * - * If +length+ is zero, exactly one read operation will occur, unless - * there is no available space in the buffer at +offset+. + * Perform one read operation of at most +length+ bytes from +io+ into the + * buffer starting at +offset+. A short read is a normal result. If an error + * occurs, return -errno. * * If +offset+ is not given, it defaults to zero, i.e. the beginning of the - * buffer. + * buffer. If +length+ is not given, it defaults to the size of the buffer + * minus the offset. A zero length is a no-op. * * IO::Buffer.for('test') do |buffer| * p buffer * # => * # * # 0x00000000 74 65 73 74 test - * buffer.read(File.open('/dev/urandom', 'rb'), 2) + * buffer.read(File.open('/dev/urandom', 'rb'), 0, 2) * p buffer * # => * # @@ -3441,110 +3379,79 @@ io_buffer_read(int argc, VALUE *argv, VALUE self) VALUE io = argv[0]; - size_t length, offset; - io_buffer_extract_length_offset(self, argc-1, argv+1, &length, &offset); + size_t offset, length; + io_buffer_extract_offset_length(self, argc-1, argv+1, &offset, &length); - return rb_io_buffer_read(self, io, length, offset); + return rb_io_buffer_read(self, io, offset, length); } struct io_buffer_pread_internal_argument { // The file descriptor to read from: int descriptor; - // The base pointer to read from: + // The base pointer to read into: char *base; - // The size of the buffer: - size_t size; - // The minimum number of bytes to read: + // The maximum number of bytes to read: size_t length; - // The offset to read from: - off_t offset; + // The position to read from: + off_t from; }; static VALUE io_buffer_pread_internal(void *_argument) { - size_t total = 0; struct io_buffer_pread_internal_argument *argument = _argument; + ssize_t result = pread(argument->descriptor, argument->base, argument->length, argument->from); - while (true) { - ssize_t result = pread(argument->descriptor, argument->base, argument->size, argument->offset); - - if (result < 0) { - return rb_fiber_scheduler_io_result(result, errno); - } - else if (result == 0) { - return rb_fiber_scheduler_io_result(total, 0); - } - else { - total += result; - - if (total >= argument->length) { - return rb_fiber_scheduler_io_result(total, 0); - } - - argument->base = argument->base + result; - argument->size = argument->size - result; - argument->offset = argument->offset + result; - } - } + return rb_fiber_scheduler_io_result(result, errno); } VALUE -rb_io_buffer_pread(VALUE self, VALUE io, rb_off_t from, size_t length, size_t offset) +rb_io_buffer_pread(VALUE self, VALUE io, rb_off_t from, size_t offset, size_t length) { io = rb_io_get_io(io); + struct rb_io_buffer *buffer = get_io_buffer(self); + io_buffer_validate_for_writing(buffer); + io_buffer_validate_range(buffer, offset, length); + + if (length == 0) return SIZET2NUM(0); + VALUE scheduler = rb_fiber_scheduler_current(); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_pread(scheduler, io, from, self, length, offset); + VALUE result = rb_fiber_scheduler_io_pread(scheduler, io, from, self, offset, length); if (!UNDEF_P(result)) { return result; } } - struct rb_io_buffer *buffer = get_io_buffer(self); - - io_buffer_validate_range(buffer, offset, length); - - int descriptor = rb_io_descriptor(io); - - void * base; + void *base; size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); - size = size - offset; - if (size == 0) return SIZET2NUM(0); - RUBY_ASSERT(base != NULL); - base = (unsigned char*)base + offset; struct io_buffer_pread_internal_argument argument = { - .descriptor = descriptor, - .base = base, - .size = size, + .descriptor = rb_io_descriptor(io), + .base = (char*)base + offset, .length = length, - .offset = from, + .from = from, }; return io_buffer_blocking_region(io, buffer, io_buffer_pread_internal, &argument); } /* - * call-seq: pread(io, from, [length, [offset]]) -> read length or -errno - * - * Read at least +length+ bytes from the +io+ starting at the specified +from+ - * position, into the buffer starting at +offset+. If an error occurs, - * return -errno. + * call-seq: pread(io, from, [offset, [length]]) -> read length or -errno * - * If +length+ is not given or +nil+, it defaults to the size of the buffer - * minus the offset, i.e. the entire buffer. - * - * If +length+ is zero, exactly one pread operation will occur, - * unless there is no available space in the buffer at +offset+. + * Perform one read operation of at most +length+ bytes from +io+ at +from+ + * into the buffer starting at +offset+. A short read is a normal result and + * the IO's current position is not modified. If an error occurs, return + * -errno. * * If +offset+ is not given, it defaults to zero, i.e. the beginning of the - * buffer. + * buffer. If +length+ is not given, it defaults to the size of the buffer + * minus the offset. A zero length is a no-op. * * IO::Buffer.for('test') do |buffer| * p buffer @@ -3569,10 +3476,10 @@ io_buffer_pread(int argc, VALUE *argv, VALUE self) VALUE io = argv[0]; rb_off_t from = NUM2OFFT(argv[1]); - size_t length, offset; - io_buffer_extract_length_offset(self, argc-2, argv+2, &length, &offset); + size_t offset, length; + io_buffer_extract_offset_length(self, argc-2, argv+2, &offset, &length); - return rb_io_buffer_pread(self, io, from, length, offset); + return rb_io_buffer_pread(self, io, from, offset, length); } struct io_buffer_write_internal_argument { @@ -3580,74 +3487,47 @@ struct io_buffer_write_internal_argument { int descriptor; // The base pointer to write from: const char *base; - // The size of the buffer: - size_t size; - // The minimum length to write: + // The maximum number of bytes to write: size_t length; }; static VALUE io_buffer_write_internal(void *_argument) { - size_t total = 0; struct io_buffer_write_internal_argument *argument = _argument; + ssize_t result = write(argument->descriptor, argument->base, argument->length); - while (true) { - ssize_t result = write(argument->descriptor, argument->base, argument->size); - - if (result < 0) { - return rb_fiber_scheduler_io_result(result, errno); - } - else if (result == 0) { - return rb_fiber_scheduler_io_result(total, 0); - } - else { - total += result; - - if (total >= argument->length) { - return rb_fiber_scheduler_io_result(total, 0); - } - - argument->base = argument->base + result; - argument->size = argument->size - result; - } - } + return rb_fiber_scheduler_io_result(result, errno); } VALUE -rb_io_buffer_write(VALUE self, VALUE io, size_t length, size_t offset) +rb_io_buffer_write(VALUE self, VALUE io, size_t offset, size_t length) { io = rb_io_get_write_io(rb_io_get_io(io)); + struct rb_io_buffer *buffer = get_io_buffer(self); + io_buffer_validate_range(buffer, offset, length); + + if (length == 0) return SIZET2NUM(0); + VALUE scheduler = rb_fiber_scheduler_current(); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_write(scheduler, io, self, length, offset); + VALUE result = rb_fiber_scheduler_io_write(scheduler, io, self, offset, length); if (!UNDEF_P(result)) { return result; } } - struct rb_io_buffer *buffer = get_io_buffer(self); - - io_buffer_validate_range(buffer, offset, length); - - int descriptor = rb_io_descriptor(io); - - const void * base; + const void *base; size_t size; io_buffer_get_bytes_for_reading(buffer, &base, &size); - size = size - offset; - if (size == 0) return SIZET2NUM(0); - RUBY_ASSERT(base != NULL); - base = (const unsigned char*)base + offset; struct io_buffer_write_internal_argument argument = { - .descriptor = descriptor, - .base = base, - .size = size, + .descriptor = rb_io_descriptor(io), + .base = (const char*)base + offset, .length = length, }; @@ -3655,22 +3535,18 @@ rb_io_buffer_write(VALUE self, VALUE io, size_t length, size_t offset) } /* - * call-seq: write(io, [length, [offset]]) -> written length or -errno - * - * Write at least +length+ bytes from the buffer starting at +offset+, into the +io+. - * If an error occurs, return -errno. + * call-seq: write(io, [offset, [length]]) -> written length or -errno * - * If +length+ is not given or +nil+, it defaults to the size of the buffer - * minus the offset, i.e. the entire buffer. - * - * If +length+ is zero, exactly one write operation will occur, - * unless there are no available bytes in the buffer at +offset+. + * Perform one write operation of at most +length+ bytes to +io+ from the + * buffer starting at +offset+. A short write is a normal result. If an error + * occurs, return -errno. * * If +offset+ is not given, it defaults to zero, i.e. the beginning of the - * buffer. + * buffer. If +length+ is not given, it defaults to the size of the buffer + * minus the offset. A zero length is a no-op. * * out = File.open('output.txt', 'wb') - * IO::Buffer.for('1234567').write(out, 3) + * IO::Buffer.for('1234567').write(out, 0, 3) * * This leads to +123+ being written into output.txt */ @@ -3681,10 +3557,10 @@ io_buffer_write(int argc, VALUE *argv, VALUE self) VALUE io = argv[0]; - size_t length, offset; - io_buffer_extract_length_offset(self, argc-1, argv+1, &length, &offset); + size_t offset, length; + io_buffer_extract_offset_length(self, argc-1, argv+1, &offset, &length); - return rb_io_buffer_write(self, io, length, offset); + return rb_io_buffer_write(self, io, offset, length); } struct io_buffer_pwrite_internal_argument { @@ -3692,113 +3568,73 @@ struct io_buffer_pwrite_internal_argument { int descriptor; // The base pointer to write from: const char *base; - // The size of the buffer: - size_t size; - // The minimum length to write: + // The maximum number of bytes to write: size_t length; - // The offset to write to: - off_t offset; + // The position to write to: + off_t from; }; static VALUE io_buffer_pwrite_internal(void *_argument) { - size_t total = 0; struct io_buffer_pwrite_internal_argument *argument = _argument; + ssize_t result = pwrite(argument->descriptor, argument->base, argument->length, argument->from); - while (true) { - ssize_t result = pwrite(argument->descriptor, argument->base, argument->size, argument->offset); - - if (result < 0) { - return rb_fiber_scheduler_io_result(result, errno); - } - else if (result == 0) { - return rb_fiber_scheduler_io_result(total, 0); - } - else { - total += result; - - if (total >= argument->length) { - return rb_fiber_scheduler_io_result(total, 0); - } - - argument->base = argument->base + result; - argument->size = argument->size - result; - argument->offset = argument->offset + result; - } - } + return rb_fiber_scheduler_io_result(result, errno); } VALUE -rb_io_buffer_pwrite(VALUE self, VALUE io, rb_off_t from, size_t length, size_t offset) +rb_io_buffer_pwrite(VALUE self, VALUE io, rb_off_t from, size_t offset, size_t length) { io = rb_io_get_write_io(rb_io_get_io(io)); + struct rb_io_buffer *buffer = get_io_buffer(self); + io_buffer_validate_range(buffer, offset, length); + + if (length == 0) return SIZET2NUM(0); + VALUE scheduler = rb_fiber_scheduler_current(); if (scheduler != Qnil) { - VALUE result = rb_fiber_scheduler_io_pwrite(scheduler, io, from, self, length, offset); + VALUE result = rb_fiber_scheduler_io_pwrite(scheduler, io, from, self, offset, length); if (!UNDEF_P(result)) { return result; } } - struct rb_io_buffer *buffer = get_io_buffer(self); - - io_buffer_validate_range(buffer, offset, length); - - int descriptor = rb_io_descriptor(io); - - const void * base; + const void *base; size_t size; io_buffer_get_bytes_for_reading(buffer, &base, &size); - size = size - offset; - if (size == 0) return SIZET2NUM(0); - RUBY_ASSERT(base != NULL); - base = (const unsigned char*)base + offset; struct io_buffer_pwrite_internal_argument argument = { - .descriptor = descriptor, - - // Move the base pointer to the offset: - .base = base, - - // And the size to the length of buffer we want to read: - .size = size, - - // And the length of the buffer we want to write: + .descriptor = rb_io_descriptor(io), + .base = (const char*)base + offset, .length = length, - - // And the offset in the file we want to write from: - .offset = from, + .from = from, }; return io_buffer_blocking_region(io, buffer, io_buffer_pwrite_internal, &argument); } /* - * call-seq: pwrite(io, from, [length, [offset]]) -> written length or -errno - * - * Write at least +length+ bytes from the buffer starting at +offset+, into - * the +io+ starting at the specified +from+ position. If an error occurs, - * return -errno. + * call-seq: pwrite(io, from, [offset, [length]]) -> written length or -errno * - * If +length+ is not given or +nil+, it defaults to the size of the buffer - * minus the offset, i.e. the entire buffer. - * - * If +length+ is zero, exactly one pwrite operation will occur, - * unless there are no available bytes in the buffer at +offset+. + * Perform one write operation of at most +length+ bytes to +io+ at +from+ + * from the buffer starting at +offset+. A short write is a normal result and + * the IO's current position is not modified. If an error occurs, return + * -errno. * * If +offset+ is not given, it defaults to zero, i.e. the beginning of the - * buffer. + * buffer. If +length+ is not given, it defaults to the size of the buffer + * minus the offset. A zero length is a no-op. * * If the +from+ position is beyond the end of the file, the gap will be * filled with null (0 value) bytes. * * out = File.open('output.txt', File::RDWR) # open for read/write, no truncation - * IO::Buffer.for('1234567').pwrite(out, 2, 3, 1) + * IO::Buffer.for('1234567').pwrite(out, 2, 1, 3) * * This leads to +234+ (3 bytes, starting from position 1) being written into * output.txt, starting from file position 2. @@ -3811,10 +3647,10 @@ io_buffer_pwrite(int argc, VALUE *argv, VALUE self) VALUE io = argv[0]; rb_off_t from = NUM2OFFT(argv[1]); - size_t length, offset; - io_buffer_extract_length_offset(self, argc-2, argv+2, &length, &offset); + size_t offset, length; + io_buffer_extract_offset_length(self, argc-2, argv+2, &offset, &length); - return rb_io_buffer_pwrite(self, io, from, length, offset); + return rb_io_buffer_pwrite(self, io, from, offset, length); } static inline void @@ -4461,6 +4297,9 @@ Init_IO_Buffer(void) RUBY_IO_BUFFER_DEFAULT_SIZE = io_buffer_default_size(RUBY_IO_BUFFER_PAGE_SIZE); + /* The IO::Buffer interface version. */ + rb_define_const(rb_cIOBuffer, "VERSION", INT2NUM(RUBY_IO_BUFFER_VERSION)); + /* The operating system page size. Used for efficient page-aligned memory allocations. */ rb_define_const(rb_cIOBuffer, "PAGE_SIZE", SIZET2NUM(RUBY_IO_BUFFER_PAGE_SIZE)); diff --git a/scheduler.c b/scheduler.c index 2a41bb1d2717a8..bcf27570eea5ea 100644 --- a/scheduler.c +++ b/scheduler.c @@ -790,29 +790,24 @@ VALUE rb_fiber_scheduler_io_selectv(VALUE scheduler, int argc, VALUE *argv) /* * Document-method: Fiber::Scheduler#io_read - * call-seq: io_read(io, buffer, length, offset) -> read length or -errno + * call-seq: io_read(io, buffer, offset, length) -> read length or -errno * - * Invoked by IO#read or IO#Buffer.read to read +length+ bytes from +io+ into a - * specified +buffer+ (see IO::Buffer) at the given +offset+. + * Invoked by IO#read or IO::Buffer#read to perform one read of at most + * +length+ bytes from +io+ into a specified +buffer+ (see IO::Buffer), + * starting at the given +offset+. * - * The +length+ argument is the "minimum length to be read". If the IO buffer - * size is 8KiB, but the +length+ is +1024+ (1KiB), up to 8KiB might be read, - * but at least 1KiB will be. Generally, the only case where less data than - * +length+ will be read is if there is an error reading the data. + * A short read is returned directly. Specifying a +length+ of 0 returns 0 + * without performing a read. * - * Specifying a +length+ of 0 is valid and means try reading at least once and - * return any available data. - * - * Suggested implementation should try to read from +io+ in a non-blocking - * manner and call #io_wait if the +io+ is not ready (which will yield control - * to other fibers). + * The implementation should perform one non-blocking read and return + * -EAGAIN if +io+ is not ready. The caller can then wait and retry as + * appropriate. * * See IO::Buffer for an interface available to return data. * * Expected to return number of bytes read, or, in case of an error, * -errno (negated number corresponding to system's error code). * - * The method should be considered _experimental_. */ static VALUE fiber_scheduler_io_read(VALUE _argument) { @@ -822,14 +817,14 @@ fiber_scheduler_io_read(VALUE _argument) { } VALUE -rb_fiber_scheduler_io_read(VALUE scheduler, VALUE io, VALUE buffer, size_t length, size_t offset) +rb_fiber_scheduler_io_read(VALUE scheduler, VALUE io, VALUE buffer, size_t offset, size_t length) { if (!rb_respond_to(scheduler, id_io_read)) { return RUBY_Qundef; } VALUE arguments[] = { - scheduler, io, buffer, SIZET2NUM(length), SIZET2NUM(offset) + scheduler, io, buffer, SIZET2NUM(offset), SIZET2NUM(length) }; if (rb_respond_to(scheduler, id_fiber_interrupt)) { @@ -841,17 +836,16 @@ rb_fiber_scheduler_io_read(VALUE scheduler, VALUE io, VALUE buffer, size_t lengt /* * Document-method: Fiber::Scheduler#io_pread - * call-seq: io_pread(io, buffer, from, length, offset) -> read length or -errno + * call-seq: io_pread(io, buffer, from, offset, length) -> read length or -errno * - * Invoked by IO#pread or IO::Buffer#pread to read +length+ bytes from +io+ - * at offset +from+ into a specified +buffer+ (see IO::Buffer) at the given - * +offset+. + * Invoked by IO#pread or IO::Buffer#pread to perform one read of at most + * +length+ bytes from +io+ at offset +from+ into a specified +buffer+ (see + * IO::Buffer), starting at the given +offset+. * * This method is semantically the same as #io_read, but it allows to specify * the offset to read from and is often better for asynchronous IO on the same * file. * - * The method should be considered _experimental_. */ static VALUE fiber_scheduler_io_pread(VALUE _argument) { @@ -861,14 +855,14 @@ fiber_scheduler_io_pread(VALUE _argument) { } VALUE -rb_fiber_scheduler_io_pread(VALUE scheduler, VALUE io, rb_off_t from, VALUE buffer, size_t length, size_t offset) +rb_fiber_scheduler_io_pread(VALUE scheduler, VALUE io, rb_off_t from, VALUE buffer, size_t offset, size_t length) { if (!rb_respond_to(scheduler, id_io_pread)) { return RUBY_Qundef; } VALUE arguments[] = { - scheduler, io, buffer, OFFT2NUM(from), SIZET2NUM(length), SIZET2NUM(offset) + scheduler, io, buffer, OFFT2NUM(from), SIZET2NUM(offset), SIZET2NUM(length) }; if (rb_respond_to(scheduler, id_fiber_interrupt)) { @@ -880,23 +874,18 @@ rb_fiber_scheduler_io_pread(VALUE scheduler, VALUE io, rb_off_t from, VALUE buff /* * Document-method: Fiber::Scheduler#io_write - * call-seq: io_write(io, buffer, length, offset) -> written length or -errno - * - * Invoked by IO#write or IO::Buffer#write to write +length+ bytes to +io+ from - * from a specified +buffer+ (see IO::Buffer) at the given +offset+. + * call-seq: io_write(io, buffer, offset, length) -> written length or -errno * - * The +length+ argument is the "minimum length to be written". If the IO - * buffer size is 8KiB, but the +length+ specified is 1024 (1KiB), at most 8KiB - * will be written, but at least 1KiB will be. Generally, the only case where - * less data than +length+ will be written is if there is an error writing the - * data. + * Invoked by IO#write or IO::Buffer#write to perform one write of at most + * +length+ bytes to +io+ from a specified +buffer+ (see IO::Buffer), starting + * at the given +offset+. * - * Specifying a +length+ of 0 is valid and means try writing at least once, as - * much data as possible. + * A short write is returned directly. Specifying a +length+ of 0 returns 0 + * without performing a write. * - * Suggested implementation should try to write to +io+ in a non-blocking - * manner and call #io_wait if the +io+ is not ready (which will yield control - * to other fibers). + * The implementation should perform one non-blocking write and return + * -EAGAIN if +io+ is not ready. The caller can then wait and retry as + * appropriate. * * See IO::Buffer for an interface available to get data from buffer * efficiently. @@ -904,7 +893,6 @@ rb_fiber_scheduler_io_pread(VALUE scheduler, VALUE io, rb_off_t from, VALUE buff * Expected to return number of bytes written, or, in case of an error, * -errno (negated number corresponding to system's error code). * - * The method should be considered _experimental_. */ static VALUE fiber_scheduler_io_write(VALUE _argument) { @@ -914,14 +902,14 @@ fiber_scheduler_io_write(VALUE _argument) { } VALUE -rb_fiber_scheduler_io_write(VALUE scheduler, VALUE io, VALUE buffer, size_t length, size_t offset) +rb_fiber_scheduler_io_write(VALUE scheduler, VALUE io, VALUE buffer, size_t offset, size_t length) { if (!rb_respond_to(scheduler, id_io_write)) { return RUBY_Qundef; } VALUE arguments[] = { - scheduler, io, buffer, SIZET2NUM(length), SIZET2NUM(offset) + scheduler, io, buffer, SIZET2NUM(offset), SIZET2NUM(length) }; if (rb_respond_to(scheduler, id_fiber_interrupt)) { @@ -933,18 +921,16 @@ rb_fiber_scheduler_io_write(VALUE scheduler, VALUE io, VALUE buffer, size_t leng /* * Document-method: Fiber::Scheduler#io_pwrite - * call-seq: io_pwrite(io, buffer, from, length, offset) -> written length or -errno + * call-seq: io_pwrite(io, buffer, from, offset, length) -> written length or -errno * - * Invoked by IO#pwrite or IO::Buffer#pwrite to write +length+ bytes to +io+ - * at offset +from+ into a specified +buffer+ (see IO::Buffer) at the given - * +offset+. + * Invoked by IO#pwrite or IO::Buffer#pwrite to perform one write of at most + * +length+ bytes to +io+ at offset +from+ from a specified +buffer+ (see + * IO::Buffer), starting at the given +offset+. * * This method is semantically the same as #io_write, but it allows to specify * the offset to write to and is often better for asynchronous IO on the same * file. * - * The method should be considered _experimental_. - * */ static VALUE fiber_scheduler_io_pwrite(VALUE _argument) { @@ -954,7 +940,7 @@ fiber_scheduler_io_pwrite(VALUE _argument) { } VALUE -rb_fiber_scheduler_io_pwrite(VALUE scheduler, VALUE io, rb_off_t from, VALUE buffer, size_t length, size_t offset) +rb_fiber_scheduler_io_pwrite(VALUE scheduler, VALUE io, rb_off_t from, VALUE buffer, size_t offset, size_t length) { @@ -963,7 +949,7 @@ rb_fiber_scheduler_io_pwrite(VALUE scheduler, VALUE io, rb_off_t from, VALUE buf } VALUE arguments[] = { - scheduler, io, buffer, OFFT2NUM(from), SIZET2NUM(length), SIZET2NUM(offset) + scheduler, io, buffer, OFFT2NUM(from), SIZET2NUM(offset), SIZET2NUM(length) }; if (rb_respond_to(scheduler, id_fiber_interrupt)) { @@ -974,11 +960,11 @@ rb_fiber_scheduler_io_pwrite(VALUE scheduler, VALUE io, rb_off_t from, VALUE buf } VALUE -rb_fiber_scheduler_io_read_memory(VALUE scheduler, VALUE io, void *base, size_t size, size_t length) +rb_fiber_scheduler_io_read_memory(VALUE scheduler, VALUE io, void *base, size_t size) { VALUE buffer = rb_io_buffer_new_locked(base, size, 0); - VALUE result = rb_fiber_scheduler_io_read(scheduler, io, buffer, length, 0); + VALUE result = rb_fiber_scheduler_io_read(scheduler, io, buffer, 0, size); rb_io_buffer_free_locked(buffer); @@ -986,11 +972,11 @@ rb_fiber_scheduler_io_read_memory(VALUE scheduler, VALUE io, void *base, size_t } VALUE -rb_fiber_scheduler_io_write_memory(VALUE scheduler, VALUE io, const void *base, size_t size, size_t length) +rb_fiber_scheduler_io_write_memory(VALUE scheduler, VALUE io, const void *base, size_t size) { VALUE buffer = rb_io_buffer_new_locked((void*)base, size, RB_IO_BUFFER_READONLY); - VALUE result = rb_fiber_scheduler_io_write(scheduler, io, buffer, length, 0); + VALUE result = rb_fiber_scheduler_io_write(scheduler, io, buffer, 0, size); rb_io_buffer_free_locked(buffer); @@ -998,11 +984,11 @@ rb_fiber_scheduler_io_write_memory(VALUE scheduler, VALUE io, const void *base, } VALUE -rb_fiber_scheduler_io_pread_memory(VALUE scheduler, VALUE io, rb_off_t from, void *base, size_t size, size_t length) +rb_fiber_scheduler_io_pread_memory(VALUE scheduler, VALUE io, rb_off_t from, void *base, size_t size) { VALUE buffer = rb_io_buffer_new_locked(base, size, 0); - VALUE result = rb_fiber_scheduler_io_pread(scheduler, io, from, buffer, length, 0); + VALUE result = rb_fiber_scheduler_io_pread(scheduler, io, from, buffer, 0, size); rb_io_buffer_free_locked(buffer); @@ -1010,11 +996,11 @@ rb_fiber_scheduler_io_pread_memory(VALUE scheduler, VALUE io, rb_off_t from, voi } VALUE -rb_fiber_scheduler_io_pwrite_memory(VALUE scheduler, VALUE io, rb_off_t from, const void *base, size_t size, size_t length) +rb_fiber_scheduler_io_pwrite_memory(VALUE scheduler, VALUE io, rb_off_t from, const void *base, size_t size) { VALUE buffer = rb_io_buffer_new_locked((void*)base, size, RB_IO_BUFFER_READONLY); - VALUE result = rb_fiber_scheduler_io_pwrite(scheduler, io, from, buffer, length, 0); + VALUE result = rb_fiber_scheduler_io_pwrite(scheduler, io, from, buffer, 0, size); rb_io_buffer_free_locked(buffer); diff --git a/test/fiber/scheduler.rb b/test/fiber/scheduler.rb index 8f1ce4376b2c29..7158eadec74a33 100644 --- a/test/fiber/scheduler.rb +++ b/test/fiber/scheduler.rb @@ -371,116 +371,28 @@ def blocking_operation_wait(work) # This scheduler class implements `io_read` and `io_write` hooks which require # `IO::Buffer`. class IOBufferScheduler < Scheduler - EAGAIN = -Errno::EAGAIN::Errno - - def io_read(io, buffer, length, offset) - total = 0 + def io_read(io, buffer, offset, length) io.nonblock = true - while true - result = blocking{buffer.read(io, 0, offset)} - - if result > 0 - total += result - offset += result - break if total >= length - elsif result == 0 - break - elsif result == EAGAIN - if length > 0 - self.io_wait(io, IO::READABLE, nil) - else - return result - end - elsif result < 0 - return result - end - end - - return total + blocking{buffer.read(io, offset, length)} end - def io_write(io, buffer, length, offset) - total = 0 + def io_write(io, buffer, offset, length) io.nonblock = true - while true - result = blocking{buffer.write(io, 0, offset)} - - if result > 0 - total += result - offset += result - break if total >= length - elsif result == 0 - break - elsif result == EAGAIN - if length > 0 - self.io_wait(io, IO::WRITABLE, nil) - else - return result - end - elsif result < 0 - return result - end - end - - return total + blocking{buffer.write(io, offset, length)} end - def io_pread(io, buffer, from, length, offset) - total = 0 + def io_pread(io, buffer, from, offset, length) io.nonblock = true - while true - result = blocking{buffer.pread(io, from, 0, offset)} - - if result > 0 - total += result - offset += result - from += result - break if total >= length - elsif result == 0 - break - elsif result == EAGAIN - if length > 0 - self.io_wait(io, IO::READABLE, nil) - else - return result - end - elsif result < 0 - return result - end - end - - return total + blocking{buffer.pread(io, from, offset, length)} end - def io_pwrite(io, buffer, from, length, offset) - total = 0 + def io_pwrite(io, buffer, from, offset, length) io.nonblock = true - while true - result = blocking{buffer.pwrite(io, from, 0, offset)} - - if result > 0 - total += result - offset += result - from += result - break if total >= length - elsif result == 0 - break - elsif result == EAGAIN - if length > 0 - self.io_wait(io, IO::WRITABLE, nil) - else - return result - end - elsif result < 0 - return result - end - end - - return total + blocking{buffer.pwrite(io, from, offset, length)} end def blocking(&block) @@ -493,24 +405,24 @@ def operations @operations ||= [] end - def io_write(io, buffer, length, offset) + def io_write(io, buffer, offset, length) descriptor = io.fileno - string = buffer.get_string + string = buffer.get_string(offset, length) self.operations << [:io_write, descriptor, string] Fiber.blocking do - buffer.write(io, 0, offset) + buffer.write(io, offset, length) end end end class IOErrorScheduler < Scheduler - def io_read(io, buffer, length, offset) + def io_read(io, buffer, offset, length) return -Errno::EBADF::Errno end - def io_write(io, buffer, length, offset) + def io_write(io, buffer, offset, length) return -Errno::EINVAL::Errno end end diff --git a/test/fiber/test_io_buffer.rb b/test/fiber/test_io_buffer.rb index 19e6c1f88e976d..d52baaf6c7f382 100644 --- a/test/fiber/test_io_buffer.rb +++ b/test/fiber/test_io_buffer.rb @@ -132,11 +132,11 @@ def test_io_buffer_read_write destination_buffer = IO::Buffer.new(source_buffer.size) # Test non-scheduler code path: - source_buffer.write(o, source_buffer.size) - destination_buffer.read(i, source_buffer.size) + source_buffer.write(o, 0, source_buffer.size) + destination_buffer.read(i, 0, source_buffer.size) assert_equal source_buffer, destination_buffer - # Test scheduler code path: + # Test with a scheduler installed: destination_buffer.clear thread = Thread.new do @@ -144,8 +144,8 @@ def test_io_buffer_read_write Fiber.set_scheduler scheduler Fiber.schedule do - source_buffer.write(o, source_buffer.size) - destination_buffer.read(i, source_buffer.size) + source_buffer.write(o, 0, source_buffer.size) + destination_buffer.read(i, 0, source_buffer.size) end end @@ -157,6 +157,35 @@ def test_io_buffer_read_write o&.close end + def test_io_buffer_read_write_offset_and_length + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + i, o = UNIXSocket.pair + source_buffer = IO::Buffer.for("xHELLOy") + destination_buffer = IO::Buffer.new(9) + written = read = nil + + thread = Thread.new do + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + written = source_buffer.write(o, 1, 5) + o.close_write + read = destination_buffer.read(i, 2, 5) + end + end + + thread.join + + assert_equal 5, written + assert_equal 5, read + assert_equal "HELLO", destination_buffer.get_string(2, 5) + ensure + i&.close + o&.close + end + def nonblockable?(io) io.nonblock{} true @@ -173,11 +202,11 @@ def test_io_buffer_pread_pwrite destination_buffer = IO::Buffer.new(source_buffer.size) # Test non-scheduler code path: - source_buffer.pwrite(file, 1, source_buffer.size) - destination_buffer.pread(file, 1, source_buffer.size) + source_buffer.pwrite(file, 1, 0, source_buffer.size) + destination_buffer.pread(file, 1, 0, source_buffer.size) assert_equal source_buffer, destination_buffer - # Test scheduler code path: + # Test with a scheduler installed: destination_buffer.clear file.truncate(0) @@ -186,8 +215,8 @@ def test_io_buffer_pread_pwrite Fiber.set_scheduler scheduler Fiber.schedule do - source_buffer.pwrite(file, 1, source_buffer.size) - destination_buffer.pread(file, 1, source_buffer.size) + source_buffer.pwrite(file, 1, 0, source_buffer.size) + destination_buffer.pread(file, 1, 0, source_buffer.size) end end @@ -197,4 +226,32 @@ def test_io_buffer_pread_pwrite ensure file&.close! end + + def test_io_buffer_pread_pwrite_from_offset_and_length + file = Tempfile.new("test_io_buffer_pread_pwrite_from_offset_and_length") + + omit "Non-blocking file IO is not supported" unless nonblockable?(file) + + source_buffer = IO::Buffer.for("xHELLOy") + destination_buffer = IO::Buffer.new(9) + written = read = nil + + thread = Thread.new do + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + written = source_buffer.pwrite(file, 3, 1, 5) + read = destination_buffer.pread(file, 3, 2, 5) + end + end + + thread.join + + assert_equal 5, written + assert_equal 5, read + assert_equal "HELLO", destination_buffer.get_string(2, 5) + ensure + file&.close! + end end diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 6e0c858fff52a6..25c52892d44df5 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -23,6 +23,10 @@ def assert_positive(value) assert(value > 0, "Expected #{value} to be positive!") end + def test_version + assert_equal 3, IO::Buffer::VERSION + end + def test_flags assert_equal 1, IO::Buffer::EXTERNAL assert_equal 2, IO::Buffer::INTERNAL @@ -1129,7 +1133,7 @@ def test_overlapping_reads_retain_independent_locks input2.nonblock = false buffer = IO::Buffer.new(2) - thread1 = Thread.new {buffer.read(input1, 1, 0)} + thread1 = Thread.new {buffer.read(input1, 0, 1)} thread2 = Thread.new {buffer.read(input2, 1, 1)} Thread.pass until thread1.stop? && thread2.stop? @@ -1178,18 +1182,31 @@ def test_read end end - def test_read_with_with_length + def test_read_with_length hello_world_tempfile do |io| buffer = IO::Buffer.new(128) - buffer.read(io, 5) + buffer.read(io, 0, 5) assert_equal "Hello", buffer.get_string(0, 5) end end - def test_read_with_with_offset + def test_read_returns_short_result + input, output = IO.pipe + input.nonblock = true + output.write("abc") + + buffer = IO::Buffer.new(5) + assert_equal 3, buffer.read(input, 0, 5) + assert_equal "abc", buffer.get_string(0, 3) + ensure + input&.close + output&.close + end + + def test_read_with_offset hello_world_tempfile do |io| buffer = IO::Buffer.new(128) - buffer.read(io, nil, 6) + buffer.read(io, 6) assert_equal "Hello", buffer.get_string(6, 5) end end @@ -1198,7 +1215,7 @@ def test_read_with_length_and_offset hello_world_tempfile(100) do |io| buffer = IO::Buffer.new(1024) # Only read 24 bytes from the file, as we are starting at offset 1000 in the buffer. - assert_equal 24, buffer.read(io, 0, 1000) + assert_equal 24, buffer.read(io, 1000) assert_equal "Hello World", buffer.get_string(1000, 11) end end @@ -1221,7 +1238,7 @@ def test_write_with_length_and_offset buffer = IO::Buffer.new(5) buffer.set_string("Hello") - buffer.write(io, 4, 1) + buffer.write(io, 1, 4) io.seek(0) assert_equal "ello", io.read(4) @@ -1233,10 +1250,10 @@ def test_zero_length_io io = Tempfile.new assert_zero_length_io = proc do |buffer, offset = 0| - assert_equal 0, buffer.read(io, 0, offset) - assert_equal 0, buffer.pread(io, 0, 0, offset) - assert_equal 0, buffer.write(io, 0, offset) - assert_equal 0, buffer.pwrite(io, 0, 0, offset) + assert_equal 0, buffer.read(io, offset, 0) + assert_equal 0, buffer.pread(io, 0, offset, 0) + assert_equal 0, buffer.write(io, offset, 0) + assert_equal 0, buffer.pwrite(io, 0, offset, 0) end buffer = IO::Buffer.new(0) @@ -1260,7 +1277,7 @@ def test_pread io.seek(0) buffer = IO::Buffer.new(128) - buffer.pread(io, 6, 5) + buffer.pread(io, 6, 0, 5) assert_equal "World", buffer.get_string(0, 5) assert_equal 0, io.tell @@ -1274,7 +1291,7 @@ def test_pread_offset io.seek(0) buffer = IO::Buffer.new(128) - buffer.pread(io, 6, 5, 6) + buffer.pread(io, 6, 6, 5) assert_equal "World", buffer.get_string(6, 5) assert_equal 0, io.tell @@ -1287,7 +1304,7 @@ def test_pwrite buffer = IO::Buffer.new(128) buffer.set_string("World") - buffer.pwrite(io, 6, 5) + buffer.pwrite(io, 6, 0, 5) assert_equal 0, io.tell @@ -1302,7 +1319,7 @@ def test_pwrite_offset buffer = IO::Buffer.new(128) buffer.set_string("Hello World") - buffer.pwrite(io, 6, 5, 6) + buffer.pwrite(io, 6, 6, 5) assert_equal 0, io.tell From f4cba14aa62f2cd5b98ad4d4a986617421d6cb07 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 25 Aug 2026 11:04:24 +0900 Subject: [PATCH 05/10] [ruby/rubygems] Truncate atomic write temp file names at character boundaries The basename was cut with byteslice, which can split a multibyte character in half. Filesystems enforcing UTF-8 validity such as APFS then reject the temporary name with EILSEQ. https://github.com/ruby/rubygems/commit/a7dc5647b0 Co-Authored-By: Claude Fable 5 --- lib/rubygems/util/atomic_file_writer.rb | 18 ++++- .../test_gem_util_atomic_file_writer.rb | 75 ++++++++++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb index 9af6d54b549843..63ea5ccf5988f9 100644 --- a/lib/rubygems/util/atomic_file_writer.rb +++ b/lib/rubygems/util/atomic_file_writer.rb @@ -24,7 +24,7 @@ def self.open(file_name) tmp_suffix = ".tmp.#{SecureRandom.hex}" dirname = File.dirname(file_name) basename = File.basename(file_name) - base_slice = basename.byteslice(0, 254 - tmp_suffix.bytesize) + base_slice = byteslice_at_char_boundary(basename, 254 - tmp_suffix.bytesize) tmp_path = File.join(dirname, ".#{base_slice}#{tmp_suffix}") # The temporary name is longer than the final one, so on Windows a @@ -38,7 +38,7 @@ def self.open(file_name) trim = [tmp_suffix.bytesize - (".tmp.".bytesize + 8), overflow].min tmp_suffix = tmp_suffix.byteslice(0, tmp_suffix.bytesize - trim) overflow -= trim - base_slice = base_slice.byteslice(0, [base_slice.bytesize - overflow, 0].max) if overflow > 0 + base_slice = byteslice_at_char_boundary(base_slice, [base_slice.bytesize - overflow, 0].max) if overflow > 0 tmp_path = File.join(dirname, ".#{base_slice}#{tmp_suffix}") end @@ -88,5 +88,19 @@ def self.open(file_name) return_val end end + + # Returns the longest prefix of string that is at most max_bytesize bytes + # and ends on a character boundary. A string that is invalid in its own + # encoding would be cut back to its last valid prefix, discarding an + # unbounded part of the name, so it is sliced as raw bytes instead and can + # still be cut mid-character. + def self.byteslice_at_char_boundary(string, max_bytesize) + sliced = string.byteslice(0, max_bytesize) + if string.valid_encoding? + sliced = sliced.byteslice(0, sliced.bytesize - 1) until sliced.valid_encoding? + end + sliced + end + private_class_method :byteslice_at_char_boundary end end diff --git a/test/rubygems/test_gem_util_atomic_file_writer.rb b/test/rubygems/test_gem_util_atomic_file_writer.rb index e011a38ad42a04..40b5c4600072ed 100644 --- a/test/rubygems/test_gem_util_atomic_file_writer.rb +++ b/test/rubygems/test_gem_util_atomic_file_writer.rb @@ -4,9 +4,82 @@ require "rubygems/util/atomic_file_writer" class TestGemUtilAtomicFileWriter < Gem::TestCase + def setup + super + + @dir = File.join @tempdir, "atomic" + Dir.mkdir @dir + @path = File.join @dir, "out.txt" + end + def test_external_encoding - Gem::AtomicFileWriter.open(File.join(@tempdir, "test.txt")) do |file| + Gem::AtomicFileWriter.open(@path) do |file| assert_equal(Encoding::ASCII_8BIT, file.external_encoding) end end + + def test_multibyte_basename_is_truncated_at_char_boundary + pend "long file names easily exceed MAX_PATH on Windows" if Gem.win_platform? + + path = File.join @dir, "#{"あ" * 82}.txt" + original_rename = File.method(:rename) + tmp_basename = nil + + rename_spy = lambda do |src, dest| + tmp_basename ||= File.basename(src) + original_rename.call(src, dest) + end + + # Filesystems that store names as raw bytes, such as ext4, happily create a + # temporary file whose name is cut in the middle of a character, so assert on + # the name itself rather than on the destination write failing. + File.stub(:rename, rename_spy) do + Gem::AtomicFileWriter.open(path) do |file| + file.write "hello" + end + end + + assert_predicate tmp_basename, :valid_encoding? + assert_operator tmp_basename.bytesize, :<=, 255 + assert_equal "hello", File.binread(path) + assert_equal 1, Dir.children(@dir).size + end + + def test_long_basename_is_truncated_to_the_name_length_limit + pend "long file names easily exceed MAX_PATH on Windows" if Gem.win_platform? + + path = File.join @dir, "#{"a" * 250}.txt" + original_rename = File.method(:rename) + tmp_basename = nil + + rename_spy = lambda do |src, dest| + tmp_basename ||= File.basename(src) + original_rename.call(src, dest) + end + + File.stub(:rename, rename_spy) do + Gem::AtomicFileWriter.open(path) do |file| + file.write "hello" + end + end + + assert_equal 255, tmp_basename.bytesize + assert_equal "hello", File.binread(path) + end + + def test_byteslice_at_char_boundary + sliced = Gem::AtomicFileWriter.send :byteslice_at_char_boundary, "あいう", 4 + + assert_equal "あ", sliced + assert_predicate sliced, :valid_encoding? + end + + def test_byteslice_at_char_boundary_with_invalid_encoding + invalid = "\xFFabc".dup.force_encoding(Encoding::UTF_8) + refute_predicate invalid, :valid_encoding? + + sliced = Gem::AtomicFileWriter.send :byteslice_at_char_boundary, invalid, 2 + + assert_equal "\xFFa".dup.force_encoding(Encoding::UTF_8), sliced + end end From f2b2cfacaf4955f0ae0ae3c95c895e9c4c636a1f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 25 Aug 2026 11:04:34 +0900 Subject: [PATCH 06/10] [ruby/rubygems] Flush atomic write temp files before renaming over the destination The temporary file was renamed into place before being closed, so buffered data had not reached the filesystem yet and a concurrent reader could observe an empty destination. A flush failure such as ENOSPC would surface only after the destination had already been replaced. Cleanup also moved to an ensure block, so an interrupt no longer leaks the temporary file. https://github.com/ruby/rubygems/commit/774bd08837 Co-Authored-By: Claude Fable 5 --- lib/rubygems/util/atomic_file_writer.rb | 51 +++--- .../test_gem_util_atomic_file_writer.rb | 172 ++++++++++++++++++ 2 files changed, 199 insertions(+), 24 deletions(-) diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb index 63ea5ccf5988f9..be9bf76e83b87a 100644 --- a/lib/rubygems/util/atomic_file_writer.rb +++ b/lib/rubygems/util/atomic_file_writer.rb @@ -45,47 +45,50 @@ def self.open(file_name) flags = File::RDWR | File::CREAT | File::EXCL | File::BINARY flags |= File::SHARE_DELETE if defined?(File::SHARE_DELETE) - File.open(tmp_path, flags) do |temp_file| + renamed = false + temp_file = File.open(tmp_path, flags) + + begin temp_file.binmode if old_stat # Set correct permissions on new file begin - File.chown(old_stat.uid, old_stat.gid, temp_file.path) + File.chown(old_stat.uid, old_stat.gid, tmp_path) # This operation will affect filesystem ACL's - File.chmod(old_stat.mode, temp_file.path) + File.chmod(old_stat.mode, tmp_path) rescue Errno::EPERM, Errno::EACCES # Changing file ownership failed, moving on. end end return_val = yield temp_file - rescue StandardError => error - begin - temp_file.close - rescue StandardError - nil - end - begin - File.unlink(temp_file.path) - rescue StandardError - nil - end + # Any data still buffered is handed to the filesystem on close, so a + # failing flush must surface while the destination is still intact. + # That means closing the temporary file before the rename. Note this + # does not fsync, so the write is atomic but not crash durable. + temp_file.close + File.rename(tmp_path, file_name) + renamed = true - raise error - else - begin - File.rename(temp_file.path, file_name) - rescue StandardError + return_val + ensure + unless renamed begin - File.unlink(temp_file.path) + temp_file.close rescue StandardError + nil + ensure + # The unlink runs from an ensure so that a non-StandardError raised by + # the close above, a second Ctrl-C for instance, still reaches it. An + # interrupt landing on the unlink itself is not covered. + begin + File.unlink(tmp_path) + rescue StandardError + nil + end end - - raise end - - return_val end end diff --git a/test/rubygems/test_gem_util_atomic_file_writer.rb b/test/rubygems/test_gem_util_atomic_file_writer.rb index 40b5c4600072ed..0e9e23f22e31ed 100644 --- a/test/rubygems/test_gem_util_atomic_file_writer.rb +++ b/test/rubygems/test_gem_util_atomic_file_writer.rb @@ -18,6 +18,151 @@ def test_external_encoding end end + def test_returns_block_value_and_leaves_no_temp_file + result = Gem::AtomicFileWriter.open(@path) do |file| + file.write "hello" + :done + end + + assert_equal :done, result + assert_equal "hello", File.binread(@path) + assert_equal ["out.txt"], Dir.children(@dir) + end + + def test_content_is_flushed_before_rename + original_rename = File.method(:rename) + size_at_rename = nil + + rename_spy = lambda do |src, dest| + size_at_rename = File.size(src) + original_rename.call(src, dest) + end + + File.stub(:rename, rename_spy) do + Gem::AtomicFileWriter.open(@path) do |file| + file.write "hello" + end + end + + assert_equal 5, size_at_rename + assert_equal "hello", File.binread(@path) + end + + def test_keeps_destination_and_removes_temp_file_on_error + File.binwrite @path, "old" + + error = assert_raise(RuntimeError) do + Gem::AtomicFileWriter.open(@path) do |file| + file.write "new" + raise "boom" + end + end + + assert_equal "boom", error.message + assert_equal "old", File.binread(@path) + assert_equal ["out.txt"], Dir.children(@dir) + end + + def test_keeps_destination_and_removes_temp_file_on_interrupt + File.binwrite @path, "old" + + assert_raise(Interrupt) do + Gem::AtomicFileWriter.open(@path) do |file| + file.write "new" + raise Interrupt + end + end + + assert_equal "old", File.binread(@path) + assert_equal ["out.txt"], Dir.children(@dir) + end + + def test_removes_temp_file_when_closing_it_keeps_raising + File.binwrite @path, "old" + + temp_file = nil + closes = 0 + raising = true + # A real file is closed even when its close raises, so only a stub can keep + # raising like this. The writer leaves that file to the garbage collector, + # which is why this test closes it itself. + failing_open = temp_file_open_stub do |file| + temp_file = file + file.define_singleton_method(:close) do + closes += 1 + raise Interrupt if raising + + super() + end + end + + File.stub(:open, failing_open) do + assert_raise(Interrupt) do + Gem::AtomicFileWriter.open(@path) do |file| + file.write "new" + end + end + end + + assert_equal 2, closes + assert_equal "old", File.binread(@path) + assert_equal ["out.txt"], Dir.children(@dir) + ensure + raising = false + temp_file&.close + end + + def test_keeps_destination_when_closing_the_temp_file_fails + File.binwrite @path, "old" + + failing_open = temp_file_open_stub do |file| + file.define_singleton_method(:close) do + super() + raise Errno::ENOSPC + end + end + + File.stub(:open, failing_open) do + assert_raise(Errno::ENOSPC) do + Gem::AtomicFileWriter.open(@path) do |file| + file.write "new" + end + end + end + + assert_equal "old", File.binread(@path) + assert_equal ["out.txt"], Dir.children(@dir) + end + + def test_keeps_destination_when_renaming_fails + File.binwrite @path, "old" + + File.stub(:rename, ->(_src, _dest) { raise Errno::EXDEV }) do + assert_raise(Errno::EXDEV) do + Gem::AtomicFileWriter.open(@path) do |file| + file.write "new" + end + end + end + + assert_equal "old", File.binread(@path) + assert_equal ["out.txt"], Dir.children(@dir) + end + + def test_preserves_destination_permissions + pend "Windows cannot round-trip the POSIX permission bits" if Gem.win_platform? + + File.binwrite @path, "old" + File.chmod 0o604, @path + + Gem::AtomicFileWriter.open(@path) do |file| + file.write "new" + end + + assert_equal "new", File.binread(@path) + assert_equal 0o604, File.stat(@path).mode & 0o777 + end + def test_multibyte_basename_is_truncated_at_char_boundary pend "long file names easily exceed MAX_PATH on Windows" if Gem.win_platform? @@ -82,4 +227,31 @@ def test_byteslice_at_char_boundary_with_invalid_encoding assert_equal "\xFFa".dup.force_encoding(Encoding::UTF_8), sliced end + + private + + # Returns a File.open replacement that hands the writer's temporary file to + # the given block, which installs whatever close behaviour a test needs. + # Every other path is opened normally. Call this before installing the stub, + # since it captures the current File.open. + def temp_file_open_stub(&injection) + original_open = File.method(:open) + prefix = ".#{File.basename(@path)}.tmp." + + lambda do |name, *args, **kwargs, &block| + unless File.basename(name.to_s).start_with?(prefix) + next original_open.call(name, *args, **kwargs, &block) + end + raise ArgumentError, "this stub cannot replace close on a File.open with a block" if block + + file = original_open.call(name, *args, **kwargs) + begin + injection.call(file) + rescue StandardError + file.close + raise + end + file + end + end end From 46c287e2ca39da63d5164e2b7227308a41554c9a Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Tue, 25 Aug 2026 03:24:01 +0900 Subject: [PATCH 07/10] Fix splicing an array that shares its buffer with a longer array Arrays sharing one buffer have the same data pointer, so rb_ary_splice() took `a` in `b = a.dup; b.pop; b.concat(a)` for `b` itself and rebased the source onto `b`'s shorter storage, copying uninitialized memory. Have the callers tell whether the replacement really is `ary`. [Bug #22259] Co-Authored-By: Claude Opus 5 (1M context) --- array.c | 23 +++++------- test/ruby/test_array.rb | 77 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/array.c b/array.c index d1453c19fe7f95..10bf936e1424b7 100644 --- a/array.c +++ b/array.c @@ -2307,10 +2307,9 @@ rb_ary_to_ary(VALUE obj) } static void -rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) +rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen, int self_insert) { long olen; - long rofs; if (len < 0) rb_raise(rb_eIndexError, "negative length (%ld)", len); olen = RARRAY_LEN(ary); @@ -2325,11 +2324,6 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) len = olen - beg; } - { - const VALUE *optr = RARRAY_CONST_PTR(ary); - rofs = (rptr >= optr && rptr < optr + olen) ? rptr - optr : -1; - } - if (beg >= olen) { VALUE target_ary; if (beg > ARY_MAX_SIZE - rlen) { @@ -2339,7 +2333,8 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) len = beg + rlen; ary_mem_clear(ary, olen, beg - olen); if (rlen > 0) { - if (rofs != -1) rptr = RARRAY_CONST_PTR(ary) + rofs; + /* ary's storage may have moved; only ary itself needs re-deriving. */ + if (self_insert) rptr = RARRAY_CONST_PTR(ary); ary_memcpy0(ary, beg, rlen, rptr, target_ary); } ARY_SET_LEN(ary, len); @@ -2363,13 +2358,13 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) ARY_SET_LEN(ary, alen); } if (rlen > 0) { - if (rofs == -1) { + if (!self_insert) { rb_gc_writebarrier_remember(ary); } else { /* In this case, we're copying from a region in this array, so * we don't need to fire the write barrier. */ - rptr = RARRAY_CONST_PTR(ary) + rofs; + rptr = RARRAY_CONST_PTR(ary); } /* do not use RARRAY_PTR() because it can causes GC. @@ -2469,7 +2464,7 @@ static VALUE ary_aset_by_rb_ary_splice(VALUE ary, long beg, long len, VALUE val) { VALUE rpl = rb_ary_to_ary(val); - rb_ary_splice(ary, beg, len, RARRAY_CONST_PTR(rpl), RARRAY_LEN(rpl)); + rb_ary_splice(ary, beg, len, RARRAY_CONST_PTR(rpl), RARRAY_LEN(rpl), ary == rpl); RB_GC_GUARD(rpl); return val; } @@ -2699,7 +2694,7 @@ rb_ary_insert(int argc, VALUE *argv, VALUE ary) } pos++; } - rb_ary_splice(ary, pos, 0, argv + 1, argc - 1); + rb_ary_splice(ary, pos, 0, argv + 1, argc - 1, FALSE); return ary; } @@ -4423,7 +4418,7 @@ ary_slice_bang_by_rb_ary_splice(VALUE ary, long pos, long len) } else { VALUE arg2 = rb_ary_new4(len, RARRAY_CONST_PTR(ary)+pos); - rb_ary_splice(ary, pos, len, 0, 0); + rb_ary_splice(ary, pos, len, 0, 0, FALSE); return arg2; } } @@ -5270,7 +5265,7 @@ ary_append(VALUE x, VALUE y) { long n = RARRAY_LEN(y); if (n > 0) { - rb_ary_splice(x, RARRAY_LEN(x), 0, RARRAY_CONST_PTR(y), n); + rb_ary_splice(x, RARRAY_LEN(x), 0, RARRAY_CONST_PTR(y), n, x == y); } RB_GC_GUARD(y); return x; diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 36f88016149e2a..773fb1bd8c0c39 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -670,6 +670,83 @@ def (x = Object.new).to_ary assert_equal(:ok, a.last) end + # Long enough not to be embedded, so that dup/slicing share the buffer. + SHARED_BUFFER_LEN = 200 + + def shared_buffer_src + (0...SHARED_BUFFER_LEN).map {|i| "e#{i}"} + end + + # [Bug #22259] + def test_splice_shared_buffer_longer_than_self + src = shared_buffer_src + a = @cls[*src] + b = a.dup + b.pop + # `a` and `b` share one buffer, but `a` is longer. + assert_equal(src[0..-2] + src, b.concat(a)) + GC.start + assert_equal(src.last, b.last) + end + + def test_splice_shared_buffer_overlapping + src = shared_buffer_src + a = @cls[*src] + b = a[0, 150] + c = a[50, 150] + assert_equal(src[0, 150] + src[50, 150], b.concat(c)) + GC.start + assert_equal(src.last, b.last) + end + + def test_splice_shared_buffer_at_offset + src = shared_buffer_src + a = @cls[*src] + b = a[10, SHARED_BUFFER_LEN - 10] + # `b` shares `a`'s buffer at a non-zero offset. + a[0, 0] = b + assert_equal(src[10..] + src, a) + GC.start + assert_equal(src.last, a.last) + + c = @cls[*src] + d = c[10, SHARED_BUFFER_LEN - 10] + c[5, 2] = d + assert_equal(src[0, 5] + src[10..] + src[7..], c) + end + + def test_splice_shared_buffer_replace_shorter + src = shared_buffer_src + a = @cls[*src] + b = a[SHARED_BUFFER_LEN / 2, SHARED_BUFFER_LEN / 2] + a[0, SHARED_BUFFER_LEN] = b + assert_equal(src[SHARED_BUFFER_LEN / 2..], a) + GC.start + assert_equal(src.last, a.last) + end + + def test_splice_shared_buffer_frozen_root + src = shared_buffer_src + # A frozen array becomes the shared root itself. + a = @cls[*src].freeze + b = a[0, SHARED_BUFFER_LEN - 1] + assert_equal(src[0..-2] + src, b.concat(a)) + GC.start + assert_equal(src.last, b.last) + end + + def test_splice_self_shared_buffer + src = shared_buffer_src + a = @cls[*src] + a[10, 1] # make `a` share its buffer + assert_equal(src + src, a.concat(a)) + + b = @cls[*src] + b[10, SHARED_BUFFER_LEN - 10] + b[5, 2] = b + assert_equal(src[0, 5] + src + src[7..], b) + end + def test_count a = @cls[1, 2, 3, 1, 2] assert_equal(5, a.count) From 72a511da244d63a9eaa604b61c85be89fb36db01 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Tue, 25 Aug 2026 03:24:25 +0900 Subject: [PATCH 08/10] Split rb_ary_splice() into a pointer version and an array version Most callers have the replacement as an array object; let them pass it directly. Co-Authored-By: Claude Opus 5 (1M context) --- array.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/array.c b/array.c index 10bf936e1424b7..2907dca2b0d5d8 100644 --- a/array.c +++ b/array.c @@ -2307,7 +2307,7 @@ rb_ary_to_ary(VALUE obj) } static void -rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen, int self_insert) +ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen, int self_insert) { long olen; @@ -2376,6 +2376,13 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen, int s } } +static void +rb_ary_splice(VALUE ary, long beg, long len, VALUE rpl) +{ + ary_splice(ary, beg, len, RARRAY_CONST_PTR(rpl), RARRAY_LEN(rpl), rpl == ary); + RB_GC_GUARD(rpl); +} + void rb_ary_set_len(VALUE ary, long len) { @@ -2463,9 +2470,7 @@ ary_aset_by_rb_ary_store(VALUE ary, long key, VALUE val) static VALUE ary_aset_by_rb_ary_splice(VALUE ary, long beg, long len, VALUE val) { - VALUE rpl = rb_ary_to_ary(val); - rb_ary_splice(ary, beg, len, RARRAY_CONST_PTR(rpl), RARRAY_LEN(rpl), ary == rpl); - RB_GC_GUARD(rpl); + rb_ary_splice(ary, beg, len, rb_ary_to_ary(val)); return val; } @@ -2694,7 +2699,7 @@ rb_ary_insert(int argc, VALUE *argv, VALUE ary) } pos++; } - rb_ary_splice(ary, pos, 0, argv + 1, argc - 1, FALSE); + ary_splice(ary, pos, 0, argv + 1, argc - 1, FALSE); return ary; } @@ -4418,7 +4423,7 @@ ary_slice_bang_by_rb_ary_splice(VALUE ary, long pos, long len) } else { VALUE arg2 = rb_ary_new4(len, RARRAY_CONST_PTR(ary)+pos); - rb_ary_splice(ary, pos, len, 0, 0, FALSE); + ary_splice(ary, pos, len, 0, 0, FALSE); return arg2; } } @@ -5263,11 +5268,9 @@ rb_ary_plus(VALUE x, VALUE y) static VALUE ary_append(VALUE x, VALUE y) { - long n = RARRAY_LEN(y); - if (n > 0) { - rb_ary_splice(x, RARRAY_LEN(x), 0, RARRAY_CONST_PTR(y), n, x == y); + if (RARRAY_LEN(y) > 0) { + rb_ary_splice(x, RARRAY_LEN(x), 0, y); } - RB_GC_GUARD(y); return x; } From b9bcae995d58c48b5f0f828f29d721d3ff525dbb Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 25 Aug 2026 16:55:09 +1200 Subject: [PATCH 09/10] Preserve `IO::Buffer` read-only state when resizing. (#18487) --- io_buffer.c | 3 ++- spec/ruby/core/io/buffer/resize_spec.rb | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/io_buffer.c b/io_buffer.c index 8fe5db16adce8e..6792577d9b6efd 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -2003,7 +2003,8 @@ io_buffer_resize_copy(VALUE self, struct rb_io_buffer *buffer, size_t size) { // Slow path: struct rb_io_buffer resized; - io_buffer_initialize(self, &resized, NULL, size, io_flags_for_size(size), Qnil); + enum rb_io_buffer_flags flags = io_flags_for_size(size) | (buffer->flags & RB_IO_BUFFER_READONLY); + io_buffer_initialize(self, &resized, NULL, size, flags, Qnil); if (buffer->base) { size_t preserve = buffer->size; diff --git a/spec/ruby/core/io/buffer/resize_spec.rb b/spec/ruby/core/io/buffer/resize_spec.rb index c0c94ade81e8e0..d6ea3ce8d36d2b 100644 --- a/spec/ruby/core/io/buffer/resize_spec.rb +++ b/spec/ruby/core/io/buffer/resize_spec.rb @@ -115,6 +115,14 @@ @buffer.size.should == 1 end + it "preserves read-only access when the allocation is replaced" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED | IO::Buffer::READONLY) + @buffer.resize(16) + + @buffer.should.readonly? + -> { @buffer.set_string("test") }.should.raise(IO::Buffer::AccessError, "Buffer is not writable!") + end + ruby_version_is "4.1" do it "raises FrozenError without resizing a frozen buffer" do buffer = IO::Buffer.new(4) From 45088e232680978d372ca8955f5b77e060e180e4 Mon Sep 17 00:00:00 2001 From: Michael <65680649+michaelg100@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:20:24 -0400 Subject: [PATCH 10/10] [ruby/mmtk] Prevent overflow values (https://github.com/ruby/mmtk/pull/107) https://github.com/ruby/mmtk/commit/f6b8b0d5ae --- gc/mmtk/src/utils.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/gc/mmtk/src/utils.rs b/gc/mmtk/src/utils.rs index d1979eaf58da4e..a518e50be99e98 100644 --- a/gc/mmtk/src/utils.rs +++ b/gc/mmtk/src/utils.rs @@ -118,9 +118,9 @@ pub fn parse_capacity(input: &str) -> Option { }; match suffix { - "GiB" => Some(v * GIBIBYTE), - "MiB" => Some(v * MEBIBYTE), - "KiB" => Some(v * KIBIBYTE), + "GiB" => v.checked_mul(GIBIBYTE), + "MiB" => v.checked_mul(MEBIBYTE), + "KiB" => v.checked_mul(KIBIBYTE), "" => Some(v), _ => None, } @@ -150,6 +150,20 @@ mod tests { assert_eq!(Some(10737418240), parse_capacity("10GiB")) } + #[test] + fn test_parse_capacity_rejects_overflowing_values() { + assert_eq!(None, parse_capacity("99999999999GiB")); + assert_eq!(None, parse_capacity("99999999999999MiB")); + assert_eq!(None, parse_capacity("99999999999999999KiB")); + + const GIBIBYTE: usize = 1024 * 1024 * 1024; + let max_gib = usize::MAX / GIBIBYTE; + assert_eq!( + Some(max_gib * GIBIBYTE), + parse_capacity(&format!("{max_gib}GiB")) + ); + } + #[test] fn test_parse_capacity_parses_nonsense_values() { assert_eq!(None, parse_capacity("notanumber"));