File: composite_io.rb

package info (click to toggle)
ruby-http-form-data 2.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 204 kB
  • sloc: ruby: 861; makefile: 5
file content (88 lines) | stat: -rw-r--r-- 2,156 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# frozen_string_literal: true

require "stringio"

module HTTP
  module FormData
    # Provides IO interface across multiple IO objects.
    class CompositeIO
      # @param [Array<IO>] ios Array of IO objects
      def initialize(ios)
        @index  = 0
        @buffer = "".b
        @ios    = ios.map do |io|
          if io.is_a?(String)
            StringIO.new(io)
          elsif io.respond_to?(:read)
            io
          else
            raise ArgumentError,
              "#{io.inspect} is neither a String nor an IO object"
          end
        end
      end

      # Reads and returns partial content acrosss multiple IO objects.
      #
      # @param [Integer] length Number of bytes to retrieve
      # @param [String] outbuf String to be replaced with retrieved data
      #
      # @return [String, nil]
      def read(length = nil, outbuf = nil)
        data   = outbuf.clear.force_encoding(Encoding::BINARY) if outbuf
        data ||= "".b

        read_chunks(length) { |chunk| data << chunk }

        data unless length && data.empty?
      end

      # Returns sum of all IO sizes.
      def size
        @size ||= @ios.map(&:size).inject(0, :+)
      end

      # Rewinds all IO objects and set cursor to the first IO object.
      def rewind
        @ios.each(&:rewind)
        @index = 0
      end

      private

      # Yields chunks with total length up to `length`.
      def read_chunks(length = nil)
        while (chunk = readpartial(length))
          yield chunk.force_encoding(Encoding::BINARY)

          next if length.nil?

          length -= chunk.bytesize

          break if length.zero?
        end
      end

      # Reads chunk from current IO with length up to `max_length`.
      def readpartial(max_length = nil)
        while current_io
          chunk = current_io.read(max_length, @buffer)

          return chunk if chunk && !chunk.empty?

          advance_io
        end
      end

      # Returns IO object under the cursor.
      def current_io
        @ios[@index]
      end

      # Advances cursor to the next IO object.
      def advance_io
        @index += 1
      end
    end
  end
end