io-stream 0.0.1 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 259533ca29c9be8e0476777d9060a4aa6928fa856848396110b3f668628db636
4
- data.tar.gz: ed074a9ff19c5f0f74223228d1bb9929ff7534f921c11e4adf34fe88c5f2824b
3
+ metadata.gz: 15a89f071ddff7b47577a25a20195cb1d246f9b7105af8ee605ae01e0d267923
4
+ data.tar.gz: c29039d3ab0d1e234733ddf38f1ddc4be40cbffa92b5f9b47ba1dc247e9ea357
5
5
  SHA512:
6
- metadata.gz: f90e4bca685926a8cc28b21b65ddceea17fd8d3ba25ccefd6daf9095403ef7446c71b9afdfe57367faccf2ee46635bcf939e14f8ac36e9ebc6b9ee55bdafaada
7
- data.tar.gz: f7aaa56fb744df8cd057412083ad63c707165e3e1c404397dfa447f2135f7d11fef6d0b855cf88c23d28346ba96c320bc9566e7395520889d89ea7b63ea4fd1c
6
+ metadata.gz: 90d91151a245677dd5e7b96c2942af1b85f16d2a59c64c981e0d3d256d4af0ce5cc81115bfeb3a1981f765695e4777a0e76caf9c8f3d8995bbba204ebce8d91e
7
+ data.tar.gz: e40408fa7e69bab982229290640cb0e9c7a4efb9bc39ac40003ef1fe698f1aac962cd10a7464b5b2643bf46dddf6697e26296209527d507a0d135f112bc4bbae
checksums.yaml.gz.sig CHANGED
Binary file
data/lib/io/buffered.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2023-2024, by Samuel Williams.
5
5
 
6
- unless IO.method_defined?(:buffered?)
6
+ unless IO.method_defined?(:buffered?, false)
7
7
  class IO
8
8
  def buffered?
9
9
  return !self.sync
@@ -17,16 +17,19 @@ end
17
17
 
18
18
  require 'socket'
19
19
 
20
- unless BasicSocket.method_defined?(:buffered?)
20
+ unless BasicSocket.method_defined?(:buffered?, false)
21
21
  class BasicSocket
22
- # Is it likely that the socket is still connected?
23
- # May return false positive, but won't return false negative.
22
+ def ip_protocol_tcp?
23
+ local_address = self.local_address
24
+
25
+ return (local_address.afamily == ::Socket::AF_INET || local_address.afamily == ::Socket::AF_INET6) && local_address.socktype == ::Socket::SOCK_STREAM
26
+ end
27
+
24
28
  def buffered?
25
29
  return false unless super
26
30
 
27
- case self.local_address.protocol
28
- when IPPROTO_TCP
29
- return !self.getsockopt(IPPROTO_TCP, TCP_NODELAY).bool
31
+ if ip_protocol_tcp?
32
+ return !self.getsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY).bool
30
33
  else
31
34
  return true
32
35
  end
@@ -35,14 +38,13 @@ unless BasicSocket.method_defined?(:buffered?)
35
38
  def buffered=(value)
36
39
  super
37
40
 
38
- case self.local_address.socktype
39
- when SOCK_STREAM
41
+ if ip_protocol_tcp?
40
42
  # When buffered is set to true, TCP_NODELAY shold be disabled.
41
- self.setsockopt(IPPROTO_TCP, TCP_NODELAY, value ? 0 : 1)
43
+ self.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, value ? 0 : 1)
42
44
  end
43
- rescue Errno::EINVAL
45
+ rescue ::Errno::EINVAL
44
46
  # On Darwin, sometimes occurs when the connection is not yet fully formed. Empirically, TCP_NODELAY is enabled despite this result.
45
- rescue Errno::EOPNOTSUPP
47
+ rescue ::Errno::EOPNOTSUPP
46
48
  # Some platforms may simply not support the operation.
47
49
  end
48
50
  end
@@ -50,7 +52,7 @@ end
50
52
 
51
53
  require 'stringio'
52
54
 
53
- unless StringIO.method_defined?(:buffered?)
55
+ unless StringIO.method_defined?(:buffered?, false)
54
56
  class StringIO
55
57
  def buffered?
56
58
  return !self.sync
data/lib/io/readable.rb CHANGED
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2023-2024, by Samuel Williams.
5
5
 
6
6
  class IO
7
- unless method_defined?(:readable?)
7
+ unless method_defined?(:readable?, false)
8
8
  def readable?
9
9
  # Do not call `eof?` here as it is not concurrency-safe and it can block.
10
10
  !closed?
@@ -15,10 +15,10 @@ end
15
15
  require 'socket'
16
16
 
17
17
  class BasicSocket
18
- unless method_defined?(:readable?)
18
+ unless method_defined?(:readable?, false)
19
19
  def readable?
20
20
  # If we can wait for the socket to become readable, we know that the socket may still be open.
21
- result = self.recv_nonblock(1, MSG_PEEK, exception: false)
21
+ result = self.recv_nonblock(1, ::Socket::MSG_PEEK, exception: false)
22
22
 
23
23
  # No data was available - newer Ruby can return nil instead of empty string:
24
24
  return false if result.nil?
@@ -35,7 +35,7 @@ end
35
35
  require 'stringio'
36
36
 
37
37
  class StringIO
38
- unless method_defined?(:readable?)
38
+ unless method_defined?(:readable?, false)
39
39
  def readable?
40
40
  !eof?
41
41
  end
@@ -45,7 +45,7 @@ end
45
45
  require 'openssl'
46
46
 
47
47
  class OpenSSL::SSL::SSLSocket
48
- unless method_defined?(:readable?)
48
+ unless method_defined?(:readable?, false)
49
49
  def readable?
50
50
  to_io.readable?
51
51
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023-2024, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+
8
+ module IO::Stream
9
+ class Buffered < Generic
10
+ def self.open(path, mode = "r+", **options)
11
+ stream = self.new(::File.open(path, mode), **options)
12
+
13
+ return stream unless block_given?
14
+
15
+ begin
16
+ yield stream
17
+ ensure
18
+ stream.close
19
+ end
20
+ end
21
+
22
+ def self.wrap(io, **options)
23
+ if io.respond_to?(:buffered=)
24
+ io.buffered = false
25
+ end
26
+
27
+ stream = self.new(io, **options)
28
+
29
+ return stream unless block_given?
30
+
31
+ begin
32
+ yield stream
33
+ ensure
34
+ stream.close
35
+ end
36
+ end
37
+
38
+ def initialize(io, ...)
39
+ super(...)
40
+
41
+ @io = io
42
+ end
43
+
44
+ attr :io
45
+
46
+ def closed?
47
+ @io.closed?
48
+ end
49
+
50
+ def close_read
51
+ @io.close_read
52
+ end
53
+
54
+ def close_write
55
+ super
56
+ ensure
57
+ @io.close_write
58
+ end
59
+
60
+ def readable?
61
+ super && @io.readable?
62
+ end
63
+
64
+ protected
65
+
66
+ def sysclose
67
+ @io.close
68
+ end
69
+
70
+ def syswrite(buffer)
71
+ @io.write(buffer)
72
+ end
73
+
74
+ # Reads data from the underlying stream as efficiently as possible.
75
+ def sysread(size, buffer)
76
+ # Come on Ruby, why couldn't this just return `nil`? EOF is not exceptional. Every file has one.
77
+ while true
78
+ result = @io.read_nonblock(size, buffer, exception: false)
79
+
80
+ case result
81
+ when :wait_readable
82
+ @io.wait_readable
83
+ when :wait_writable
84
+ @io.wait_writable
85
+ else
86
+ return result
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -8,6 +8,8 @@ require_relative 'string_buffer'
8
8
  require_relative '../buffered'
9
9
  require_relative '../readable'
10
10
 
11
+ require_relative 'openssl'
12
+
11
13
  module IO::Stream
12
14
  # The default block size for IO buffers. Defaults to 64KB (typical pipe buffer size).
13
15
  BLOCK_SIZE = ENV.fetch('IO_STREAM_BLOCK_SIZE', 1024*64).to_i
@@ -15,32 +17,11 @@ module IO::Stream
15
17
  # The maximum read size when appending to IO buffers. Defaults to 8MB.
16
18
  MAXIMUM_READ_SIZE = ENV.fetch('IO_STREAM_MAXIMUM_READ_SIZE', BLOCK_SIZE * 128).to_i
17
19
 
18
- class BufferedStream
19
- def self.open(path, mode = "r+", **options)
20
- stream = self.new(File.open(path, mode), **options)
21
-
22
- return stream unless block_given?
23
-
24
- begin
25
- yield stream
26
- ensure
27
- stream.close
28
- end
29
- end
30
-
31
- def self.wrap(io, **options)
32
- if io.respond_to?(:buffered=)
33
- io.buffered = false
34
- end
35
-
36
- self.new(io, **options)
37
- end
38
-
39
- def initialize(io, block_size: BLOCK_SIZE, maximum_read_size: MAXIMUM_READ_SIZE)
40
- @io = io
20
+ class Generic
21
+ def initialize(block_size: BLOCK_SIZE, maximum_read_size: MAXIMUM_READ_SIZE)
41
22
  @eof = false
42
23
 
43
- @writing = Thread::Mutex.new
24
+ @writing = ::Thread::Mutex.new
44
25
 
45
26
  @block_size = block_size
46
27
  @maximum_read_size = maximum_read_size
@@ -53,9 +34,7 @@ module IO::Stream
53
34
  @input_buffer = StringBuffer.new
54
35
  end
55
36
 
56
- attr :io
57
-
58
- attr :block_size
37
+ attr_accessor :block_size
59
38
 
60
39
  # Reads `size` bytes from the stream. If size is not specified, read until end of file.
61
40
  def read(size = nil)
@@ -101,7 +80,7 @@ module IO::Stream
101
80
  raise exception, "encountered eof while reading data"
102
81
  end
103
82
 
104
- # This is a compatibility shim for existing code:
83
+ # This is a compatibility shim for existing code that uses `readpartial`.
105
84
  def readpartial(size = nil)
106
85
  read_partial(size) or raise EOFError, "Encountered eof while reading data!"
107
86
  end
@@ -158,7 +137,7 @@ module IO::Stream
158
137
  @write_buffer, @drain_buffer = @drain_buffer, @write_buffer
159
138
 
160
139
  begin
161
- @io.syswrite(@drain_buffer)
140
+ syswrite(@drain_buffer)
162
141
  ensure
163
142
  # If the write operation fails, we still need to clear this buffer, and the data is essentially lost.
164
143
  @drain_buffer.clear
@@ -195,34 +174,27 @@ module IO::Stream
195
174
  flush
196
175
  end
197
176
 
198
- def connected?
199
- @io.connected?
200
- end
201
-
202
177
  def closed?
203
- @io.closed?
178
+ false
204
179
  end
205
180
 
206
181
  def close_read
207
- @io.close_read
208
182
  end
209
183
 
210
184
  def close_write
211
185
  flush
212
- ensure
213
- @io.close_write
214
186
  end
215
187
 
216
188
  # Best effort to flush any unwritten data, and then close the underling IO.
217
189
  def close
218
- return if @io.closed?
190
+ return if closed?
219
191
 
220
192
  begin
221
193
  flush
222
194
  rescue
223
195
  # We really can't do anything here unless we want #close to raise exceptions.
224
196
  ensure
225
- @io.close
197
+ sysclose
226
198
  end
227
199
  end
228
200
 
@@ -261,19 +233,26 @@ module IO::Stream
261
233
  end
262
234
 
263
235
  # If the underlying stream is readable, we can read more data:
264
- return @io.readable?
236
+ return !closed?
265
237
  end
266
238
 
267
- private
239
+ protected
240
+
241
+ def sysclose
242
+ raise NotImplementedError
243
+ end
244
+
245
+ def syswrite(buffer)
246
+ raise NotImplementedError
247
+ end
268
248
 
269
249
  # Reads data from the underlying stream as efficiently as possible.
270
250
  def sysread(size, buffer)
271
- # Come on Ruby, why couldn't this just return `nil`? EOF is not exceptional. Every file has one.
272
- @io.sysread(size, buffer)
273
- rescue EOFError
274
- return false
251
+ raise NotImplementedError
275
252
  end
276
253
 
254
+ private
255
+
277
256
  # Fills the buffer from the underlying stream.
278
257
  def fill_read_buffer(size = @block_size)
279
258
  # We impose a limit because the underlying `read` system call can fail if we request too much data in one go.
@@ -0,0 +1,31 @@
1
+ require 'openssl'
2
+
3
+ module OpenSSL
4
+ module SSL
5
+ class SSLSocket
6
+ unless method_defined?(:close_read)
7
+ def close_read
8
+ # Ignored.
9
+ end
10
+ end
11
+
12
+ unless method_defined?(:close_write)
13
+ def close_write
14
+ self.stop
15
+ end
16
+ end
17
+
18
+ unless method_defined?(:wait_readable)
19
+ def wait_readable(...)
20
+ to_io.wait_readable(...)
21
+ end
22
+ end
23
+
24
+ unless method_defined?(:wait_writable)
25
+ def wait_writable(...)
26
+ to_io.wait_writable(...)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2023-2024, by Samuel Williams.
5
5
 
6
6
  module IO::Stream
7
- VERSION = "0.0.1"
7
+ VERSION = "0.1.0"
8
8
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-stream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -48,7 +48,9 @@ files:
48
48
  - lib/io/buffered.rb
49
49
  - lib/io/readable.rb
50
50
  - lib/io/stream.rb
51
- - lib/io/stream/buffered_stream.rb
51
+ - lib/io/stream/buffered.rb
52
+ - lib/io/stream/generic.rb
53
+ - lib/io/stream/openssl.rb
52
54
  - lib/io/stream/string_buffer.rb
53
55
  - lib/io/stream/version.rb
54
56
  - license.md
metadata.gz.sig CHANGED
Binary file