io-extra 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ ##############################################################################
2
+ # example_io_extra.rb
3
+ #
4
+ # This is a small example program for the io-extra library. Modify as you see
5
+ # fit. You can run this via the 'rake example' task.
6
+ ##############################################################################
7
+ require_relative "../lib/io-extra"
8
+ puts "VERSION: #{IO::EXTRA_VERSION}"
9
+
10
+ begin
11
+ fh = File.open("foo.txt","w+")
12
+
13
+ puts "DIRECTIO"
14
+ sleep 2
15
+
16
+ p fh.directio?
17
+ fh.directio = IO::DIRECTIO_ON
18
+ p fh.directio?
19
+
20
+ puts "FDWALK"
21
+ sleep 2
22
+
23
+ IO.fdwalk(0){ |handle|
24
+ p handle
25
+ p handle.fileno
26
+ puts
27
+ }
28
+
29
+ =begin
30
+ STDIN.close
31
+
32
+ # Should print "Hello" 2 times
33
+ IO.fdwalk(0){ |fd|
34
+ puts "Hello #{fd}"
35
+ }
36
+
37
+
38
+ IO.closefrom(0)
39
+
40
+ puts "Done" # Shouldn't see this
41
+ =end
42
+
43
+ puts "IO.writev"
44
+ sleep 2
45
+
46
+ a = (1..1000).map(&:to_s)
47
+ IO.writev(fh, a)
48
+ fh.rewind
49
+ p fh.read
50
+ ensure
51
+ fh.close
52
+ File.delete("foo.txt") if File.exist?("foo.txt")
53
+ end
@@ -0,0 +1,24 @@
1
+ ########################################################################
2
+ # example_pread.rb
3
+ #
4
+ # Example program demonstrating the use of IO.pread.
5
+ ########################################################################
6
+ require_relative '../lib/io-extra'
7
+ require 'tmpdir'
8
+
9
+ # Create a temporary file with a little data in it.
10
+ file = File.join(Dir.tmpdir, 'pread_test.txt')
11
+ File.open(file, 'w'){ |fh| 100.times{ |n| fh.puts "Hello: #{n}" } }
12
+
13
+ # Read from the file using pread.
14
+ begin
15
+ fh = File.open(file)
16
+
17
+ puts "Handle position before read: #{fh.pos}"
18
+ puts IO.pread(fh, 18, 0)
19
+
20
+ puts "Handle position after read: #{fh.pos}"
21
+ puts IO.pread(fh, 18, 0)
22
+ ensure
23
+ fh.close
24
+ end
@@ -0,0 +1,19 @@
1
+ require 'benchmark'
2
+ require 'io-extra'
3
+
4
+ a = (0..1023).to_a.map(&:to_s)
5
+
6
+ file1 = 'write_test.txt'
7
+ file2 = 'writev_test.txt'
8
+
9
+ fh = File.open(file2, 'w')
10
+
11
+ Benchmark.bm(25) do |x|
12
+ x.report('write'){ 100000.times{ File.write(file1, a.join) } }
13
+ x.report('writev'){ 100000.times{ IO.writev(fh.fileno, a) } }
14
+ end
15
+
16
+ fh.close
17
+
18
+ File.delete(file1)
19
+ File.delete(file2)
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>English</string>
7
+ <key>CFBundleIdentifier</key>
8
+ <string>com.apple.xcode.dsym.extra.bundle</string>
9
+ <key>CFBundleInfoDictionaryVersion</key>
10
+ <string>6.0</string>
11
+ <key>CFBundlePackageType</key>
12
+ <string>dSYM</string>
13
+ <key>CFBundleSignature</key>
14
+ <string>????</string>
15
+ <key>CFBundleShortVersionString</key>
16
+ <string>1.0</string>
17
+ <key>CFBundleVersion</key>
18
+ <string>1</string>
19
+ </dict>
20
+ </plist>
@@ -0,0 +1,5 @@
1
+ ---
2
+ triple: 'arm64-apple-darwin'
3
+ binary-path: extra.bundle
4
+ relocations: []
5
+ ...
data/ext/io/extra.c CHANGED
@@ -87,9 +87,18 @@ static int open_max(void){
87
87
  */
88
88
  static VALUE io_closefrom(VALUE klass, VALUE v_low_fd){
89
89
  int i, lowfd;
90
- int maxfd = open_max();
90
+ int maxfd;
91
+
91
92
  lowfd = NUM2INT(v_low_fd);
92
93
 
94
+ if(lowfd < 0)
95
+ rb_raise(rb_eArgError, "lowfd must be non-negative");
96
+
97
+ maxfd = open_max();
98
+
99
+ if(maxfd < 0)
100
+ rb_raise(rb_eRuntimeError, "failed to determine maximum file descriptor");
101
+
93
102
  for(i = lowfd; i < maxfd; i++) {
94
103
  if(!RB_RESERVED_FD_P(i))
95
104
  close(i);
@@ -99,54 +108,7 @@ static VALUE io_closefrom(VALUE klass, VALUE v_low_fd){
99
108
  }
100
109
 
101
110
  #ifndef HAVE_FDWALK
102
- static int fdwalk(int (*func)(void *data, int fd), void *data){
103
- int rv = 0;
104
- int fd;
105
-
106
- #ifdef PROC_SELF_FD_DIR
107
- DIR *dir = opendir(PROC_SELF_FD_DIR);
108
-
109
- if(dir){ /* procfs may not be mounted... */
110
- struct dirent *ent;
111
- int saved_errno;
112
- int dir_fd = dirfd(dir);
113
-
114
- while((ent = readdir(dir))){
115
- char *err = NULL;
116
-
117
- if(ent->d_name[0] == '.')
118
- continue;
119
-
120
- errno = 0;
121
- fd = (int)strtol(ent->d_name, &err, 10);
122
-
123
- if (errno || ! err || *err || fd == dir_fd)
124
- continue;
125
-
126
- if ((rv = func(data, fd)))
127
- break;
128
- }
129
- saved_errno = errno;
130
- closedir(dir);
131
- errno = saved_errno;
132
- } else
133
- #endif /* PROC_SELF_FD_DIR */
134
- {
135
- int maxfd = open_max();
136
-
137
- for(fd = 0; fd < maxfd; fd++){
138
- /* use fcntl to detect whether fd is a valid file descriptor */
139
- errno = 0;
140
- if(fcntl(fd, F_GETFD) < 0)
141
- continue;
142
-
143
- errno = 0;
144
- if ((rv = func(data, fd)))
145
- break;
146
- }
147
- }
148
- return rv;
149
- }
111
+ /* Define HAVE_FDWALK so the io_fdwalk implementation below is compiled */
150
112
  #define HAVE_FDWALK
151
113
  #endif
152
114
 
@@ -169,6 +131,74 @@ static int close_func(void* lowfd, int fd){
169
131
  return 0;
170
132
  }
171
133
 
134
+ /* Structure to pass data to fdwalk_body and fdwalk_ensure */
135
+ struct fdwalk_data {
136
+ int lowfd;
137
+ #ifdef PROC_SELF_FD_DIR
138
+ DIR *dir;
139
+ #endif
140
+ };
141
+
142
+ /* Cleanup function for rb_ensure */
143
+ static VALUE fdwalk_ensure(VALUE arg){
144
+ struct fdwalk_data *data = (struct fdwalk_data *)arg;
145
+ #ifdef PROC_SELF_FD_DIR
146
+ if(data->dir){
147
+ closedir(data->dir);
148
+ data->dir = NULL;
149
+ }
150
+ #endif
151
+ (void)data; /* Suppress unused warning when PROC_SELF_FD_DIR not defined */
152
+ return Qnil;
153
+ }
154
+
155
+ /* Main fdwalk iteration body */
156
+ static VALUE fdwalk_body(VALUE arg){
157
+ struct fdwalk_data *data = (struct fdwalk_data *)arg;
158
+ int fd;
159
+
160
+ #ifdef PROC_SELF_FD_DIR
161
+ if(data->dir){
162
+ struct dirent *ent;
163
+ int dir_fd = dirfd(data->dir);
164
+
165
+ while((ent = readdir(data->dir))){
166
+ char *err = NULL;
167
+
168
+ if(ent->d_name[0] == '.')
169
+ continue;
170
+
171
+ errno = 0;
172
+ fd = (int)strtol(ent->d_name, &err, 10);
173
+
174
+ if (errno || !err || *err || fd == dir_fd)
175
+ continue;
176
+
177
+ /* Validate fd is still open before calling callback (reduces race window) */
178
+ if(fcntl(fd, F_GETFD) < 0)
179
+ continue;
180
+
181
+ close_func(&data->lowfd, fd);
182
+ }
183
+ } else
184
+ #endif /* PROC_SELF_FD_DIR */
185
+ {
186
+ int maxfd = open_max();
187
+
188
+ for(fd = 0; fd < maxfd; fd++){
189
+ /* use fcntl to detect whether fd is a valid file descriptor */
190
+ errno = 0;
191
+ if(fcntl(fd, F_GETFD) < 0)
192
+ continue;
193
+
194
+ errno = 0;
195
+ close_func(&data->lowfd, fd);
196
+ }
197
+ }
198
+
199
+ return Qnil;
200
+ }
201
+
172
202
  /*
173
203
  * call-seq:
174
204
  * IO.fdwalk(lowfd){ |fh| ... }
@@ -179,12 +209,19 @@ static int close_func(void* lowfd, int fd){
179
209
  */
180
210
  static VALUE io_fdwalk(int argc, VALUE* argv, VALUE klass){
181
211
  VALUE v_low_fd, v_block;
182
- int lowfd;
212
+ struct fdwalk_data data;
183
213
 
184
214
  rb_scan_args(argc, argv, "1&", &v_low_fd, &v_block);
185
- lowfd = NUM2INT(v_low_fd);
215
+ data.lowfd = NUM2INT(v_low_fd);
186
216
 
187
- fdwalk(close_func, &lowfd);
217
+ if(data.lowfd < 0)
218
+ rb_raise(rb_eArgError, "lowfd must be non-negative");
219
+
220
+ #ifdef PROC_SELF_FD_DIR
221
+ data.dir = opendir(PROC_SELF_FD_DIR);
222
+ #endif
223
+
224
+ rb_ensure(fdwalk_body, (VALUE)&data, fdwalk_ensure, (VALUE)&data);
188
225
 
189
226
  return klass;
190
227
  }
@@ -284,29 +321,51 @@ static VALUE io_set_directio(VALUE self, VALUE v_advice){
284
321
  }
285
322
  #endif
286
323
 
287
- /* this can't be a function since we use alloca() */
288
- #define ARY2IOVEC(iov,iovcnt,expect,ary) \
289
- do { \
290
- VALUE *cur; \
291
- struct iovec *tmp; \
292
- long n; \
293
- if (TYPE(ary) != T_ARRAY) \
294
- rb_raise(rb_eArgError, "must be an array of strings"); \
295
- cur = RARRAY_PTR(ary); \
296
- n = RARRAY_LEN(ary); \
297
- if (n > IOV_MAX) \
298
- rb_raise(rb_eArgError, "array is larger than IOV_MAX"); \
299
- iov = tmp = alloca(sizeof(struct iovec) * n); \
300
- expect = 0; \
301
- iovcnt = (int)n; \
302
- for (; --n >= 0; tmp++, cur++) { \
303
- if (TYPE(*cur) != T_STRING) \
304
- rb_raise(rb_eArgError, "must be an array of strings"); \
305
- tmp->iov_base = RSTRING_PTR(*cur); \
306
- tmp->iov_len = RSTRING_LEN(*cur); \
307
- expect += tmp->iov_len; \
308
- } \
309
- } while (0)
324
+ /* Structure to track iovec allocation */
325
+ struct iovec_buffer {
326
+ struct iovec *iov;
327
+ int iovcnt;
328
+ ssize_t expected;
329
+ };
330
+
331
+ /* Convert Ruby array to iovec using heap allocation */
332
+ static void ary2iovec(struct iovec_buffer *buf, VALUE ary) {
333
+ VALUE *cur;
334
+ struct iovec *tmp;
335
+ long n;
336
+
337
+ if (TYPE(ary) != T_ARRAY)
338
+ rb_raise(rb_eArgError, "must be an array of strings");
339
+
340
+ cur = RARRAY_PTR(ary);
341
+ n = RARRAY_LEN(ary);
342
+
343
+ if (n > IOV_MAX)
344
+ rb_raise(rb_eArgError, "array is larger than IOV_MAX");
345
+
346
+ buf->iov = tmp = ALLOC_N(struct iovec, n);
347
+ buf->expected = 0;
348
+ buf->iovcnt = (int)n;
349
+
350
+ for (; --n >= 0; tmp++, cur++) {
351
+ if (TYPE(*cur) != T_STRING) {
352
+ xfree(buf->iov);
353
+ buf->iov = NULL;
354
+ rb_raise(rb_eArgError, "must be an array of strings");
355
+ }
356
+ tmp->iov_base = RSTRING_PTR(*cur);
357
+ tmp->iov_len = RSTRING_LEN(*cur);
358
+ buf->expected += tmp->iov_len;
359
+ }
360
+ }
361
+
362
+ /* Free iovec buffer */
363
+ static void free_iovec_buffer(struct iovec_buffer *buf) {
364
+ if (buf->iov) {
365
+ xfree(buf->iov);
366
+ buf->iov = NULL;
367
+ }
368
+ }
310
369
 
311
370
  #if defined(HAVE_WRITEV)
312
371
  struct writev_args {
@@ -336,13 +395,17 @@ static VALUE s_io_writev(VALUE klass, VALUE fd, VALUE ary) {
336
395
  ssize_t result = 0;
337
396
  ssize_t left;
338
397
  struct writev_args args;
398
+ struct iovec_buffer iov_buf;
339
399
 
340
400
  // Allow a fileno or filehandle
341
401
  if(rb_respond_to(fd, rb_intern("fileno")))
342
402
  fd = rb_funcall(fd, rb_intern("fileno"), 0, 0);
343
403
 
344
404
  args.fd = NUM2INT(fd);
345
- ARY2IOVEC(args.iov, args.iovcnt, left, ary);
405
+ ary2iovec(&iov_buf, ary);
406
+ args.iov = iov_buf.iov;
407
+ args.iovcnt = iov_buf.iovcnt;
408
+ left = iov_buf.expected;
346
409
 
347
410
  for(;;) {
348
411
  ssize_t w = (ssize_t)rb_thread_call_without_gvl(
@@ -359,6 +422,7 @@ static VALUE s_io_writev(VALUE klass, VALUE fd, VALUE ary) {
359
422
  * we'll let the next write (or close) fail instead */
360
423
  break;
361
424
  }
425
+ free_iovec_buffer(&iov_buf);
362
426
  rb_sys_fail("writev");
363
427
  }
364
428
  }
@@ -376,13 +440,25 @@ static VALUE s_io_writev(VALUE klass, VALUE fd, VALUE ary) {
376
440
  left -= w;
377
441
 
378
442
  // Skip over iovecs we've already written completely
379
- for(i = 0; i < args.iovcnt; i++, new_iov++){
443
+ for(i = 0; i < args.iovcnt; i++){
380
444
  if (w == 0)
381
445
  break;
382
446
 
447
+ // Bounds check before pointer arithmetic
448
+ if(new_iov == NULL){
449
+ free_iovec_buffer(&iov_buf);
450
+ rb_raise(rb_eRuntimeError, "writev: iovec bounds check failed");
451
+ }
452
+
383
453
  // Partially written iov, modify and retry with current iovec in front
384
454
  if(new_iov->iov_len > (size_t)w){
385
- VALUE base = (VALUE)new_iov->iov_base;
455
+ char* base = (char*)new_iov->iov_base;
456
+
457
+ // Validate base pointer before arithmetic
458
+ if(base == NULL){
459
+ free_iovec_buffer(&iov_buf);
460
+ rb_raise(rb_eRuntimeError, "writev: null iov_base");
461
+ }
386
462
 
387
463
  new_iov->iov_len -= w;
388
464
  new_iov->iov_base = (void *)(base + w);
@@ -390,6 +466,13 @@ static VALUE s_io_writev(VALUE klass, VALUE fd, VALUE ary) {
390
466
  }
391
467
 
392
468
  w -= new_iov->iov_len;
469
+ new_iov++;
470
+ }
471
+
472
+ // Validate we haven't exceeded bounds before modifying args
473
+ if(i > args.iovcnt){
474
+ free_iovec_buffer(&iov_buf);
475
+ rb_raise(rb_eRuntimeError, "writev: exceeded iovec array bounds");
393
476
  }
394
477
 
395
478
  // Retry without the already-written iovecs
@@ -398,6 +481,7 @@ static VALUE s_io_writev(VALUE klass, VALUE fd, VALUE ary) {
398
481
  }
399
482
  }
400
483
 
484
+ free_iovec_buffer(&iov_buf);
401
485
  return LONG2NUM(result);
402
486
  }
403
487
  #endif
@@ -418,8 +502,15 @@ static VALUE io_get_ttyname(VALUE self){
418
502
 
419
503
  int fd = NUM2INT(rb_funcall(self, rb_intern("fileno"), 0, 0));
420
504
 
421
- if(isatty(fd))
422
- v_return = rb_str_new2(ttyname(fd));
505
+ if(fd < 0)
506
+ rb_raise(rb_eArgError, "invalid file descriptor");
507
+
508
+ errno = 0;
509
+ if(isatty(fd)){
510
+ char *name = ttyname(fd);
511
+ if(name != NULL)
512
+ v_return = rb_str_new2(name);
513
+ }
423
514
 
424
515
  return v_return;
425
516
  }
@@ -428,7 +519,7 @@ static VALUE io_get_ttyname(VALUE self){
428
519
  /* Adds the IO.closefrom, IO.fdwalk class methods, as well as the IO#directio
429
520
  * and IO#directio? instance methods (if supported on your platform).
430
521
  */
431
- void Init_extra(){
522
+ void Init_extra(void){
432
523
  rb_define_singleton_method(rb_cIO, "closefrom", io_closefrom, 1);
433
524
 
434
525
  #ifdef HAVE_FDWALK
data/io-extra.gemspec ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'rbconfig'
3
+
4
+ Gem::Specification.new do |spec|
5
+ if File::ALT_SEPARATOR
6
+ STDERR.puts 'Not supported on this platform. Exiting.'
7
+ exit(-1)
8
+ end
9
+
10
+ spec.name = 'io-extra'
11
+ spec.version = '1.5.0'
12
+ spec.author = 'Daniel J. Berger'
13
+ spec.license = 'Apache-2.0'
14
+ spec.email = 'djberg96@gmail.com'
15
+ spec.homepage = 'https://github.com/djberg96/io-extra'
16
+ spec.summary = 'Adds extra methods to the IO class'
17
+ spec.test_files = Dir['spec/*_spec.rb']
18
+ spec.extensions = ['ext/extconf.rb']
19
+ spec.cert_chain = ['certs/djberg96_pub.pem']
20
+ spec.files = Dir['**/*'].reject{ |f| f.include?('git') }
21
+
22
+ spec.extra_rdoc_files = [
23
+ 'CHANGES.md',
24
+ 'README.md',
25
+ 'MANIFEST.md',
26
+ 'ext/io/extra.c'
27
+ ]
28
+
29
+ spec.add_development_dependency('rake')
30
+ spec.add_development_dependency('rspec', '~> 3.9')
31
+ spec.add_development_dependency('rubocop')
32
+ spec.add_development_dependency('rubocop-rspec')
33
+
34
+ spec.metadata = {
35
+ 'homepage_uri' => 'https://github.com/djberg96/io-extra',
36
+ 'bug_tracker_uri' => 'https://github.com/djberg96/io-extra/issues',
37
+ 'changelog_uri' => 'https://github.com/djberg96/io-extra/blob/main/CHANGES.md',
38
+ 'documentation_uri' => 'https://github.com/djberg96/io-extra/wiki',
39
+ 'source_code_uri' => 'https://github.com/djberg96/io-extra',
40
+ 'wiki_uri' => 'https://github.com/djberg96/io-extra/wiki',
41
+ 'rubygems_mfa_required' => 'true',
42
+ 'github_repo' => 'https://github.com/djberg96/io-extra',
43
+ 'funding_uri' => 'https://github.com/sponsors/djberg96',
44
+ }
45
+
46
+ spec.description = <<-EOF
47
+ Adds the IO.closefrom, IO.fdwalk, IO.pread, IO.pwrite, and IO.writev
48
+ singleton methods as well as the IO#directio, IO#directio? and IO#ttyname
49
+ instance methods (for those platforms that support them).
50
+ EOF
51
+ end
data/lib/io-extra.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'io/extra'
2
2
 
3
+ # Reopen the core IO class to define pread and pwrite singleton methods
4
+ # for backwards compatibility.
3
5
  class IO
4
- EXTRA_VERSION = '1.4.0'.freeze
6
+ EXTRA_VERSION = '1.5.0'.freeze
5
7
 
6
8
  # Singleton version of the IO#pwrite method.
7
9
  #