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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -1
  3. data/NEWS.md +14 -1
  4. data/README.md +75 -94
  5. data/lib/io/like.rb +1916 -1314
  6. data/lib/io/like_helpers/abstract_io.rb +512 -0
  7. data/lib/io/like_helpers/blocking_io.rb +86 -0
  8. data/lib/io/like_helpers/buffered_io.rb +555 -0
  9. data/lib/io/like_helpers/character_io/basic_reader.rb +122 -0
  10. data/lib/io/like_helpers/character_io/converter_reader.rb +252 -0
  11. data/lib/io/like_helpers/character_io.rb +529 -0
  12. data/lib/io/like_helpers/delegated_io.rb +250 -0
  13. data/lib/io/like_helpers/duplexed_io.rb +259 -0
  14. data/lib/io/like_helpers/io.rb +21 -0
  15. data/lib/io/like_helpers/io_wrapper.rb +290 -0
  16. data/lib/io/like_helpers/pipeline.rb +77 -0
  17. data/lib/io/like_helpers/ruby_facts.rb +33 -0
  18. data/lib/io/like_helpers.rb +14 -0
  19. metadata +107 -224
  20. data/.yardopts +0 -1
  21. data/Rakefile +0 -228
  22. data/ruby.1.8.mspec +0 -7
  23. data/spec/binmode_spec.rb +0 -29
  24. data/spec/close_read_spec.rb +0 -64
  25. data/spec/close_spec.rb +0 -36
  26. data/spec/close_write_spec.rb +0 -61
  27. data/spec/closed_spec.rb +0 -16
  28. data/spec/each_byte_spec.rb +0 -38
  29. data/spec/each_line_spec.rb +0 -11
  30. data/spec/each_spec.rb +0 -11
  31. data/spec/eof_spec.rb +0 -11
  32. data/spec/fixtures/classes.rb +0 -96
  33. data/spec/fixtures/gets.txt +0 -9
  34. data/spec/fixtures/numbered_lines.txt +0 -5
  35. data/spec/fixtures/one_byte.txt +0 -1
  36. data/spec/fixtures/paragraphs.txt +0 -7
  37. data/spec/fixtures/readlines.txt +0 -6
  38. data/spec/flush_spec.rb +0 -8
  39. data/spec/getc_spec.rb +0 -44
  40. data/spec/gets_spec.rb +0 -212
  41. data/spec/isatty_spec.rb +0 -6
  42. data/spec/lineno_spec.rb +0 -84
  43. data/spec/output_spec.rb +0 -47
  44. data/spec/pos_spec.rb +0 -53
  45. data/spec/print_spec.rb +0 -97
  46. data/spec/printf_spec.rb +0 -24
  47. data/spec/putc_spec.rb +0 -57
  48. data/spec/puts_spec.rb +0 -99
  49. data/spec/read_spec.rb +0 -162
  50. data/spec/readchar_spec.rb +0 -49
  51. data/spec/readline_spec.rb +0 -60
  52. data/spec/readlines_spec.rb +0 -140
  53. data/spec/readpartial_spec.rb +0 -92
  54. data/spec/rewind_spec.rb +0 -56
  55. data/spec/seek_spec.rb +0 -72
  56. data/spec/shared/each.rb +0 -204
  57. data/spec/shared/eof.rb +0 -116
  58. data/spec/shared/pos.rb +0 -39
  59. data/spec/shared/tty.rb +0 -12
  60. data/spec/shared/write.rb +0 -53
  61. data/spec/sync_spec.rb +0 -56
  62. data/spec/sysread_spec.rb +0 -87
  63. data/spec/sysseek_spec.rb +0 -68
  64. data/spec/syswrite_spec.rb +0 -60
  65. data/spec/tell_spec.rb +0 -7
  66. data/spec/to_io_spec.rb +0 -19
  67. data/spec/tty_spec.rb +0 -6
  68. data/spec/ungetc_spec.rb +0 -118
  69. data/spec/write_spec.rb +0 -61
  70. data/spec_helper.rb +0 -49
  71. /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