Class: IO::LikeHelpers::CharacterIO

Inherits:
Object
  • Object
show all
Includes:
RubyFacts
Defined in:
lib/io/like_helpers/character_io.rb,
lib/io/like_helpers/character_io/basic_reader.rb,
lib/io/like_helpers/character_io/converter_reader.rb

Overview

This class implements a stream that reads or writes characters to or from a byte oriented stream.

Defined Under Namespace

Classes: BasicReader, ConverterReader

Constant Summary

Constants included from RubyFacts

RubyFacts::RBVER_LT_3_0, RubyFacts::RBVER_LT_3_0_4, RubyFacts::RBVER_LT_3_1, RubyFacts::RBVER_LT_3_2, RubyFacts::RBVER_LT_3_3, RubyFacts::RBVER_LT_3_4

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(buffered_io, blocking_io = buffered_io, encoding_opts: {}, external_encoding: nil, internal_encoding: nil, sync: false) ⇒ CharacterIO

Creates a new intance of this class.

Parameters:

  • buffered_io (LikeHelpers::BufferedIO)

    a readable and/or writable stream that always blocks

  • blocking_io (LikeHelpers::BlockingIO) (defaults to: buffered_io)

    a readable and/or writable stream that always blocks

  • internal_encoding (Encoding, String) (defaults to: nil)

    the internal encoding

  • external_encoding (Encoding, String) (defaults to: nil)

    the external encoding

  • encoding_opts (Hash) (defaults to: {})

    options to be passed to String#encode

  • sync (Boolean) (defaults to: false)

    when ‘true` causes write operations to bypass internal buffering

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/io/like_helpers/character_io.rb', line 26

def initialize(
  buffered_io,
  blocking_io = buffered_io,
  encoding_opts: {},
  external_encoding: nil,
  internal_encoding: nil,
  sync: false
)
  raise ArgumentError, 'buffered_io cannot be nil' if buffered_io.nil?
  raise ArgumentError, 'blocking_io cannot be nil' if blocking_io.nil?

  @buffered_io = buffered_io
  @blocking_io = blocking_io
  self.sync = sync

  set_encoding(external_encoding, internal_encoding, **encoding_opts)
end

Instance Attribute Details

#blocking_ioObject

Returns the value of attribute blocking_io.



45
46
47
# File 'lib/io/like_helpers/character_io.rb', line 45

def blocking_io
  @blocking_io
end

#buffered_ioObject

Returns the value of attribute buffered_io.



44
45
46
# File 'lib/io/like_helpers/character_io.rb', line 44

def buffered_io
  @buffered_io
end

#external_encodingObject (readonly)

The external encoding of this stream.



58
59
60
# File 'lib/io/like_helpers/character_io.rb', line 58

def external_encoding
  @external_encoding
end

#internal_encodingObject (readonly)

The internal encoding of this stream. This is only used for read operations.



63
64
65
# File 'lib/io/like_helpers/character_io.rb', line 63

def internal_encoding
  @internal_encoding
end

Instance Method Details

#buffer_empty?Boolean

Returns ‘true` if the read buffer is empty and `false` otherwise.

Returns:

  • (Boolean)


51
52
53
54
# File 'lib/io/like_helpers/character_io.rb', line 51

def buffer_empty?
  return true unless readable?
  character_reader.empty?
end

#clearnil

Clears the state of this stream.

Returns:

  • (nil)


226
227
228
229
230
# File 'lib/io/like_helpers/character_io.rb', line 226

def clear
  return unless @character_reader
  @character_reader.clear
  nil
end

#read_allString

Reads all remaining characters from the stream.

Returns:

  • (String)

    a buffer containing the characters that were read

Raises:

  • (Encoding::InvalidByteSequenceError)

    if character conversion is being performed and the next sequence of bytes are invalid in the external encoding

  • (EOFError)

    when reading at the end of the stream

  • (IOError)

    if the stream is not readable



75
76
77
# File 'lib/io/like_helpers/character_io.rb', line 75

def read_all
  read_all_internal
end

#read_charString

Returns the next character from the stream.

Returns:

  • (String)

    a buffer containing the character that was read

Raises:

  • (EOFError)

    when reading at the end of the stream

  • (IOError)

    if the stream is not readable



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/io/like_helpers/character_io.rb', line 86

def read_char
  char = nil

  begin
    # The delegate's read buffer will have at least 1 byte in it at this
    # point.
    loop do
      buffer = character_reader.content
      char = buffer.force_encoding(character_reader.encoding)[0]
      # Return the next character if it is valid for the encoding.
      break if ! char.nil? && char.valid_encoding?
      # Or if the buffer has more than 16 bytes in it, valid or not.
      break if buffer.bytesize >= 16

      character_reader.refill(false)
      # At least 1 byte was added to the buffer, so try again.
    end
  rescue EOFError, IOError
    # Reraise when no bytes were available.
    raise if char.nil?
  end

  character_reader.consume(char.bytesize)
  char
end

#read_line(separator: $/, limit: nil, chomp: false, discard_newlines: false) ⇒ String

Returns the next line from the stream.

Parameters:

  • separator (String, nil) (defaults to: $/)

    a non-empty String that separates each line, an empty String that equates to 2 or more successive newlines as the separator, or ‘nil` to indicate reading all remaining data

  • limit (Integer, nil) (defaults to: nil)

    an Integer limiting the number of bytes returned in each line or ‘nil` to indicate no limit

  • chomp (Boolean) (defaults to: false)

    when ‘true` trailing newlines and carriage returns will be removed from each line; ignored when `separator` is `nil`

Returns:

  • (String)

    a buffer containing the characters that were read

Raises:

  • (EOFError)

    when reading at the end of the stream

  • (IOError)

    if the stream is not readable



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/io/like_helpers/character_io.rb', line 127

def read_line(separator: $/, limit: nil, chomp: false, discard_newlines: false)
  return read_all_internal(chomp: chomp) if ! (separator || limit)

  if String === separator && separator.encoding != Encoding::BINARY
    separator = separator.encode(character_reader.encoding).b
  end
  content = ''.b

  return content.force_encoding(character_reader.encoding) if limit == 0

  begin
    self.discard_newlines if discard_newlines

    index = nil
    extra = 0
    need_more = false
    offset = 0
    loop do
      already_consumed = content.bytesize
      content << character_reader.content

      if separator && ! index
        if Regexp === separator
          match = content.match(separator, offset)
          if match
            index = match.end(0)
            # Truncate the content to the end of the separator.
            content.slice!(index..-1)
          end
        else
          index = content.index(separator, offset)
          if index
            index += separator.bytesize
            # Truncate the content to the end of the separator.
            content.slice!(index..-1)
          else
            # Optimize the search that happens in the next loop iteration by
            # excluding the range of bytes already searched.
            offset = [0, content.bytesize - separator.bytesize + 1].max
          end
        end
      end

      if limit && content.bytesize >= limit
        # Truncate the content to no more than limit + 16 bytes in order to
        # ensure that the last character is not truncated at the limit
        # boundary.
        need_more =
          loop do
            last_character =
              content[0, limit + extra]
              .force_encoding(character_reader.encoding)[-1]
            # No more bytes are needed because the last character is whole and
            # valid or we hit the limit + 16 bytes hard limit.
            break false if last_character.valid_encoding?
            break false if extra >= 16
            extra += 1
            # More bytes are needed, but the end of the character buffer has
            # been reached.
            break true if limit + extra > content.bytesize
          end

        content.slice!((limit + extra)..-1) unless need_more
      end

      character_reader.consume(content.bytesize - already_consumed)

      # The separator string was found.
      break if index
      # The limit was reached.
      break if limit && content.bytesize >= limit && ! need_more

      character_reader.refill(false)
    end

    self.discard_newlines if discard_newlines
  rescue EOFError
    raise if content.empty?
  end

  # Remove the separator when requested.
  content.slice!(separator) if chomp && separator

  content.force_encoding(character_reader.encoding)
end

#readable?Boolean

Returns ‘true` if the stream is readable and `false` otherwise.

Returns:

  • (Boolean)


217
218
219
220
# File 'lib/io/like_helpers/character_io.rb', line 217

def readable?
  return @readable if defined?(@readable) && ! @readable.nil?
  @readable = buffered_io.readable?
end

#set_encoding(external, internal, **opts) ⇒ nil

Sets the external and internal encodings of the stream.

Parameters:

  • external (Encoding, nil)

    the external encoding

  • internal (Encoding, nil)

    the internal encoding

  • opts (Hash)

    encoding conversion options used when character or newline conversion is performed

Returns:

  • (nil)


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/io/like_helpers/character_io.rb', line 241

def set_encoding(external, internal, **opts)
  if external.nil? && ! internal.nil?
    raise ArgumentError,
      'external encoding cannot be nil when internal encoding is not nil'
  end

  internal = nil if internal == external

  self.encoding_opts = opts
  @internal_encoding = internal
  @external_encoding = external
  @character_reader = nil

  nil
end

#sync=(sync) ⇒ Boolean

When set to ‘true` the internal write buffer will be bypassed. Any data currently in the buffer will be flushed prior to the next output operation. When set to `false`, the internal write buffer will be enabled.

Parameters:

  • sync (Boolean)

    the sync mode

Returns:

  • (Boolean)

    the given value for ‘sync`



265
266
267
# File 'lib/io/like_helpers/character_io.rb', line 265

def sync=(sync)
  @sync = sync ? true : false
end

#sync?Boolean

Returns ‘true` if the internal write buffer is being bypassed and `false` otherwise.

Returns:

  • (Boolean)

    ‘true` if the internal write buffer is being bypassed and `false` otherwise



272
273
274
# File 'lib/io/like_helpers/character_io.rb', line 272

def sync?
  @sync ||= false
end

#unread(buffer, length: buffer.bytesize) ⇒ nil

Places bytes at the beginning of the read buffer.

Parameters:

  • buffer (String)

    the bytes to insert into the read buffer

  • length (Integer) (defaults to: buffer.bytesize)

    the number of bytes from the beginning of ‘buffer` to insert into the read buffer

Returns:

  • (nil)

Raises:

  • (IOError)

    if the remaining space in the internal buffer is insufficient to contain the given data

  • (IOError)

    if the stream is not readable



288
289
290
291
292
293
294
295
# File 'lib/io/like_helpers/character_io.rb', line 288

def unread(buffer, length: buffer.bytesize)
  length = Integer(length)
  raise ArgumentError, 'length must be at least 0' if length < 0

  assert_readable

  character_reader(length).unread(buffer.b, length: length)
end

#writable?Boolean

Returns ‘true` if the stream is writable and `false` otherwise.

Returns:

  • (Boolean)


301
302
303
304
# File 'lib/io/like_helpers/character_io.rb', line 301

def writable?
  return @writable if defined?(@writable) && ! @writable.nil?
  @writable = buffered_io.writable?
end

#write(buffer) ⇒ Integer

Writes characters to the stream, performing character and newline conversion first if necessary.

This method always blocks until all data is written.

Parameters:

  • buffer (String)

    the characters to write

Returns:

  • (Integer)

    the number of bytes written, after conversion

Raises:

  • (IOError)

    if the stream is not writable



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/io/like_helpers/character_io.rb', line 317

def write(buffer)
  assert_writable

  target_encoding = external_encoding
  if target_encoding.nil? || target_encoding == Encoding::BINARY
    target_encoding = buffer.encoding
  end
  if target_encoding != buffer.encoding || ! encoding_opts_w.empty?
    buffer = buffer.encode(target_encoding, **encoding_opts_w)
  end

  writer = sync? ? blocking_io : buffered_io
  buffer = buffer.b
  bytes_written = 0
  while bytes_written < buffer.bytesize do
    bytes_written += writer.write(buffer[bytes_written..-1])
  end
  bytes_written
end