io-stream 0.7.0 → 0.9.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: c48d464c539d56862e04437f88dee2997e4393951efca83cf622a23bac9e4b75
4
- data.tar.gz: 610277102a9233e5d470daabd4f12917d429dc6a141cf52897ff95af3a06de95
3
+ metadata.gz: 108295b1d28ce05e7c9e9e3f7b3d26427f564a85527a9f23a24ad5b47a261dea
4
+ data.tar.gz: 2aa1c2f6d600d38667e5030122aecd1faa97f81be44695edf6776dda541d5a89
5
5
  SHA512:
6
- metadata.gz: 5761c359f16364169757dca90c4f41bfb87269ab3219b6a565b0081aafb3146f2ff6b11e77a95bebd478b31cbc702752e486c484666536f286e4ccdb4a2c6520
7
- data.tar.gz: 6e44c1ce99d3741c6c9e8f65976f5664d287586e5a704749466a70992dcfcf5bac55ab84e1429cc5a22417af889ee7938af55ec63081309b3522629a9e05721b
6
+ metadata.gz: c6c9fc15b84c0ef6d58e2400aeebaac0a36fd67f656dcf10e2f4a1dde2176434dc8f9464809f2b5c05af2563c8bcb1a74ef1e243b7263348d36d24f00df2af7c
7
+ data.tar.gz: 92c3a90776a6f6308ab7d0fa25d35b7df65021f70cb8aadf01e7363961c818392c2ce3fe861b20b38965acebcca851e7419708ad18b2536ae892496b9ea39ade
checksums.yaml.gz.sig CHANGED
Binary file
@@ -106,23 +106,26 @@ module IO::Stream
106
106
  end
107
107
  end
108
108
 
109
- def syswrite(buffer)
110
- # This fails due to re-entrancy issues with a concurrent call to `sysclose`.
111
- # return @io.write(buffer)
112
-
113
- while true
114
- result = @io.write_nonblock(buffer, exception: false)
115
-
116
- case result
117
- when :wait_readable
118
- @io.wait_readable(@io.timeout) or raise ::IO::TimeoutError, "read timeout"
119
- when :wait_writable
120
- @io.wait_writable(@io.timeout) or raise ::IO::TimeoutError, "write timeout"
121
- else
122
- if result == buffer.bytesize
123
- return
109
+ if RUBY_VERSION >= "3.3"
110
+ def syswrite(buffer)
111
+ return @io.write(buffer)
112
+ end
113
+ else
114
+ def syswrite(buffer)
115
+ while true
116
+ result = @io.write_nonblock(buffer, exception: false)
117
+
118
+ case result
119
+ when :wait_readable
120
+ @io.wait_readable(@io.timeout) or raise ::IO::TimeoutError, "read timeout"
121
+ when :wait_writable
122
+ @io.wait_writable(@io.timeout) or raise ::IO::TimeoutError, "write timeout"
124
123
  else
125
- buffer = buffer.byteslice(result, buffer.bytesize)
124
+ if result == buffer.bytesize
125
+ return
126
+ else
127
+ buffer = buffer.byteslice(result, buffer.bytesize)
128
+ end
126
129
  end
127
130
  end
128
131
  end
@@ -35,7 +35,7 @@ module IO::Stream
35
35
  return if closed?
36
36
 
37
37
  begin
38
- flush
38
+ self.flush
39
39
  rescue
40
40
  # We really can't do anything here unless we want #close to raise exceptions.
41
41
  ensure
@@ -58,9 +58,18 @@ module IO::Stream
58
58
 
59
59
  # Read data from the stream.
60
60
  # @parameter size [Integer | Nil] The number of bytes to read. If nil, read until end of stream.
61
- # @returns [String] The data read from the stream.
62
- def read(size = nil)
63
- return String.new(encoding: Encoding::BINARY) if size == 0
61
+ # @parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
62
+ # @returns [String] The data read from the stream, or the provided buffer filled with data.
63
+ def read(size = nil, buffer = nil)
64
+ if size == 0
65
+ if buffer
66
+ buffer.clear
67
+ buffer.force_encoding(Encoding::BINARY)
68
+ return buffer
69
+ else
70
+ return String.new(encoding: Encoding::BINARY)
71
+ end
72
+ end
64
73
 
65
74
  if size
66
75
  until @done or @read_buffer.bytesize >= size
@@ -76,26 +85,37 @@ module IO::Stream
76
85
  end
77
86
  end
78
87
 
79
- return consume_read_buffer(size)
88
+ return consume_read_buffer(size, buffer)
80
89
  end
81
90
 
82
91
  # Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible.
83
- def read_partial(size = nil)
84
- return String.new(encoding: Encoding::BINARY) if size == 0
92
+ # @parameter size [Integer | Nil] The number of bytes to read. If nil, read all available data.
93
+ # @parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
94
+ # @returns [String] The data read from the stream, or the provided buffer filled with data.
95
+ def read_partial(size = nil, buffer = nil)
96
+ if size == 0
97
+ if buffer
98
+ buffer.clear
99
+ buffer.force_encoding(Encoding::BINARY)
100
+ return buffer
101
+ else
102
+ return String.new(encoding: Encoding::BINARY)
103
+ end
104
+ end
85
105
 
86
106
  if !@done and @read_buffer.empty?
87
107
  fill_read_buffer
88
108
  end
89
109
 
90
- return consume_read_buffer(size)
110
+ return consume_read_buffer(size, buffer)
91
111
  end
92
112
 
93
113
  # Read exactly the specified number of bytes.
94
114
  # @parameter size [Integer] The number of bytes to read.
95
115
  # @parameter exception [Class] The exception to raise if not enough data is available.
96
116
  # @returns [String] The data read from the stream.
97
- def read_exactly(size, exception: EOFError)
98
- if buffer = read(size)
117
+ def read_exactly(size, buffer = nil, exception: EOFError)
118
+ if buffer = read(size, buffer)
99
119
  if buffer.bytesize != size
100
120
  raise exception, "Could not read enough data!"
101
121
  end
@@ -107,8 +127,11 @@ module IO::Stream
107
127
  end
108
128
 
109
129
  # This is a compatibility shim for existing code that uses `readpartial`.
110
- def readpartial(size = nil)
111
- read_partial(size) or raise EOFError, "Encountered done while reading data!"
130
+ # @parameter size [Integer | Nil] The number of bytes to read.
131
+ # @parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
132
+ # @returns [String] The data read from the stream.
133
+ def readpartial(size = nil, buffer = nil)
134
+ read_partial(size, buffer) or raise EOFError, "Encountered done while reading data!"
112
135
  end
113
136
 
114
137
  # Find the index of a pattern in the read buffer, reading more data if needed.
@@ -116,17 +139,28 @@ module IO::Stream
116
139
  # @parameter offset [Integer] The offset to start searching from.
117
140
  # @parameter limit [Integer | Nil] The maximum number of bytes to read while searching.
118
141
  # @returns [Integer | Nil] The index of the pattern, or nil if not found.
119
- private def index_of(pattern, offset, limit)
142
+ private def index_of(pattern, offset, limit, discard = false)
120
143
  # We don't want to split on the pattern, so we subtract the size of the pattern.
121
144
  split_offset = pattern.bytesize - 1
122
-
145
+
123
146
  until index = @read_buffer.index(pattern, offset)
124
147
  offset = @read_buffer.bytesize - split_offset
125
148
 
126
149
  offset = 0 if offset < 0
127
150
 
128
- return nil if limit and offset >= limit
129
- return nil unless fill_read_buffer
151
+ if limit and offset >= limit
152
+ return nil
153
+ end
154
+
155
+ unless fill_read_buffer
156
+ return nil
157
+ end
158
+
159
+ if discard
160
+ # If we are discarding, we should consume the read buffer up to the offset:
161
+ consume_read_buffer(offset)
162
+ offset = 0
163
+ end
130
164
  end
131
165
 
132
166
  return index
@@ -136,7 +170,8 @@ module IO::Stream
136
170
  # @parameter pattern [String] The pattern to match.
137
171
  # @parameter offset [Integer] The offset to start searching from.
138
172
  # @parameter limit [Integer] The maximum number of bytes to read, including the pattern (even if chomped).
139
- # @returns [String | Nil] The contents of the stream up until the pattern, which is consumed but not returned.
173
+ # @parameter chomp [Boolean] Whether to remove the pattern from the returned data.
174
+ # @returns [String | Nil] The contents of the stream up until the pattern, or nil if the pattern was not found.
140
175
  def read_until(pattern, offset = 0, limit: nil, chomp: true)
141
176
  if index = index_of(pattern, offset, limit)
142
177
  return nil if limit and index >= limit
@@ -149,6 +184,28 @@ module IO::Stream
149
184
  end
150
185
  end
151
186
 
187
+ # Efficiently discard data from the stream until encountering pattern.
188
+ # @parameter pattern [String] The pattern to match.
189
+ # @parameter offset [Integer] The offset to start searching from.
190
+ # @parameter limit [Integer] The maximum number of bytes to read, including the pattern.
191
+ # @returns [String | Nil] The contents of the stream up until the pattern, or nil if the pattern was not found.
192
+ def discard_until(pattern, offset = 0, limit: nil)
193
+ if index = index_of(pattern, offset, limit, true)
194
+ @read_buffer.freeze
195
+
196
+ if limit and index >= limit
197
+ @read_buffer = @read_buffer.byteslice(limit, @read_buffer.bytesize)
198
+
199
+ return nil
200
+ end
201
+
202
+ matched = @read_buffer.byteslice(0, index+pattern.bytesize)
203
+ @read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize)
204
+
205
+ return matched
206
+ end
207
+ end
208
+
152
209
  # Peek at data in the buffer without consuming it.
153
210
  # @parameter size [Integer | Nil] The number of bytes to peek at. If nil, peek at all available data.
154
211
  # @returns [String] The data in the buffer without consuming it.
@@ -296,25 +353,43 @@ module IO::Stream
296
353
 
297
354
  # Consumes at most `size` bytes from the buffer.
298
355
  # @parameter size [Integer | Nil] The amount of data to consume. If nil, consume entire buffer.
299
- def consume_read_buffer(size = nil)
356
+ # @parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
357
+ # @returns [String | Nil] The consumed data, or nil if no data available.
358
+ def consume_read_buffer(size = nil, buffer = nil)
300
359
  # If we are at done, and the read buffer is empty, we can't consume anything.
301
- return nil if @done && @read_buffer.empty?
360
+ if @done && @read_buffer.empty?
361
+ # Clear the buffer even when returning nil
362
+ if buffer
363
+ buffer.clear
364
+ buffer.force_encoding(Encoding::BINARY)
365
+ end
366
+ return nil
367
+ end
302
368
 
303
369
  result = nil
304
370
 
305
371
  if size.nil? or size >= @read_buffer.bytesize
306
372
  # Consume the entire read buffer:
307
- result = @read_buffer
373
+ if buffer
374
+ buffer.clear
375
+ buffer << @read_buffer
376
+ result = buffer
377
+ else
378
+ result = @read_buffer
379
+ end
308
380
  @read_buffer = StringBuffer.new
309
381
  else
310
- # This approach uses more memory.
311
- # result = @read_buffer.slice!(0, size)
312
-
313
382
  # We know that we are not going to reuse the original buffer.
314
383
  # But byteslice will generate a hidden copy. So let's freeze it first:
315
384
  @read_buffer.freeze
316
385
 
317
- result = @read_buffer.byteslice(0, size)
386
+ if buffer
387
+ # Use replace instead of clear + << for better performance
388
+ buffer.replace(@read_buffer.byteslice(0, size))
389
+ result = buffer
390
+ else
391
+ result = @read_buffer.byteslice(0, size)
392
+ end
318
393
  @read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize)
319
394
  end
320
395
 
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2023-2024, by Samuel Williams.
5
5
 
6
6
  module IO::Stream
7
- VERSION = "0.7.0"
7
+ VERSION = "0.9.0"
8
8
  end
data/readme.md CHANGED
@@ -12,6 +12,15 @@ Please see the [project documentation](https://socketry.github.io/io-stream) for
12
12
 
13
13
  Please see the [project releases](https://socketry.github.io/io-streamreleases/index) for all releases.
14
14
 
15
+ ### v0.9.0
16
+
17
+ - Add support for `buffer` parameter in `read`, `read_exactly`, and `read_partial` methods to allow reading into a provided buffer.
18
+
19
+ ### v0.8.0
20
+
21
+ - On Ruby v3.3+, use `IO#write` directly instead of `IO#write_nonblock`, for better performance.
22
+ - Introduce support for `Readable#discard_until` method to discard data until a specific pattern is found.
23
+
15
24
  ### v0.7.0
16
25
 
17
26
  - Split stream functionality into separate `Readable` and `Writable` modules for better modularity and composition.
@@ -53,15 +62,6 @@ Please see the [project releases](https://socketry.github.io/io-streamreleases/i
53
62
 
54
63
  - Add convenient `IO.Stream()` constructor method for creating buffered streams.
55
64
 
56
- ### v0.3.0
57
-
58
- - Add support for timeouts with compatibility shims for various IO types.
59
-
60
- ### v0.2.0
61
-
62
- - Prefer `write_nonblock` in `syswrite` implementation for better non-blocking behavior.
63
- - Add test cases for crash scenarios.
64
-
65
65
  ## See Also
66
66
 
67
67
  - [async-io](https://github.com/socketry/async-io) — Where this implementation originally came from.
data/releases.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Releases
2
2
 
3
+ ## v0.9.0
4
+
5
+ - Add support for `buffer` parameter in `read`, `read_exactly`, and `read_partial` methods to allow reading into a provided buffer.
6
+
7
+ ## v0.8.0
8
+
9
+ - On Ruby v3.3+, use `IO#write` directly instead of `IO#write_nonblock`, for better performance.
10
+ - Introduce support for `Readable#discard_until` method to discard data until a specific pattern is found.
11
+
3
12
  ## v0.7.0
4
13
 
5
14
  - Split stream functionality into separate `Readable` and `Writable` modules for better modularity and composition.
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.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file