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/array.c b/array.c index d1453c19fe7f95..2907dca2b0d5d8 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) +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. @@ -2381,6 +2376,13 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) } } +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) { @@ -2468,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)); - RB_GC_GUARD(rpl); + rb_ary_splice(ary, beg, len, rb_ary_to_ary(val)); return val; } @@ -2699,7 +2699,7 @@ rb_ary_insert(int argc, VALUE *argv, VALUE ary) } pos++; } - rb_ary_splice(ary, pos, 0, argv + 1, argc - 1); + ary_splice(ary, pos, 0, argv + 1, argc - 1, FALSE); return ary; } @@ -4423,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); + ary_splice(ary, pos, len, 0, 0, FALSE); return arg2; } } @@ -5268,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); + if (RARRAY_LEN(y) > 0) { + rb_ary_splice(x, RARRAY_LEN(x), 0, y); } - RB_GC_GUARD(y); return x; } 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/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")); 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..6792577d9b6efd 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) { @@ -2035,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; @@ -3331,76 +3300,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 +3351,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 +3380,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 +3477,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 +3488,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 +3536,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 +3558,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 +3569,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 +3648,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 +4298,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/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb index 9af6d54b549843..be9bf76e83b87a 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,55 +38,72 @@ 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 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 + end + end - return_val + # 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/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/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb index 8469d96a760112..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 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 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) 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_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) 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 diff --git a/test/rubygems/test_gem_util_atomic_file_writer.rb b/test/rubygems/test_gem_util_atomic_file_writer.rb index e011a38ad42a04..0e9e23f22e31ed 100644 --- a/test/rubygems/test_gem_util_atomic_file_writer.rb +++ b/test/rubygems/test_gem_util_atomic_file_writer.rb @@ -4,9 +4,254 @@ 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_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? + + 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 + + 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