aboutsummaryrefslogtreecommitdiffstats
path: root/lib/plum/hpack/decoder.rb
blob: f80494f364d524480d5f8dfe2b94b112eb15c9ba (plain)
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
143
144
145
146
147
148
149
150
151
152
153
154
# frozen-string-literal: true

using Plum::BinaryString

module Plum
  module HPACK
    class Decoder
      include HPACK::Context

      def initialize(dynamic_table_limit)
        super
      end

      def decode(str)
        headers = []
        pos = 0
        lpos = str.bytesize
        while pos < lpos
          l, succ = parse(str, pos)
          pos += succ
          headers << l if l
        end
        headers
      end

      private
      def parse(str, pos)
        first_byte = str.getbyte(pos)
        if first_byte >= 0x80 # 0b1XXXXXXX
          parse_indexed(str, pos)
        elsif first_byte >= 0x40 # 0b01XXXXXX
          parse_indexing(str, pos)
        elsif first_byte >= 0x20 # 0b001XXXXX
          self.limit, succ = read_integer(str, pos, 5)
          [nil, succ]
        else # 0b0000XXXX (without indexing) or 0b0001XXXX (never indexing)
          parse_no_indexing(str, pos)
        end
      end

      def read_integer(str, pos, prefix_length)
        raise HPACKError.new("integer: end of buffer") if str.empty?

        mask = 1 << prefix_length
        ret = str.getbyte(pos) % mask
        return [ret, 1] if ret != mask - 1

        octets = 0
        while next_value = str.uint8(pos + octets + 1)
          ret += (next_value % 0x80) << (7 * octets)
          octets += 1

          if next_value < 0x80
            return [ret, 1 + octets]
          elsif octets == 4 # RFC 7541 5.1 tells us that we MUST have limitation. at least > 2 ** 28
            raise HPACKError.new("integer: too large integer")
          end
        end

        raise HPACKError.new("integer: end of buffer")
      end

      def read_string(str, pos)
        raise HPACKError.new("string: end of buffer") if str.empty?
        first_byte = str.uint8(pos)

        huffman = first_byte > 0x80
        length, ilen = read_integer(str, pos, 7)
        raise HTTPError.new("string: end of buffer") if str.bytesize < length

        bin = str.byteslice(pos + ilen, length)
        if huffman
          [Huffman.decode(bin), ilen + length]
        else
          [bin, ilen + length]
        end
      end

      def parse_indexed(str, pos)
        # indexed
        # +---+---+---+---+---+---+---+---+
        # | 1 |        Index (7+)         |
        # +---+---------------------------+
        index, succ = read_integer(str, pos, 7)
        [fetch(index), succ]
      end

      def parse_indexing(str, pos)
        # +---+---+---+---+---+---+---+---+
        # | 0 | 1 |      Index (6+)       |
        # +---+---+-----------------------+
        # | H |     Value Length (7+)     |
        # +---+---------------------------+
        # | Value String (Length octets)  |
        # +-------------------------------+
        # or
        # +---+---+---+---+---+---+---+---+
        # | 0 | 1 |           0           |
        # +---+---+-----------------------+
        # | H |     Name Length (7+)      |
        # +---+---------------------------+
        # |  Name String (Length octets)  |
        # +---+---------------------------+
        # | H |     Value Length (7+)     |
        # +---+---------------------------+
        # | Value String (Length octets)  |
        # +-------------------------------+
        ret, len = parse_literal(str, pos, 6)
        store(*ret)

        [ret, len]
      end

      def parse_no_indexing(str, pos)
        # +---+---+---+---+---+---+---+---+
        # | 0 | 0 | 0 |0,1|  Index (4+)   |
        # +---+---+-----------------------+
        # | H |     Value Length (7+)     |
        # +---+---------------------------+
        # | Value String (Length octets)  |
        # +-------------------------------+
        # or
        # +---+---+---+---+---+---+---+---+
        # | 0 | 0 | 0 |0,1|       0       |
        # +---+---+-----------------------+
        # | H |     Name Length (7+)      |
        # +---+---------------------------+
        # |  Name String (Length octets)  |
        # +---+---------------------------+
        # | H |     Value Length (7+)     |
        # +---+---------------------------+
        # | Value String (Length octets)  |
        # +-------------------------------+
        ret, len = parse_literal(str, pos, 4)

        [ret, len]
      end

      def parse_literal(str, pos, pref)
        index, ilen = read_integer(str, pos, pref)
        if index == 0
          name, nlen = read_string(str, pos + ilen)
        else
          name, = fetch(index)
          nlen = 0
        end

        val, vlen = read_string(str, pos + ilen + nlen)

        [[name, val], ilen + nlen + vlen]
      end
    end
  end
end