File: encoder.rb

package info (click to toggle)
ruby-aws-eventstream 1.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 108 kB
  • sloc: ruby: 283; makefile: 3
file content (142 lines) | stat: -rw-r--r-- 4,539 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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# frozen_string_literal: true

require 'zlib'

module Aws
  module EventStream

    # This class provides #encode method for encoding
    # Aws::EventStream::Message into binary.
    #
    # * {#encode} - encode Aws::EventStream::Message into binary
    #   when output IO-like object is provided, binary string
    #   would be written to IO. If not, the encoded binary string
    #   would be returned directly
    #
    # ## Examples
    #
    #   message = Aws::EventStream::Message.new(
    #     headers: {
    #       "foo" => Aws::EventStream::HeaderValue.new(
    #         value: "bar", type: "string"
    #        )
    #     },
    #     payload: "payload"
    #   )
    #   encoder = Aws::EventsStream::Encoder.new
    #   file = Tempfile.new
    #
    #   # encode into IO ouput
    #   encoder.encode(message, file)
    #
    #   # get encoded binary string
    #   encoded_message = encoder.encode(message)
    #
    #   file.read == encoded_message
    #   # => true
    #
    class Encoder

      # bytes of total overhead in a message, including prelude
      # and 4 bytes total message crc checksum
      OVERHEAD_LENGTH = 16

      # Maximum header length allowed (after encode) 128kb
      MAX_HEADERS_LENGTH = 131072

      # Maximum payload length allowed (after encode) 16mb
      MAX_PAYLOAD_LENGTH = 16777216

      # Encodes Aws::EventStream::Message to output IO when
      #   provided, else return the encoded binary string
      #
      # @param [Aws::EventStream::Message] message
      #
      # @param [IO#write, nil] io An IO-like object that
      #   responds to `#write`, encoded message will be
      #   written to this IO when provided
      #
      # @return [nil, String] when output IO is provided,
      #   encoded message will be written to that IO, nil
      #   will be returned. Else, encoded binary string is
      #   returned.
      def encode(message, io = nil)
        encoded = encode_message(message)
        if io
          io.write(encoded)
          io.close
        else
          encoded
        end
      end

      # Encodes an Aws::EventStream::Message
      #   into String
      #
      # @param [Aws::EventStream::Message] message
      #
      # @return [String]
      def encode_message(message)
        # create context buffer with encode headers
        encoded_header = encode_headers(message)
        header_length = encoded_header.bytesize
        # encode payload
        if message.payload.length > MAX_PAYLOAD_LENGTH
          raise Aws::EventStream::Errors::EventPayloadLengthExceedError.new
        end
        encoded_payload = message.payload.read
        total_length = header_length + encoded_payload.bytesize + OVERHEAD_LENGTH

        # create message buffer with prelude section
        encoded_prelude = encode_prelude(total_length, header_length)

        # append message context (headers, payload)
        encoded_content = [
          encoded_prelude,
          encoded_header,
          encoded_payload,
        ].pack('a*a*a*')
        # append message checksum
        message_checksum = Zlib.crc32(encoded_content)
        [encoded_content, message_checksum].pack('a*N')
      end

      # Encodes headers part of an Aws::EventStream::Message
      #   into String
      #
      # @param [Aws::EventStream::Message] message
      #
      # @return [String]
      def encode_headers(message)
        header_entries = message.headers.map do |key, value|
          encoded_key = [key.bytesize, key].pack('Ca*')

          # header value
          pattern, value_length, type_index = Types.pattern[value.type]
          encoded_value = [type_index].pack('C')
          # boolean types doesn't need to specify value
          next [encoded_key, encoded_value].pack('a*a*') if !!pattern == pattern
          encoded_value = [encoded_value, value.value.bytesize].pack('a*S>') unless value_length

          [
            encoded_key,
            encoded_value,
            pattern ? [value.value].pack(pattern) : value.value,
          ].pack('a*a*a*')
        end
        header_entries.join.tap do |encoded_header|
          break encoded_header if encoded_header.bytesize <= MAX_HEADERS_LENGTH
          raise Aws::EventStream::Errors::EventHeadersLengthExceedError.new
        end
      end

      private

      def encode_prelude(total_length, headers_length)
        prelude_body = [total_length, headers_length].pack('NN')
        checksum = Zlib.crc32(prelude_body)
        [prelude_body, checksum].pack('a*N')
      end
    end
  end
end