io-like 0.3.1 → 0.4.0.pre1
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 +7 -0
- data/LICENSE +1 -1
- data/NEWS.md +14 -1
- data/README.md +75 -94
- data/lib/io/like.rb +1916 -1314
- data/lib/io/like_helpers/abstract_io.rb +512 -0
- data/lib/io/like_helpers/blocking_io.rb +86 -0
- data/lib/io/like_helpers/buffered_io.rb +555 -0
- data/lib/io/like_helpers/character_io/basic_reader.rb +122 -0
- data/lib/io/like_helpers/character_io/converter_reader.rb +252 -0
- data/lib/io/like_helpers/character_io.rb +529 -0
- data/lib/io/like_helpers/delegated_io.rb +250 -0
- data/lib/io/like_helpers/duplexed_io.rb +259 -0
- data/lib/io/like_helpers/io.rb +21 -0
- data/lib/io/like_helpers/io_wrapper.rb +290 -0
- data/lib/io/like_helpers/pipeline.rb +77 -0
- data/lib/io/like_helpers/ruby_facts.rb +33 -0
- data/lib/io/like_helpers.rb +14 -0
- metadata +107 -224
- data/.yardopts +0 -1
- data/Rakefile +0 -228
- data/ruby.1.8.mspec +0 -7
- data/spec/binmode_spec.rb +0 -29
- data/spec/close_read_spec.rb +0 -64
- data/spec/close_spec.rb +0 -36
- data/spec/close_write_spec.rb +0 -61
- data/spec/closed_spec.rb +0 -16
- data/spec/each_byte_spec.rb +0 -38
- data/spec/each_line_spec.rb +0 -11
- data/spec/each_spec.rb +0 -11
- data/spec/eof_spec.rb +0 -11
- data/spec/fixtures/classes.rb +0 -96
- data/spec/fixtures/gets.txt +0 -9
- data/spec/fixtures/numbered_lines.txt +0 -5
- data/spec/fixtures/one_byte.txt +0 -1
- data/spec/fixtures/paragraphs.txt +0 -7
- data/spec/fixtures/readlines.txt +0 -6
- data/spec/flush_spec.rb +0 -8
- data/spec/getc_spec.rb +0 -44
- data/spec/gets_spec.rb +0 -212
- data/spec/isatty_spec.rb +0 -6
- data/spec/lineno_spec.rb +0 -84
- data/spec/output_spec.rb +0 -47
- data/spec/pos_spec.rb +0 -53
- data/spec/print_spec.rb +0 -97
- data/spec/printf_spec.rb +0 -24
- data/spec/putc_spec.rb +0 -57
- data/spec/puts_spec.rb +0 -99
- data/spec/read_spec.rb +0 -162
- data/spec/readchar_spec.rb +0 -49
- data/spec/readline_spec.rb +0 -60
- data/spec/readlines_spec.rb +0 -140
- data/spec/readpartial_spec.rb +0 -92
- data/spec/rewind_spec.rb +0 -56
- data/spec/seek_spec.rb +0 -72
- data/spec/shared/each.rb +0 -204
- data/spec/shared/eof.rb +0 -116
- data/spec/shared/pos.rb +0 -39
- data/spec/shared/tty.rb +0 -12
- data/spec/shared/write.rb +0 -53
- data/spec/sync_spec.rb +0 -56
- data/spec/sysread_spec.rb +0 -87
- data/spec/sysseek_spec.rb +0 -68
- data/spec/syswrite_spec.rb +0 -60
- data/spec/tell_spec.rb +0 -7
- data/spec/to_io_spec.rb +0 -19
- data/spec/tty_spec.rb +0 -6
- data/spec/ungetc_spec.rb +0 -118
- data/spec/write_spec.rb +0 -61
- data/spec_helper.rb +0 -49
- /data/{LICENSE-rubyspec → rubyspec/LICENSE} +0 -0
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'io/like_helpers/character_io/basic_reader'
|
3
|
+
|
4
|
+
class IO; module LikeHelpers; class CharacterIO
|
5
|
+
|
6
|
+
##
|
7
|
+
# This class is a character reader that converts bytes from one character
|
8
|
+
# encoding to another. It is also used when simply handling universal newline
|
9
|
+
# conversion.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class ConverterReader < BasicReader
|
13
|
+
# This comes from MRI.
|
14
|
+
MIN_BUFFER_SIZE = 128 * 1024
|
15
|
+
|
16
|
+
##
|
17
|
+
# Creates a new intance of this class.
|
18
|
+
#
|
19
|
+
# @param buffered_io [LikeHelpers::BufferedIO] a readable stream that always
|
20
|
+
# blocks
|
21
|
+
# @param buffer_size [Integer, nil] the size of the internal buffer
|
22
|
+
# @param encoding_opts [Hash]
|
23
|
+
# @param external_encoding [Encoding, nil] the encoding to apply to the
|
24
|
+
# content provided by the underlying stream
|
25
|
+
# @param internal_encoding [Encoding, nil] the encoding into which characters
|
26
|
+
# from the underlying stream are converted
|
27
|
+
#
|
28
|
+
# Note that `MIN_BUFFER_SIZE` is used for the buffer size when `buffer_size`
|
29
|
+
# is `nil` or less than `MIN_BUFFER_SIZE`.
|
30
|
+
#
|
31
|
+
# When `internal_encoding` is `nil`, character conversion is not performed,
|
32
|
+
# but newline conversion will still be performed if `encoding_opts` specifies
|
33
|
+
# such.
|
34
|
+
def initialize(
|
35
|
+
buffered_io,
|
36
|
+
buffer_size: MIN_BUFFER_SIZE,
|
37
|
+
encoding_opts: {},
|
38
|
+
external_encoding: nil,
|
39
|
+
internal_encoding: nil
|
40
|
+
)
|
41
|
+
super(buffered_io, encoding: internal_encoding || external_encoding)
|
42
|
+
|
43
|
+
if ! buffer_size || buffer_size < MIN_BUFFER_SIZE
|
44
|
+
buffer_size = MIN_BUFFER_SIZE
|
45
|
+
end
|
46
|
+
@buffer_size = buffer_size
|
47
|
+
@start_idx = @end_idx = @buffer_size
|
48
|
+
@buffer = "\0".b * @buffer_size
|
49
|
+
@converter = internal_encoding ?
|
50
|
+
Encoding::Converter.new(
|
51
|
+
external_encoding, internal_encoding, **encoding_opts
|
52
|
+
) :
|
53
|
+
Encoding::Converter.new('', '', **encoding_opts)
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Clears the state of this reader.
|
58
|
+
#
|
59
|
+
# @return [nil]
|
60
|
+
def clear
|
61
|
+
super
|
62
|
+
@start_idx = @end_idx = @buffer_size
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Returns the bytes of the buffer as a binary encoded String.
|
68
|
+
#
|
69
|
+
# The returned bytes should be encoded using the value of {#encoding} and
|
70
|
+
# `String#force_encoding`. Bytes are returned rather than characters because
|
71
|
+
# CharacterIO#read_line works on bytes for compatibility with the MRI
|
72
|
+
# implementation and working with characters would be inefficient in that
|
73
|
+
# case.
|
74
|
+
#
|
75
|
+
# @return [String] the bytes of the buffer
|
76
|
+
def content
|
77
|
+
@buffer[@start_idx..@end_idx-1]
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Consumes bytes from the front of the buffer.
|
82
|
+
#
|
83
|
+
# @param length [Integer] the number of bytes to consume
|
84
|
+
#
|
85
|
+
# @return [nil]
|
86
|
+
def consume(length)
|
87
|
+
existing_content_size = @end_idx - @start_idx
|
88
|
+
length = existing_content_size if length > existing_content_size
|
89
|
+
@start_idx += length
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Returns `true` if the read buffer is empty and `false` otherwise.
|
95
|
+
#
|
96
|
+
# @return [Boolean]
|
97
|
+
def empty?
|
98
|
+
@start_idx >= @end_idx
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Refills the buffer from the stream.
|
103
|
+
#
|
104
|
+
# @param many [Boolean] read and convert only 1 character when `false`;
|
105
|
+
# otherwise, read and convert many characters
|
106
|
+
#
|
107
|
+
# @return [nil]
|
108
|
+
#
|
109
|
+
# @raise [Encoding::InvalidByteSequenceError] if character conversion is being
|
110
|
+
# performed and the next sequence of bytes are invalid in the external
|
111
|
+
# encoding
|
112
|
+
# @raise [Encoding::UndefinedConversionError] if character conversion is being
|
113
|
+
# performed and the character read from the stream cannot be converted to a
|
114
|
+
# character in the target encoding
|
115
|
+
# @raise [EOFError] when reading at the end of the stream
|
116
|
+
# @raise [IOError] if the stream is not readable
|
117
|
+
# @raise [IOError] if the buffer is already full
|
118
|
+
def refill(many = true)
|
119
|
+
existing_content_size = @end_idx - @start_idx
|
120
|
+
# Nothing to do if the character buffer is already full.
|
121
|
+
return nil if existing_content_size >= @buffer_size
|
122
|
+
|
123
|
+
conversion_buffer = ''.b
|
124
|
+
|
125
|
+
conversion_options = Encoding::Converter::PARTIAL_INPUT
|
126
|
+
conversion_options |= Encoding::Converter::AFTER_OUTPUT unless many
|
127
|
+
|
128
|
+
begin
|
129
|
+
loop do
|
130
|
+
buffered_io.refill if buffered_io.read_buffer_empty?
|
131
|
+
input = buffered_io.peek
|
132
|
+
input_count = input.bytesize
|
133
|
+
|
134
|
+
result = @converter.primitive_convert(
|
135
|
+
input,
|
136
|
+
conversion_buffer,
|
137
|
+
0,
|
138
|
+
@buffer_size - existing_content_size,
|
139
|
+
conversion_options
|
140
|
+
)
|
141
|
+
|
142
|
+
case result
|
143
|
+
when :after_output, :source_buffer_empty, :destination_buffer_full
|
144
|
+
consumed = input_count - input.bytesize
|
145
|
+
buffered_io.skip(consumed)
|
146
|
+
break unless conversion_buffer.empty?
|
147
|
+
next
|
148
|
+
when :invalid_byte_sequence
|
149
|
+
putback = @converter.putback
|
150
|
+
consumed = input_count - input.bytesize - putback.bytesize
|
151
|
+
buffered_io.skip(consumed)
|
152
|
+
end
|
153
|
+
|
154
|
+
raise @converter.last_error
|
155
|
+
end
|
156
|
+
rescue EOFError
|
157
|
+
conversion_buffer << @converter.finish
|
158
|
+
# Ignore this if there is still buffered data.
|
159
|
+
raise if conversion_buffer.empty?
|
160
|
+
end
|
161
|
+
|
162
|
+
append_to_buffer(conversion_buffer.b)
|
163
|
+
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Places bytes at the beginning of the read buffer.
|
169
|
+
#
|
170
|
+
# @param buffer [String] the bytes to insert into the read buffer
|
171
|
+
# @param length [Integer] the number of bytes from the beginning of `buffer`
|
172
|
+
# to insert into the read buffer
|
173
|
+
#
|
174
|
+
# @return [nil]
|
175
|
+
#
|
176
|
+
# @raise [IOError] if the remaining space in the internal buffer is
|
177
|
+
# insufficient to contain the given data
|
178
|
+
def unread(buffer, length: buffer.bytesize)
|
179
|
+
existing_content_size = @end_idx - @start_idx
|
180
|
+
if length > @buffer_size - existing_content_size
|
181
|
+
raise IOError, 'insufficient buffer space for unread'
|
182
|
+
end
|
183
|
+
|
184
|
+
prepend_to_buffer(buffer.b[0, length])
|
185
|
+
|
186
|
+
nil
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
##
|
192
|
+
# Appends `content` to the end of the internal buffer, shifting existing
|
193
|
+
# content to the beginning of the buffer first.
|
194
|
+
#
|
195
|
+
# @param content [String] the bytes to insert into the buffer
|
196
|
+
#
|
197
|
+
# @return [nil]
|
198
|
+
def append_to_buffer(content)
|
199
|
+
shift_content_to_beginning
|
200
|
+
@buffer[@end_idx, content.bytesize] = content.b
|
201
|
+
@end_idx += content.bytesize
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# Prepends `content` to the beginning of the internal buffer, shifting
|
207
|
+
# existing content to the end of the buffer first.
|
208
|
+
#
|
209
|
+
# @param content [String] the bytes to insert into the buffer
|
210
|
+
#
|
211
|
+
# @return [nil]
|
212
|
+
def prepend_to_buffer(content)
|
213
|
+
shift_content_to_end
|
214
|
+
@start_idx -= content.bytesize
|
215
|
+
@buffer[@start_idx, content.bytesize] = content.b
|
216
|
+
nil
|
217
|
+
end
|
218
|
+
|
219
|
+
##
|
220
|
+
# Shifts data in the buffer to the beginning if it is not already at the
|
221
|
+
# beginning.
|
222
|
+
#
|
223
|
+
# @return [nil]
|
224
|
+
def shift_content_to_beginning
|
225
|
+
if @start_idx > 0
|
226
|
+
existing_content_size = @end_idx - @start_idx
|
227
|
+
if existing_content_size > 0
|
228
|
+
@buffer[0, existing_content_size] = @buffer[@start_idx, existing_content_size]
|
229
|
+
end
|
230
|
+
@start_idx = 0
|
231
|
+
@end_idx = existing_content_size
|
232
|
+
end
|
233
|
+
nil
|
234
|
+
end
|
235
|
+
|
236
|
+
##
|
237
|
+
# Shifts data in the buffer to the end if it is not already at the end.
|
238
|
+
#
|
239
|
+
# @return [nil]
|
240
|
+
def shift_content_to_end
|
241
|
+
if @end_idx < @buffer_size
|
242
|
+
existing_content_size = @end_idx - @start_idx
|
243
|
+
new_start_idx = @buffer_size - existing_content_size
|
244
|
+
@buffer[new_start_idx, existing_content_size] =
|
245
|
+
@buffer[@start_idx, existing_content_size]
|
246
|
+
@start_idx = new_start_idx
|
247
|
+
@end_idx = @buffer_size
|
248
|
+
end
|
249
|
+
nil
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end; end; end
|